Compare commits

..

220 Commits

Author SHA1 Message Date
aks07
48f1a4cbf3 Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items 2026-05-06 20:57:43 +05:30
aks07
bb499973bf feat: remove antd component usage 2026-05-06 20:57:04 +05:30
dasmat
72036b42e3 fix(frontend): open trace details in new tab from funnel results (#10999)
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
2026-05-06 15:06:48 +00:00
aks07
13c087d34d feat: remove common component 2026-05-06 19:54:09 +05:30
aks07
feb9031bcd feat: packages remove 2026-05-06 19:17:51 +05:30
aks07
bc4a6b7ded feat: packages remove 2026-05-06 18:01:18 +05:30
aks07
9216bb5f34 Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items 2026-05-06 17:26:48 +05:30
aks07
18d2806f95 feat: minor refactors 2026-05-06 17:03:36 +05:30
aks07
8d666471e1 feat: v3 behind feature flag 2026-05-06 17:00:16 +05:30
aks07
9d022772b7 feat: minor refactors 2026-05-06 16:28:47 +05:30
aks07
648a48cbaa feat: minor refactors 2026-05-06 15:39:44 +05:30
Tushar Vats
9301b2fb1c fix: dashboard date refresh (#11201)
* fix: dashboard invalid date state upon refresh

* fix: dashboard invalid date state upon refresh

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2026-05-06 09:25:47 +00:00
primus-bot[bot]
6d0e60822c chore(release): bump to v0.122.0 (#11204)
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
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-05-06 08:01:25 +00:00
Abhi kumar
acdaef6c2e chore: added reference to crosspanel sync docs (#11200)
* chore: added reference to crosspanel sync docs

* chore: minor changes
2026-05-06 05:44:03 +00:00
Abhi kumar
a7690bdaa2 fix: added fix for all series in tooltip sync mode (#11197)
* fix: added fix for all series in tooltip sync mode

* chore: minor cleanup
2026-05-06 04:24:33 +00:00
Nityananda Gohain
a7fde606ca fix: use ms in prepareFillZeroArgsWithStep (#11196)
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
2026-05-05 18:27:19 +00:00
Vikrant Gupta
0aaf556137 Update CODEOWNERS (#11192)
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
2026-05-05 14:11:58 +00:00
Vinicius Lourenço
582ba1c677 chore(oxfmt): add more patterns to ignore (#11173) 2026-05-05 13:37:53 +00:00
Pandey
3d8cddf84e refactor: split typeable infrastructure into pkg/types/coretypes (#11105)
* refactor: move authtypes to coretypes

* refactor: migrate downstream consumers to coretypes Kind/Type/Relation

Wire all consumers of the typeable infrastructure through coretypes:
- Replace authtypes.Name/Type/Relation references with coretypes equivalents
- Switch Typeable singletons to constructor calls (authtypes.NewTypeableUser
  etc.), with the embedded coretypes.Typeable populated so Kind/Type/Prefix/
  Scope dispatch correctly through the embed
- Update dashboardtypes meta-resource declarations to use authtypes
  constructors so they expose Tuples (authz callers need it)
- Rename Resource.Name field accesses to Resource.Kind to match the field
  rename in authtypes.Resource
- Fix typeable_metaresource.go calling the plural NewTypeableMetaResources
  helper — should be the singular NewTypeableMetaResource

go build ./... and go vet ./... clean (parser-generated unreachable-code
warnings are pre-existing). Authz unit tests pass.

* refactor(audittypes): unify Action with coretypes.Relation

Drop the duplicate Action enum from audittypes — the verbs (create/update/
delete) match coretypes.Relation exactly. Move PastTense onto Relation so
audit EventName derivation continues to work without a parallel hierarchy.

Also retypes AuditDef.ResourceKind from string to coretypes.Kind so audit
declarations get the same regex validation that authz already enforces.

* refactor(retentiontypes): extract TTLSetting into its own package

TTLSetting is the bun model for ClickHouse TTL settings — has nothing to do
with the Organization domain it was previously co-located with in
pkg/types/organization.go. Moved to pkg/types/retentiontypes/ alongside the
ClickHouse reader that's its sole consumer.

No schema change; the bun table tag (table:ttl_setting) is unchanged.

* chore(openapi): regenerate spec for coretypes.Relation and Resource.Kind

* chore(frontend): regenerate API client and migrate Resource.name → Resource.kind

Regenerated TypeScript API types after the AuthtypesResource field rename
and the new CoretypesRelation enum. Updated:

- frontend/scripts/generate-permissions-type.cjs to read `r.kind` from the
  /api/v1/authz/resources response and emit `kind:` in the static
  permissions.type.ts file.
- frontend/src/hooks/useAuthZ/{permissions.type,types,utils,useAuthZ}.tsx:
  Resource.name → Resource.kind throughout.
- frontend/src/container/RolesSettings/{utils.tsx,__tests__/utils.test.ts}:
  same field migration.
- frontend/src/components/createGuardedRoute/createGuardedRoute.test.tsx:
  same.
- useAuthZ/utils.ts: cast string relations to CoretypesRelationDTO at the
  AuthtypesTransactionDTO boundary now that relation is an enum, not a raw
  string.

yarn generate:api passes (orval generation + lint + typecheck).

* refactor: migrate downstream consumers to Resource/Verb rename

* chore(openapi): regenerate spec for Resource/Verb rename

* feat(coretypes): add ListResources accessor with stable sort

* feat(cmd): add 'generate authz' subcommand for permissions type

* refactor(authz): drop runtime authz/resources endpoint

* refactor(frontend): consume static permissions.type.ts directly

* chore(frontend): regenerate Orval client without authz/resources

* ci: move authz schema check from jsci to goci

* refactor(coretypes): move Selector/Object/Transaction from authtypes

* feat(coretypes): add managed role names and permission policy

* feat(coretypes): add Registry assembling resources, types, and managed-role transactions

* refactor(authz): wire *coretypes.Registry; drop RegisterTypeable

* refactor(cmd): wire coretypes.NewRegistry into server bootstraps

* chore: regenerate openapi spec for authtypes -> coretypes type moves

* chore(frontend): regenerate API client for Authtypes -> Coretypes type moves

* refactor(coretypes): rename GettableResource to ResourceRef

* refactor(authz): collapse Registry around static data; bridge once at construction

* refactor(coretypes): tighten Registry, restore anonymous public-dashboard grant

Drops passthrough fields from coretypes.Registry; adds an O(1) lookup map
for NewResourceFromTypeAndKind; replaces stringly-typed Type compares with
Type.Equals; removes the now-redundant getUniqueTypes helper. Restores the
signoz-anonymous read grant on metaresource/public-dashboard that was
silently dropped, and removes the invalid signoz-admin/VerbCreate/TypeUser
entry that panicked at startup.

* chore: regenerate openapi spec for coretypes -> authtypes type moves

* chore(frontend): regenerate API client for Coretypes -> Authtypes type moves

* fix(authz): disambiguate kind→type by relation, preserve multi-part selectors

permissions.type.ts now lists the same kind (dashboard, role,
public-dashboard) under both metaresource and metaresources, so the prior
kind→type map silently overwrote one with the other. Resolve the type
using the requesting relation's allowed types, and slice the selector at
the first colon so multi-part selectors (e.g. id:version) round-trip
correctly. Updates useAuthZ.test.tsx to use the regenerated kind field.

* refactor(authtypes): introduce Relation wrapper over coretypes.Verb

The authz layer modeled relations as raw coretypes.Verb everywhere, which
forced authz-level concepts (action, role-binding) to share a type with
schema-level enumerations. Introduce authtypes.Relation as a thin wrapper
over coretypes.Verb so the authz APIs (CheckWithTupleCreation, ListObjects,
GetObjects, PatchObjects, NewTuples, Transaction.Relation, etc.) can grow
authz-specific affordances without leaking back into coretypes.

Also reshuffles the static coretypes data into dedicated registry_*.go files
(types, kinds, verbs, resources, managed roles) to keep the schema declarations
isolated from the value types they configure.

* refactor(authtypes): expose Relation.Enum() and regenerate openapi spec

Without an Enum() method on Relation the openapi generator emitted an
empty AuthtypesRelation schema (no allowed values). Forward the enum
from the embedded coretypes.Verb so the wire contract is faithful.

* refactor(ee/authz): drop always-nil error returns from managed-role tuple helpers

getManagedRoleGrantTuples and getManagedRoleTransactionTuples never
returned a non-nil error, which the linter (unparam) had flagged. Drop
the unused error return; callers no longer need the err check either.

* chore(frontend): regenerate API client for authtypes.Relation

* fix(authz): satisfy go-lint — keyed Relation literal, drop redundant Verb selector

* refactor(coretypes): sync Kinds slice with full registry_kind declarations

* feat(coretypes): register metaresource and metaresources for all new kinds

Adds 21 metaresource and 21 metaresources entries (covering notification-channel,
route-policy, apdex-setting, auth-domain, session, cloud-integration,
cloud-integration-service, ingestion-key, ingestion-limit, pipeline,
user-preference, org-preference, quick-filter, ttl-setting, rule,
planned-maintenance, saved-view, trace-funnel, factor-password, factor-api-key,
license) so the authz schema covers every resource Kind declared in
registry_kind. Regenerates the static frontend permissions.type.ts to match.

* feat(coretypes): populate ManagedRoleToTransactions from signozapiserver routes

Enumerates every (verb, resource) tuple each managed role holds, derived
from the AdminAccess/EditAccess/ViewAccess middleware on routes in
pkg/apiserver/signozapiserver and the legacy http_handler in
pkg/query-service/app. Admin gets 123 transactions, editor 53, viewer 25,
anonymous keeps the single public-dashboard read.

* feat(coretypes): add integration kind with full CRUD for viewer/editor/admin

Install/uninstall/list integration routes (legacy /api/v1/integrations) all
sit behind ViewAccess, so every authenticated role gets the full CRUD
surface on (metaresource, integration) and (metaresources, integration).
Regenerates the static frontend permissions.type.ts to match.

* feat(coretypes): add subscription kind alongside license, document LCRUD shape

License covers the in-product license resource (Activate/Refresh/GetActive).
Subscription is the billing lifecycle (checkout/portal/billing) served by
ee/query-service routes. Both are admin-only and modeled with a uniform
LCRUD shape; comments call out which verbs actually map to routes versus
which are placeholders for shape parity (e.g. cancellation flows through
Stripe's portal, not an in-process delete).

* feat(coretypes): model telemetryresource for logs, traces, metrics

Mirrors the telemetryresource type from ee/authz/openfgaschema/base.fga
into coretypes: a read-only Type with three Kinds (logs, traces, metrics)
matching telemetrytypes.Signal. Selector is wildcard-only for v1; future
work can narrow per-service or per-environment when the use case lands.
Every managed role (admin/editor/viewer) gets read on each signal,
matching the schema's role#assignee grant. Anonymous stays unchanged.
Regenerates the static frontend permissions.type.ts.

* feat(coretypes): add audit-logs and meter-metrics kinds under telemetryresource

Audit logs (signal=logs, source=audit) and meter metrics (signal=metrics,
source=meter) are sensitive source-qualified telemetry streams that don't
belong under the broad read-grant every role gets on regular logs/traces/
metrics. Modeled as distinct Kinds so they can be permissioned
independently. Admin-only read for now; widen on explicit ask (e.g. an
auditor flow that needs viewer access to audit-logs). Regenerates the
static frontend permissions.type.ts.

* feat(coretypes): add logs-field and traces-field kinds for stored field config

GET/POST /logs/fields and /api/v2/traces/fields manage stored, mutable
field metadata (indexed/promoted columns) over each signal. They're
configuration, not telemetry data, so they sit under metaresource rather
than telemetryresource. Viewer reads, editor/admin update; no
create/delete since POST overwrites. Plural prefix (logs-field /
traces-field) matches the signal naming.

* chore(frontend): regenerate permissions.type.ts to match generate authz output

* feat(authz): add attach permissions to fga model

* fix(tests): use role permissions instead of dashboards

* fix(authz): couple of issues with register flow

* fix(authz): public dashboard read should be anomymous

* fix(tests): integration test for public dashboard access

---------

Co-authored-by: vikrantgupta25 <vikrant@signoz.io>
2026-05-05 19:13:09 +05:30
Nikhil Soni
ac46cd8e80 fix: return span start time similar to waterfall v2 (#11183)
* fix: return span start time similar to waterfall v2

* chore: update openapi specs

* chore: rename timestamp field to match style of other fields

* chore: rename the struct field to keep json and field same
2026-05-05 11:50:18 +00:00
Abhi kumar
18d5e92ae2 fix: added fix for panel sync mode in non-view panels (#11187) 2026-05-05 10:06:28 +00:00
Vikrant Gupta
5eaca31759 chore(service-account): remove api keys deprecation banner (#11188) 2026-05-05 09:53:26 +00:00
Abhi kumar
8b0ccc8ddc feat: user dashboard preference (#11159)
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
build-staging / prepare (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: user dashboard preference

* chore: moved to module css

* chore: pr review changes

* chore: minor fixes

* feat: added changes for synced tooltip modes (#11175)

* feat: added changes for synced tooltip modes

* chore: pr review changes

* chore: minor fix

* chore: added changes for deleting dashboard preferences on dashboard delete
2026-05-05 08:07:33 +00:00
SagarRajput-7
1118136b69 feat: updated the cancel subscription banner styles and message (#11181)
* feat: updated the cancel subscription banner styles and message

* feat: cancel button update

* feat: updated the confirmation dialog styles and added 'cancel' input

* feat: added test cases

* feat: added test cases

* feat: updated messages

* feat: updated test cases

* feat: updated styles as per feedback
2026-05-05 07:41:59 +00:00
Abhi kumar
ae3f5114c4 chore: minor ui fixes in tooltip (#11099)
* chore: minor ui fixes in tooltip

* chore: preetify

* chore: exposed tooltip + added panelid in events

* chore: fixed and updated tooltip test

* chore: added tooltip footer tests

* chore: updated pr review changes and added support for multi query

* chore: minor fix
2026-05-05 06:45:59 +00:00
Pandey
8409a9798d fix(authdomain): nest config response, rename Updateable→Updatable, return Identifiable on create (#11176)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix(authdomain): nest config response, rename Updateable→Updatable, return Identifiable on create

Three small API-shape corrections on auth_domain:

- GettableAuthDomain previously embedded AuthDomainConfig, which
  flattened sso_enabled / saml_config / oidc_config / google_auth_config /
  role_mapping at the response root and made the response shape
  diverge from the request shape (PostableAuthDomain has them under
  `config`). Move it under a named `Config` field with a `config`
  json tag so request and response carry the same nested object.
- UpdateableAuthDomain → UpdatableAuthDomain (typo fix; aligns with
  UpdatableUser already in the codebase).
- CreateAuthDomain previously returned the full GettableAuthDomain;
  the only field clients actually need from the create response is
  the new ID. Switch to Identifiable so the contract states what the
  endpoint guarantees and clients re-Read for the full domain when
  needed.

Frontend schema and OpenAPI spec regenerated.

* fix(authdomain-frontend): adapt to nested config + Identifiable create response

Regenerate the orval client (`yarn generate:api`) and update the
auth-domain UI for the API shape changes from the previous commit:

- `record.ssoType`, `.ssoEnabled`, `.googleAuthConfig`, `.oidcConfig`,
  `.samlConfig`, `.roleMapping` are now nested under `record.config.*`
  in `AuthtypesGettableAuthDomainDTO` — update SSOEnforcementToggle,
  CreateEdit form initial-values, the list page's Configure button,
  and the auth-domain test mocks.
- `mockCreateSuccessResponse` now returns `{ id }` (Identifiable)
  instead of the full domain.

`yarn generate:api` ran clean: lint OK, tsgo OK.

* fix(authdomain): align CreateAuthDomain success code with handler + adjust integration test

The Create handler returns http.StatusCreated but the OpenAPI
annotation said StatusOK. Sync the annotation to 201, regenerate the
spec + frontend client.

The callbackauthn integration test (01_domain.py) still read
`domain["ssoType"]` off the GET response — now nested under
`domain["config"]["ssoType"]` after the previous shape change. Update
the assertion.
2026-05-04 20:44:41 +00:00
Vinicius Lourenço
de6e4890ae feat(query-search-v2): add initial expression support & store to manage (#11062)
* feat(query-search-v2): add initial expression support & store to manage

* fix(qbv2): format issue
2026-05-04 16:22:52 +00:00
Vinicius Lourenço
20dd264ac1 feat(infra-monitoring): use new table component (#11122)
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
* feat(infra-monitoring): use new table component

* test(k8s-base-list): try fix issue with flaky test

* fix(table): tweaks in the layout

* fix(pr-comments): usage of const and move disable lint to line

* fix(css): format of css file

* test(k8s-base-list): flaky test

* test(k8s-base-list): second try to fix flaky test

* fix(table): have different ids for expanded table

* fix(k8s-base-list): third attempt to de-flaky test

* refactor(table): tiny adjustments on table

* fix(k8s-empty-state): better title size
2026-05-04 13:16:23 +00:00
Vishal Sharma
8a7793794d feat(global): add ai assistant url to global config (#11171) 2026-05-04 13:06:33 +00:00
Pandey
680bcd08c3 fix(types): correct OpenAPI schema for AuthDomainConfig and PostableChannel (#11164)
* fix(authtypes): embed values and expose AuthDomainConfig oneOf

GettableAuthDomain now embeds StorableAuthDomain and AuthDomainConfig
by value so the response flattens correctly. AuthDomainConfig also
implements jsonschema.OneOfExposer over the SAML/Google/OIDC variants.

* fix(alertmanagertypes): expose PostableChannel JSONSchema

PostableChannel now implements jsonschema.Exposer, requiring name
and a oneOf branch per *_configs field so the OpenAPI request body
for POST /channels matches the runtime contract enforced in
NewChannelFromReceiver. Switched the route's Request type from
Receiver to PostableChannel and regenerated the OpenAPI spec.

* fix(alertmanagertypes): use components/schemas prefix in PostableChannel refs

The standalone reflector inside JSONSchema defaulted to #/definitions/
prefix, producing dangling refs to ConfigDiscordConfig etc. that broke
the generated frontend client. Pass DefinitionsPrefix("#/components/schemas/")
so refs point to existing OpenAPI components, and regenerate the frontend
Orval client.

* feat(authdomain): add GET /api/v1/domains/{id} endpoint

Returns a single GettableAuthDomain scoped to the caller's organization,
backed by the existing module.GetByOrgIDAndID. Adds Get to the Handler
interface, wires the route under AdminAccess, and regenerates the
OpenAPI spec and frontend Orval client.

* feat(authtypes): expose AuthNProvider enum in OpenAPI schema

AuthNProvider now implements jsonschema.Enum, narrowing the generated
TypeScript type from string to a typed enum. Updated callers in the
auth-domain settings UI and mocks to use AuthtypesAuthNProviderDTO,
and added an early-return guard in the create/edit submit handler so
TS can narrow the union before passing it as ssoType.

* chore(types): document oneOf/discriminator mismatch on PostableChannel and AuthDomainConfig

Both types emit a oneOf in the OpenAPI spec but neither shape supports an
OpenAPI discriminator: PostableChannel implies the variant by which *_configs
field is present, and AuthDomainConfig keeps the variant payload in a
sibling field instead of being the payload itself. Leave a TODO pointing at
ruletypes.RuleThresholdData as the envelope pattern to migrate to.

* fix(ruletypes): handle string driver values in Schedule.Scan and Recurrence.Scan

The Scan methods only handled []byte and silently no-op'd on anything
else. SQLite's TEXT columns come back as string from the driver, so
every GET of a planned_maintenance returned a zero-valued Schedule
(empty timezone, 0001-01-01 startTime/endTime, no recurrence) — even
though Create + Update wrote the values correctly.

Switch on src type, accept []byte, string, and nil; error on anything
else. Aligns Schedule with the existing pattern; in Recurrence fixes
the receiver — Unmarshal was being passed src (the interface{} arg)
rather than r.
2026-05-04 18:00:43 +05:30
aks07
eb22c57a67 feat: minor change 2026-05-04 17:43:31 +05:30
aks07
896379b680 feat: minor change 2026-05-04 17:34:32 +05:30
aks07
f041b16e4b feat: delete waterfall go 2026-05-04 17:04:39 +05:30
aks07
9ca3a7fd3e Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items 2026-05-04 16:57:14 +05:30
aks07
43e122367c Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items
# Conflicts:
#	docs/api/openapi.yml
#	frontend/package.json
#	frontend/src/api/generated/services/sigNoz.schemas.ts
#	frontend/src/api/generated/services/tracedetail/index.ts
#	frontend/src/auto-import-registry.d.ts
#	frontend/src/components/LogDetail/index.tsx
#	frontend/src/hooks/trace/useCopySpanLink.ts
#	frontend/src/styles.scss
#	frontend/yarn.lock
#	pkg/apiserver/signozapiserver/provider.go
#	pkg/apiserver/signozapiserver/tracedetail.go
#	pkg/modules/tracedetail/impltracedetail/handler.go
#	pkg/modules/tracedetail/impltracedetail/module.go
#	pkg/modules/tracedetail/impltracedetail/store.go
#	pkg/modules/tracedetail/impltracedetail/waterfall_test.go
#	pkg/signoz/module.go
#	pkg/signoz/openapi.go
#	pkg/signoz/provider.go
#	pkg/types/tracedetailtypes/store.go
#	pkg/types/tracedetailtypes/waterfall.go
2026-05-04 16:56:32 +05:30
Vinicius Lourenço
5cf0e0fbb9 Reapply "feat(global-time-store): add support to context, url persistence, store persistence, drift handle (#11081)" (#11152) (#11157)
This reverts commit 8b13f004ed.
2026-05-04 11:04:26 +00:00
aks07
603077c575 feat: open in new tab 2026-04-21 10:01:34 +05:30
aks07
7e5c4476f7 feat: add service name 2026-04-21 09:22:17 +05:30
aks07
da648ed3f3 feat: style fix 2026-04-21 01:43:12 +05:30
aks07
9fa56aacd1 feat: uncollapse spans on select from flamegraph 2026-04-21 01:37:22 +05:30
aks07
5acd79419c Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items 2026-04-21 01:30:25 +05:30
aks07
9b7b0f8862 feat: load all spans in waterfall under limit 2026-04-21 01:23:33 +05:30
aks07
c29e8a0136 feat: add status codes 2026-04-20 21:25:49 +05:30
aks07
ebac945ac2 feat: minor fix 2026-04-20 14:04:05 +05:30
aks07
e787497695 feat: fix colors 2026-04-20 13:46:25 +05:30
aks07
eba6bd5f5b feat: disable crosshair in waterfall 2026-04-20 11:26:35 +05:30
aks07
1aeab2718d feat: fix test 2026-04-20 10:44:21 +05:30
aks07
d879af4fb3 Merge branch 'ns/waterfall-v3-2' of github.com:SigNoz/signoz into feat/dropdown-items 2026-04-20 10:19:56 +05:30
Nikhil Soni
ac10be2eb2 Merge remote-tracking branch 'origin/main' into ns/waterfall-v3-2 2026-04-20 10:04:09 +05:30
aks07
113d1544ba feat: add crosshair 2026-04-20 09:46:14 +05:30
aks07
df02da664c feat: use semantic tokens 2026-04-17 21:03:41 +05:30
aks07
d0a491ed8e feat: use semantic tokens 2026-04-17 20:59:46 +05:30
aks07
77c39a9f05 feat: delete unused file 2026-04-17 20:26:50 +05:30
aks07
309a76e5fd feat: rename copy to copy value 2026-04-17 19:13:28 +05:30
aks07
43e80caf09 Merge branch 'feat/filter-search' of github.com:SigNoz/signoz into feat/dropdown-items 2026-04-17 10:31:48 +05:30
aks07
a2d853daf5 Merge branch 'main' of github.com:SigNoz/signoz into feat/filter-search 2026-04-17 10:23:53 +05:30
aks07
3970619afa Merge remote-tracking branch 'origin/ns/waterfall-v3-2' into feat/filter-search 2026-04-17 10:11:31 +05:30
aks07
9dc87761c1 feat: minor fix 2026-04-16 20:22:17 +05:30
aks07
86a44fad42 Merge branch 'feat/filter-search' of github.com:SigNoz/signoz into feat/dropdown-items 2026-04-16 20:20:41 +05:30
aks07
91f74144cb feat: old trace btn added 2026-04-16 20:19:27 +05:30
aks07
0863c5170b feat: no data screen 2026-04-16 18:14:18 +05:30
aks07
837cd2a463 feat: fix color duplications 2026-04-16 16:46:37 +05:30
aks07
c88a2d5d90 feat: dropdown added to span details 2026-04-16 15:42:09 +05:30
Nikhil Soni
c9abc2cb30 chore: use sqlbuilder in clickhouse store where possible 2026-04-16 09:39:36 +05:30
Nikhil Soni
01824b0b62 chore: remove unused duration nano field 2026-04-16 09:39:36 +05:30
Nikhil Soni
d1b378992d refactor: use same method for cache key creation 2026-04-16 09:39:36 +05:30
Nikhil Soni
52ca921d2a refactor: return 404 on missing trace and other cleanup 2026-04-16 09:39:36 +05:30
Nikhil Soni
42f12dfef3 refactor: breakdown GetSelectedSpans for readability 2026-04-16 09:39:36 +05:30
Nikhil Soni
f2a694447e refactor: separate out store calls and computations 2026-04-16 09:39:36 +05:30
Nikhil Soni
2e7dfa739f refactor: move error to types as well 2026-04-16 09:39:36 +05:30
Nikhil Soni
0aa73580a3 refactor: extract ClickHouse queries into a store abstraction
Move GetTraceSummary and GetTraceSpans out of module.go into a
traceStore interface backed by clickhouseTraceStore in store.go.
The module struct now holds a traceStore instead of a raw
telemetrystore.TelemetryStore, keeping DB access separate from
business logic.
2026-04-16 09:39:36 +05:30
Nikhil Soni
2ff1a43bf8 refactor: move type creation and constants to types package
- Move DB/table/cache/windowing constants to tracedetailtypes package
- Add NewWaterfallTrace and NewWaterfallResponse constructors in types
- Use constructors in module.go instead of inline struct literals
- Reorder waterfall.go so public functions precede private ones
2026-04-16 09:39:36 +05:30
Nikhil Soni
c1477c78be chore: avoid returning nil, nil 2026-04-16 09:39:36 +05:30
Nikhil Soni
9807dd5295 refactor: break down GetWaterfall method for readability 2026-04-16 09:39:36 +05:30
Nikhil Soni
2c59eeff26 fix: update openapi specs 2026-04-16 09:39:36 +05:30
Nikhil Soni
8ccfb4efef fix: use int16 for status code as per db schema 2026-04-16 09:39:36 +05:30
Nikhil Soni
87d18160e8 fix: remove timeout since waterfall take longer 2026-04-16 09:39:36 +05:30
Nikhil Soni
bfa7ee96da chore: generate openapi spec for v3 waterfall 2026-04-16 09:39:33 +05:30
Nikhil Soni
5e3eb66d3a fix: use typed paramter field in logs 2026-04-16 09:38:12 +05:30
Nikhil Soni
3d8cdf18bd fix: add timeout to module context 2026-04-16 09:38:12 +05:30
Nikhil Soni
cb4e501047 fix: rename timestamp to milli for readability 2026-04-16 09:38:12 +05:30
Nikhil Soni
cb8b2137ba fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-16 09:38:12 +05:30
Nikhil Soni
998315a255 chore: avoid sorting on every traversal 2026-04-16 09:38:12 +05:30
Nikhil Soni
250657e46b chore: add same test cases as for old waterfall api 2026-04-16 09:38:12 +05:30
Nikhil Soni
795ae9ab18 refactor: convert waterfall api to modules format 2026-04-16 09:38:05 +05:30
Nikhil Soni
6a9ea8d9f8 fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-16 09:31:48 +05:30
Nikhil Soni
2723e18023 fix: update span.attributes to map of string to any
To support otel format of diffrent types of attributes
2026-04-16 09:31:48 +05:30
Nikhil Soni
6e89d5f6eb chore: add reason for using snake case in response 2026-04-16 09:31:48 +05:30
Nikhil Soni
4c2a815236 refactor: move type conversion logic to types pkg 2026-04-16 09:31:48 +05:30
Nikhil Soni
b1d66b2e5f feat: setup types and interface for waterfall v3
v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service
2026-04-16 09:31:48 +05:30
aks07
ae88edbb5e feat: disable scroll to view for collapse and uncollapse 2026-04-15 22:36:58 +05:30
aks07
7c9484d47b feat: api integration v3 2026-04-15 21:50:02 +05:30
aks07
24128bd394 feat: show caution on sampled flamegraph 2026-04-15 13:35:55 +05:30
aks07
2118916a23 feat: fix flamegraph and waterfall bg color 2026-04-15 00:51:39 +05:30
aks07
52220412a1 fix: align timeline and span length in flamegraph and waterfall 2026-04-15 00:32:35 +05:30
aks07
85abee8476 feat: show child count in waterfall 2026-04-14 21:27:15 +05:30
aks07
650a29d184 feat: set limit to 100k for flamegraph 2026-04-14 21:17:26 +05:30
aks07
d9c7101d22 feat: reduce time 2026-04-14 18:45:14 +05:30
aks07
b1e7c25189 Merge branch 'main' of github.com:SigNoz/signoz into feat/filter-search 2026-04-14 15:33:19 +05:30
aks07
e9904a0558 Merge branch 'feat/filter-search' of github.com:SigNoz/signoz into feat/filter-search 2026-04-14 15:33:04 +05:30
aks07
5cd199f535 feat: automatically scroll left on vertical scroll 2026-04-14 15:32:23 +05:30
aks07
f6f48ca0bc feat: api integration 2026-04-14 14:46:49 +05:30
Aditya Singh
847f91e22e Merge branch 'main' into feat/filter-search 2026-04-14 13:14:46 +05:30
aks07
29d0abe5a8 Merge branch 'ns/waterfall-v3-2' of github.com:SigNoz/signoz into feat/filter-search 2026-04-14 13:11:26 +05:30
Nikhil Soni
c08840a827 fix: update openapi specs 2026-04-14 10:50:36 +05:30
Nikhil Soni
a3e7bb90b0 fix: use int16 for status code as per db schema 2026-04-14 10:50:36 +05:30
Nikhil Soni
8515d2f37c fix: remove timeout since waterfall take longer 2026-04-14 10:50:36 +05:30
Nikhil Soni
07c05ac3a6 chore: generate openapi spec for v3 waterfall 2026-04-14 10:50:36 +05:30
Nikhil Soni
6289f59ba3 fix: use typed paramter field in logs 2026-04-14 10:50:36 +05:30
Nikhil Soni
76371c9fa2 fix: add timeout to module context 2026-04-14 10:50:36 +05:30
Nikhil Soni
f082e396eb fix: rename timestamp to milli for readability 2026-04-14 10:50:36 +05:30
Nikhil Soni
840eb8f228 fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-14 10:50:36 +05:30
Nikhil Soni
2911baf6bb chore: avoid sorting on every traversal 2026-04-14 10:50:36 +05:30
Nikhil Soni
fc5be4eeb5 chore: add same test cases as for old waterfall api 2026-04-14 10:50:36 +05:30
Nikhil Soni
a1b92c79a4 refactor: convert waterfall api to modules format 2026-04-14 10:50:31 +05:30
Nikhil Soni
7a0acd5c8b fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-14 10:49:58 +05:30
Nikhil Soni
069cbe2c6f fix: update span.attributes to map of string to any
To support otel format of diffrent types of attributes
2026-04-14 10:49:58 +05:30
Nikhil Soni
4c821f9721 chore: add reason for using snake case in response 2026-04-14 10:49:58 +05:30
Nikhil Soni
4eccea92db refactor: move type conversion logic to types pkg 2026-04-14 10:49:58 +05:30
Nikhil Soni
c8d8966a5d feat: setup types and interface for waterfall v3
v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service
2026-04-14 10:49:58 +05:30
aks07
1e52a5603e Merge branch 'ns/waterfall-v3-2' of github.com:SigNoz/signoz into feat/filter-search 2026-04-14 10:33:46 +05:30
aks07
780ba1a359 feat: new filter ui 2026-04-14 10:27:21 +05:30
aks07
3b71abe820 feat: remove trace header 2026-04-13 18:49:38 +05:30
aks07
70b9d0ff02 feat: filter ui fix 2026-04-13 15:49:43 +05:30
aks07
f4657861e1 feat: make filter and search work with flamegraph 2026-04-13 15:49:09 +05:30
Nikhil Soni
66fe5b5240 fix: update openapi specs 2026-04-13 14:04:45 +05:30
Nikhil Soni
c333cecf43 fix: use int16 for status code as per db schema 2026-04-13 13:23:39 +05:30
Nikhil Soni
276e09853e fix: remove timeout since waterfall take longer 2026-04-13 12:31:08 +05:30
aks07
4defd41504 feat: prevent api call on closing span detail 2026-04-13 12:07:28 +05:30
aks07
ab53b29a14 feat: add span details loader 2026-04-13 10:25:49 +05:30
aks07
b58e82efbf feat: disable anaytics span tab for now 2026-04-12 21:33:07 +05:30
aks07
0a1a676877 feat: show total span count 2026-04-12 21:14:19 +05:30
aks07
bb2aa9f77c feat: auto scroll horizontally to span 2026-04-12 20:08:55 +05:30
aks07
04bef4ac06 feat: sync error and loading state for flamegraph for n/w and computation logic 2026-04-11 23:48:47 +05:30
aks07
3bcb2c2c41 feat: added loading to flamegraph and timeout to webworker 2026-04-11 22:37:10 +05:30
aks07
9e77b76122 feat: add icons 2026-04-11 21:28:00 +05:30
aks07
ff4a41d842 feat: analytics 2026-04-11 14:34:57 +05:30
aks07
387deb779d feat: span details ux 2026-04-10 21:31:40 +05:30
aks07
1ec2663d51 feat: lint fix 2026-04-10 13:40:43 +05:30
aks07
1b17370da0 feat: fix test 2026-04-10 13:10:28 +05:30
aks07
c6484a79e2 feat: fix test 2026-04-10 13:09:08 +05:30
Nikhil Soni
16a2c7a1af chore: generate openapi spec for v3 waterfall 2026-04-10 10:54:36 +05:30
aks07
3c4ac0e85e feat: supress click 2026-04-10 09:08:45 +05:30
aks07
87ba729a00 feat: minor change 2026-04-10 02:46:55 +05:30
aks07
f1ed7145e4 feat: add limit 2026-04-10 02:41:22 +05:30
aks07
bc15495e17 Merge branch 'main' of github.com:SigNoz/signoz into feat/span-details 2026-04-10 02:01:59 +05:30
aks07
f7d3012daf feat: api integration 2026-04-10 00:53:25 +05:30
Nikhil Soni
6ec9a2ec41 fix: use typed paramter field in logs 2026-04-09 21:39:29 +05:30
Nikhil Soni
9c056f809a fix: add timeout to module context 2026-04-09 20:03:47 +05:30
Nikhil Soni
c1d4273416 fix: rename timestamp to milli for readability 2026-04-09 16:54:44 +05:30
Nikhil Soni
618fe891d5 fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-09 14:02:44 +05:30
Nikhil Soni
549c7e7034 chore: avoid sorting on every traversal 2026-04-09 11:38:55 +05:30
Nikhil Soni
dd65f83c3d chore: add same test cases as for old waterfall api 2026-04-09 11:38:55 +05:30
Nikhil Soni
8463a131fc refactor: convert waterfall api to modules format 2026-04-09 11:38:55 +05:30
Nikhil Soni
2d42518440 fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-09 11:38:55 +05:30
Nikhil Soni
43d75a3853 fix: update span.attributes to map of string to any
To support otel format of diffrent types of attributes
2026-04-09 11:38:55 +05:30
Nikhil Soni
c5bb34e385 chore: add reason for using snake case in response 2026-04-09 11:38:54 +05:30
Nikhil Soni
6fd129991d refactor: move type conversion logic to types pkg 2026-04-09 11:38:54 +05:30
Nikhil Soni
9c5cca426a feat: setup types and interface for waterfall v3
v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service
2026-04-09 11:38:54 +05:30
aks07
a467efb97d feat: style fixes 2026-04-09 08:31:12 +05:30
aks07
58e2718090 feat: linked spans 2026-04-08 22:04:42 +05:30
aks07
65fee725c9 feat: key value label style fixes 2026-04-08 14:21:54 +05:30
aks07
ea87174088 feat: fix span details headr 2026-04-08 11:39:47 +05:30
aks07
627c483d86 feat: copy link change and url clear on span close 2026-04-07 20:06:53 +05:30
aks07
2533137db4 feat: use resizable for waterfall table as well 2026-04-07 15:55:06 +05:30
aks07
a774f8a4fe feat: add collapsible sections in trace details 2026-04-07 13:28:59 +05:30
aks07
8487f6cf66 feat: add bound to drags while floating 2026-04-01 19:33:53 +05:30
aks07
6ebe51126e feat: fix pinning. fix drag on top 2026-04-01 16:48:59 +05:30
aks07
ed64d5cd9f feat: replace draggable package 2026-03-31 21:33:42 +05:30
aks07
c04076e664 feat: span details folder rename 2026-03-31 17:50:02 +05:30
aks07
3c129e2c7d feat: span details floating drawer added 2026-03-31 17:44:25 +05:30
aks07
0ba51e2058 feat: json viewer with select dropdown added 2026-03-31 15:17:15 +05:30
aks07
cdc2ab134c feat: style fix 2026-03-26 20:27:41 +05:30
aks07
fb0c05b553 feat: refactor 2026-03-26 20:25:25 +05:30
aks07
68e9707e3b feat: search in pretty view 2026-03-26 20:08:25 +05:30
aks07
17ffaf9ccf feat: minor change 2026-03-26 19:42:48 +05:30
aks07
efec669b76 feat: update yarn lock 2026-03-26 19:31:10 +05:30
aks07
17b9e14d34 feat: added pretty view 2026-03-26 19:25:06 +05:30
aks07
2db9f969c3 feat: key attr section added 2026-03-26 15:01:16 +05:30
aks07
9fa466b124 feat: added span percentile 2026-03-26 14:33:06 +05:30
aks07
0c7768ebff feat: details field component 2026-03-25 03:10:50 +05:30
aks07
58dd51e92f feat: span details header 2026-03-25 02:22:11 +05:30
aks07
870c9bf6dc Merge branch 'main' of github.com:SigNoz/signoz into feat/span-details 2026-03-25 01:08:22 +05:30
aks07
7604956bf0 feat: span details init 2026-03-25 00:34:56 +05:30
aks07
66510e4919 feat: waterfall resizable 2026-03-18 20:58:01 +05:30
aks07
a1bf0e67db feat: event dots in trace details 2026-03-18 18:54:19 +05:30
aks07
a06046612a feat: connector line ux 2026-03-17 18:45:11 +05:30
aks07
31c9d4309b feat: move to service worker 2026-03-17 17:06:05 +05:30
aks07
7bef8b86c4 feat: subtree segregated tree 2026-03-17 14:39:57 +05:30
aks07
d26acd36a3 feat: subtree segregated tree 2026-03-17 13:55:58 +05:30
aks07
1cee595135 feat: subtree segregated tree 2026-03-17 13:29:07 +05:30
aks07
dd1868fcbc feat: row based flamegraph 2026-03-16 23:27:31 +05:30
aks07
a20beb8ba2 feat: span hover popover sync 2026-03-16 14:18:12 +05:30
aks07
998d652feb feat: fix hover option overflow 2026-03-16 10:19:42 +05:30
aks07
3695d3c180 feat: match span style 2026-03-13 15:53:46 +05:30
aks07
da175bafbc feat: add TimelineV3 ruler to waterfall header with padding fix
Add the TimelineV3 component to the sticky header of the waterfall's
right panel so timeline tick marks are visible. Add horizontal padding
to both the timeline header and span duration bars to prevent label
overflow/clipping at the edges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:12:49 +05:30
aks07
021b187cbc feat: decouple waterfall left (span tree) and right (timeline bars) panels
Split the waterfall into two independent panels with a shared virtualizer
so deeply nested span names are visible via horizontal scroll in the left
panel. Left panel uses useReactTable + <table> for future column
extensibility; right panel uses plain divs for timeline bars. A draggable
resize handle separates the two panels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:04:32 +05:30
aks07
f42b468597 feat: waterfall init 2026-03-11 17:15:27 +05:30
aks07
7e2cf57819 Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-flamegraph 2026-03-11 12:24:06 +05:30
aks07
dc9ebc5b26 feat: add test utils 2026-03-11 03:31:10 +05:30
aks07
398ab6e9d9 feat: add test cases for flamegraph 2026-03-11 03:28:35 +05:30
aks07
fec60671d8 feat: minor comment added 2026-03-11 03:03:49 +05:30
aks07
99259cc4e8 feat: remove unnecessary props 2026-03-10 16:01:24 +05:30
aks07
ca311717c2 feat: bg color for selected and hover spans 2026-03-06 23:16:35 +05:30
aks07
a614da2c65 fix: update color 2026-03-06 20:22:23 +05:30
aks07
ce18709002 fix: style fix 2026-03-06 20:03:42 +05:30
aks07
2b6977e891 feat: reduce timeline intervals 2026-03-06 20:01:50 +05:30
aks07
3e6eedbcab feat: fix style 2026-03-06 19:57:19 +05:30
aks07
fd9e3f0411 feat: fix style 2026-03-06 19:26:23 +05:30
aks07
e99465e030 Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-flamegraph 2026-03-06 18:45:18 +05:30
aks07
9ad2db4b99 feat: scroll to selected span 2026-03-06 16:00:48 +05:30
aks07
07fd5f70ef feat: fix timerange unit selection when zoomed 2026-03-06 12:55:34 +05:30
aks07
ba79121795 feat: temp change 2026-03-06 12:49:06 +05:30
aks07
6e4e419b5e Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-flamegraph 2026-03-06 10:30:41 +05:30
aks07
2f06afaf27 feat: handle click and hover with tooltip 2026-03-05 23:25:10 +05:30
aks07
f77c3cb23c feat: update span colors 2026-03-05 22:40:06 +05:30
aks07
9e3a8efcfc feat: zoom and drag added 2026-03-05 18:22:55 +05:30
aks07
8e325ba8b3 feat: added timeline v3 2026-03-05 12:31:17 +05:30
aks07
884f516766 feat: add text to spans 2026-03-03 12:20:06 +05:30
aks07
4bcbb4ffc3 feat: flamegraph canvas init 2026-03-02 19:21:23 +05:30
870 changed files with 37205 additions and 17223 deletions

1
.github/CODEOWNERS vendored
View File

@@ -107,6 +107,7 @@ go.mod @therealpandey
/pkg/modules/organization/ @therealpandey
/pkg/modules/authdomain/ @therealpandey
/pkg/modules/role/ @therealpandey
/pkg/types/coretypes/ @therealpandey @vikrantgupta25
# IdentN Owners

View File

@@ -102,3 +102,20 @@ jobs:
run: |
go run cmd/enterprise/*.go generate openapi
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)
authz:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: generate-authz
run: |
go run cmd/enterprise/*.go generate authz
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in authz permissions. Run go run cmd/enterprise/*.go generate authz locally and commit."; exit 1)

View File

@@ -63,46 +63,6 @@ jobs:
uses: actions/checkout@v4
- name: run
run: bash frontend/scripts/validate-md-languages.sh
authz:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v5
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: deps-install
working-directory: ./frontend
run: |
yarn install
- name: uv-install
uses: astral-sh/setup-uv@v5
- name: uv-deps
working-directory: ./tests/integration
run: |
uv sync
- name: setup-test
run: |
make py-test-setup
- name: generate
working-directory: ./frontend
run: |
yarn generate:permissions-type
- name: teardown-test
if: always()
run: |
make py-test-teardown
- name: validate
run: |
if ! git diff --exit-code frontend/src/hooks/useAuthZ/permissions.type.ts; then
echo "::error::frontend/src/hooks/useAuthZ/permissions.type.ts is out of date. Please run the generator locally and commit the changes: npm run generate:permissions-type (from the frontend directory)"
exit 1
fi
openapi:
if: |
github.event_name == 'merge_group' ||

117
cmd/authz.go Normal file
View File

@@ -0,0 +1,117 @@
package cmd
import (
"bytes"
"context"
"os"
"sort"
"text/template"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/spf13/cobra"
)
const permissionsTypePath = "frontend/src/hooks/useAuthZ/permissions.type.ts"
var permissionsTypeTemplate = template.Must(template.New("permissions").Parse(
`// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY cmd/enterprise/*.go generate authz
export default {
status: 'success',
data: {
resources: [
{{- range .Resources }}
{
kind: '{{ .Kind }}',
type: '{{ .Type }}',
},
{{- end }}
],
relations: {
{{- range .Relations }}
{{ .Verb }}: [{{ range $i, $t := .Types }}{{ if $i }}, {{ end }}'{{ $t }}'{{ end }}],
{{- end }}
},
},
} as const;
`))
type permissionsTypeRelation struct {
Verb string
Types []string
}
type permissionsTypeResource struct {
Kind string
Type string
}
type permissionsTypeData struct {
Resources []permissionsTypeResource
Relations []permissionsTypeRelation
}
func registerGenerateAuthz(parentCmd *cobra.Command) {
authzCmd := &cobra.Command{
Use: "authz",
Short: "Generate authz permissions for the frontend",
RunE: func(currCmd *cobra.Command, args []string) error {
return runGenerateAuthz(currCmd.Context())
},
}
parentCmd.AddCommand(authzCmd)
}
func runGenerateAuthz(_ context.Context) error {
registry := coretypes.NewRegistry()
allowedResources := map[string]bool{
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesRole).String(): true,
}
allowedTypes := map[string]bool{}
refs := registry.ResourceRefs()
resources := make([]permissionsTypeResource, 0, len(refs))
for _, ref := range refs {
if !allowedResources[ref.String()] {
continue
}
allowedTypes[ref.Type.StringValue()] = true
resources = append(resources, permissionsTypeResource{
Kind: ref.Kind.String(),
Type: ref.Type.StringValue(),
})
}
typesByVerb := registry.TypesByVerb()
verbs := make([]coretypes.Verb, 0, len(typesByVerb))
for verb := range typesByVerb {
verbs = append(verbs, verb)
}
sort.Slice(verbs, func(i, j int) bool { return verbs[i].StringValue() < verbs[j].StringValue() })
relations := make([]permissionsTypeRelation, 0, len(verbs))
for _, verb := range verbs {
types := make([]string, 0, len(typesByVerb[verb]))
for _, t := range typesByVerb[verb] {
if !allowedTypes[t.StringValue()] {
continue
}
types = append(types, t.StringValue())
}
relations = append(relations, permissionsTypeRelation{
Verb: verb.StringValue(),
Types: types,
})
}
var buf bytes.Buffer
if err := permissionsTypeTemplate.Execute(&buf, permissionsTypeData{Resources: resources, Relations: relations}); err != nil {
return err
}
return os.WriteFile(permissionsTypePath, buf.Bytes(), 0o600)
}

View File

@@ -92,13 +92,13 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete, _ dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
func(ctx context.Context, sqlstore sqlstore.SQLStore, config authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)
if err != nil {
return nil, err
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore), nil
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)

View File

@@ -137,12 +137,12 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return authNs, nil
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, dashboardModule dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
func(ctx context.Context, sqlstore sqlstore.SQLStore, config authz.Config, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)
if err != nil {
return nil, err
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, dashboardModule), nil
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)

View File

@@ -16,6 +16,7 @@ func RegisterGenerate(parentCmd *cobra.Command, logger *slog.Logger) {
}
registerGenerateOpenAPI(generateCmd)
registerGenerateAuthz(generateCmd)
parentCmd.AddCommand(generateCmd)
}

View File

@@ -13,6 +13,8 @@ global:
ingestion_url: <unset>
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.
# mcp_url: <unset>
# the url of the SigNoz AI Assistant server. when unset, the AI Assistant is hidden in the frontend.
# ai_assistant_url: <unset>
##################### Version #####################
version:
@@ -426,4 +428,4 @@ authz:
provider: openfga
openfga:
# maximum tuples allowed per openfga write operation.
max_tuples_per_write: 100
max_tuples_per_write: 300

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.121.1
image: signoz/signoz:v0.122.0
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.121.1
image: signoz/signoz:v0.122.0
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.121.1}
image: signoz/signoz:${VERSION:-v0.122.0}
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.121.1}
image: signoz/signoz:${VERSION:-v0.122.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -96,6 +96,122 @@ components:
- createdAt
- updatedAt
type: object
AlertmanagertypesPostableChannel:
oneOf:
- required:
- discord_configs
- required:
- email_configs
- required:
- incidentio_configs
- required:
- pagerduty_configs
- required:
- slack_configs
- required:
- webhook_configs
- required:
- opsgenie_configs
- required:
- wechat_configs
- required:
- pushover_configs
- required:
- victorops_configs
- required:
- sns_configs
- required:
- telegram_configs
- required:
- webex_configs
- required:
- msteams_configs
- required:
- msteamsv2_configs
- required:
- jira_configs
- required:
- rocketchat_configs
- required:
- mattermost_configs
properties:
discord_configs:
items:
$ref: '#/components/schemas/ConfigDiscordConfig'
type: array
email_configs:
items:
$ref: '#/components/schemas/ConfigEmailConfig'
type: array
incidentio_configs:
items:
$ref: '#/components/schemas/ConfigIncidentioConfig'
type: array
jira_configs:
items:
$ref: '#/components/schemas/ConfigJiraConfig'
type: array
mattermost_configs:
items:
$ref: '#/components/schemas/ConfigMattermostConfig'
type: array
msteams_configs:
items:
$ref: '#/components/schemas/ConfigMSTeamsConfig'
type: array
msteamsv2_configs:
items:
$ref: '#/components/schemas/ConfigMSTeamsV2Config'
type: array
name:
type: string
opsgenie_configs:
items:
$ref: '#/components/schemas/ConfigOpsGenieConfig'
type: array
pagerduty_configs:
items:
$ref: '#/components/schemas/ConfigPagerdutyConfig'
type: array
pushover_configs:
items:
$ref: '#/components/schemas/ConfigPushoverConfig'
type: array
rocketchat_configs:
items:
$ref: '#/components/schemas/ConfigRocketchatConfig'
type: array
slack_configs:
items:
$ref: '#/components/schemas/ConfigSlackConfig'
type: array
sns_configs:
items:
$ref: '#/components/schemas/ConfigSNSConfig'
type: array
telegram_configs:
items:
$ref: '#/components/schemas/ConfigTelegramConfig'
type: array
victorops_configs:
items:
$ref: '#/components/schemas/ConfigVictorOpsConfig'
type: array
webex_configs:
items:
$ref: '#/components/schemas/ConfigWebexConfig'
type: array
webhook_configs:
items:
$ref: '#/components/schemas/ConfigWebhookConfig'
type: array
wechat_configs:
items:
$ref: '#/components/schemas/ConfigWechatConfig'
type: array
required:
- name
type: object
AlertmanagertypesPostableRoutePolicy:
properties:
channels:
@@ -133,6 +249,10 @@ components:
type: string
type: object
AuthtypesAuthDomainConfig:
oneOf:
- $ref: '#/components/schemas/AuthtypesSamlConfig'
- $ref: '#/components/schemas/AuthtypesGoogleConfig'
- $ref: '#/components/schemas/AuthtypesOIDCConfig'
properties:
googleAuthConfig:
$ref: '#/components/schemas/AuthtypesGoogleConfig'
@@ -145,8 +265,15 @@ components:
ssoEnabled:
type: boolean
ssoType:
type: string
$ref: '#/components/schemas/AuthtypesAuthNProvider'
type: object
AuthtypesAuthNProvider:
enum:
- google_auth
- saml
- email_password
- oidc
type: string
AuthtypesAuthNProviderInfo:
properties:
relayStatePath:
@@ -169,7 +296,7 @@ components:
AuthtypesCallbackAuthNSupport:
properties:
provider:
type: string
$ref: '#/components/schemas/AuthtypesAuthNProvider'
url:
type: string
type: object
@@ -177,62 +304,23 @@ components:
properties:
authNProviderInfo:
$ref: '#/components/schemas/AuthtypesAuthNProviderInfo'
config:
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
createdAt:
format: date-time
type: string
googleAuthConfig:
$ref: '#/components/schemas/AuthtypesGoogleConfig'
id:
type: string
name:
type: string
oidcConfig:
$ref: '#/components/schemas/AuthtypesOIDCConfig'
orgId:
type: string
roleMapping:
$ref: '#/components/schemas/AuthtypesRoleMapping'
samlConfig:
$ref: '#/components/schemas/AuthtypesSamlConfig'
ssoEnabled:
type: boolean
ssoType:
type: string
updatedAt:
format: date-time
type: string
required:
- id
type: object
AuthtypesGettableObjects:
properties:
resource:
$ref: '#/components/schemas/AuthtypesResource'
selectors:
items:
type: string
type: array
required:
- resource
- selectors
type: object
AuthtypesGettableResources:
properties:
relations:
additionalProperties:
items:
type: string
type: array
nullable: true
type: object
resources:
items:
$ref: '#/components/schemas/AuthtypesResource'
type: array
required:
- resources
- relations
type: object
AuthtypesGettableToken:
properties:
accessToken:
@@ -249,9 +337,9 @@ components:
authorized:
type: boolean
object:
$ref: '#/components/schemas/AuthtypesObject'
$ref: '#/components/schemas/CoretypesObject'
relation:
type: string
$ref: '#/components/schemas/AuthtypesRelation'
required:
- relation
- object
@@ -299,16 +387,6 @@ components:
issuerAlias:
type: string
type: object
AuthtypesObject:
properties:
resource:
$ref: '#/components/schemas/AuthtypesResource'
selector:
type: string
required:
- resource
- selector
type: object
AuthtypesOrgSessionContext:
properties:
authNSupport:
@@ -323,23 +401,7 @@ components:
AuthtypesPasswordAuthNSupport:
properties:
provider:
type: string
type: object
AuthtypesPatchableObjects:
properties:
additions:
items:
$ref: '#/components/schemas/AuthtypesGettableObjects'
nullable: true
type: array
deletions:
items:
$ref: '#/components/schemas/AuthtypesGettableObjects'
nullable: true
type: array
required:
- additions
- deletions
$ref: '#/components/schemas/AuthtypesAuthNProvider'
type: object
AuthtypesPatchableRole:
properties:
@@ -378,16 +440,15 @@ components:
refreshToken:
type: string
type: object
AuthtypesResource:
properties:
name:
type: string
type:
type: string
required:
- name
- type
type: object
AuthtypesRelation:
enum:
- create
- read
- update
- delete
- list
- assignee
type: string
AuthtypesRole:
properties:
createdAt:
@@ -451,14 +512,14 @@ components:
AuthtypesTransaction:
properties:
object:
$ref: '#/components/schemas/AuthtypesObject'
$ref: '#/components/schemas/CoretypesObject'
relation:
type: string
$ref: '#/components/schemas/AuthtypesRelation'
required:
- relation
- object
type: object
AuthtypesUpdateableAuthDomain:
AuthtypesUpdatableAuthDomain:
properties:
config:
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
@@ -2088,6 +2149,64 @@ components:
to_user:
type: string
type: object
CoretypesObject:
properties:
resource:
$ref: '#/components/schemas/CoretypesResourceRef'
selector:
type: string
required:
- resource
- selector
type: object
CoretypesObjectGroup:
properties:
resource:
$ref: '#/components/schemas/CoretypesResourceRef'
selectors:
items:
type: string
type: array
required:
- resource
- selectors
type: object
CoretypesPatchableObjects:
properties:
additions:
items:
$ref: '#/components/schemas/CoretypesObjectGroup'
nullable: true
type: array
deletions:
items:
$ref: '#/components/schemas/CoretypesObjectGroup'
nullable: true
type: array
required:
- additions
- deletions
type: object
CoretypesResourceRef:
properties:
kind:
type: string
type:
$ref: '#/components/schemas/CoretypesType'
required:
- type
- kind
type: object
CoretypesType:
enum:
- user
- serviceaccount
- anonymous
- role
- organization
- metaresource
- metaresources
type: string
DashboardtypesDashboard:
properties:
createdAt:
@@ -2363,6 +2482,9 @@ components:
type: object
GlobaltypesConfig:
properties:
ai_assistant_url:
nullable: true
type: string
external_url:
type: string
identN:
@@ -2376,6 +2498,7 @@ components:
- external_url
- ingestion_url
- mcp_url
- ai_assistant_url
type: object
GlobaltypesIdentNConfig:
properties:
@@ -5200,6 +5323,9 @@ components:
sub_tree_node_count:
minimum: 0
type: integer
time_unix:
minimum: 0
type: integer
trace_id:
type: string
trace_state:
@@ -5580,35 +5706,6 @@ paths:
summary: Check permissions
tags:
- authz
/api/v1/authz/resources:
get:
deprecated: false
description: Gets all the available resources
operationId: AuthzResources
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesGettableResources'
status:
type: string
required:
- status
- data
type: object
description: OK
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
summary: Get resources
tags:
- authz
/api/v1/channels:
get:
deprecated: false
@@ -5665,7 +5762,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ConfigReceiver'
$ref: '#/components/schemas/AlertmanagertypesPostableChannel'
responses:
"201":
content:
@@ -6944,20 +7041,20 @@ paths:
schema:
$ref: '#/components/schemas/AuthtypesPostableAuthDomain'
responses:
"200":
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: OK
description: Created
"400":
content:
application/json:
@@ -7042,6 +7139,63 @@ paths:
summary: Delete auth domain
tags:
- authdomains
get:
deprecated: false
description: This endpoint returns an auth domain by ID
operationId: GetAuthDomain
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get auth domain by ID
tags:
- authdomains
put:
deprecated: false
description: This endpoint updates an auth domain
@@ -7056,7 +7210,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesUpdateableAuthDomain'
$ref: '#/components/schemas/AuthtypesUpdatableAuthDomain'
responses:
"204":
description: No Content
@@ -8775,7 +8929,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/AuthtypesGettableObjects'
$ref: '#/components/schemas/CoretypesObjectGroup'
type: array
status:
type: string
@@ -8848,7 +9002,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesPatchableObjects'
$ref: '#/components/schemas/CoretypesPatchableObjects'
responses:
"204":
content:

View File

@@ -2,7 +2,6 @@ package openfgaauthz
import (
"context"
"slices"
"github.com/SigNoz/signoz/ee/authz/openfgaserver"
"github.com/SigNoz/signoz/pkg/authz"
@@ -13,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
@@ -25,19 +25,19 @@ type provider struct {
openfgaServer *openfgaserver.Server
licensing licensing.Licensing
store authtypes.RoleStore
registry []authz.RegisterTypeable
registry *authtypes.Registry
settings factory.ScopedProviderSettings
onBeforeRoleDelete []authz.OnBeforeRoleDelete
}
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry *authtypes.Registry) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) {
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore, licensing, onBeforeRoleDelete, registry)
})
}
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore)
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry *authtypes.Registry) (authz.AuthZ, error) {
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore, registry)
pkgAuthzService, err := pkgOpenfgaAuthzProvider.New(ctx, settings, config)
if err != nil {
return nil, err
@@ -74,11 +74,11 @@ func (provider *provider) Stop(ctx context.Context) error {
return provider.openfgaServer.Stop(ctx)
}
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, roleSelectors []coretypes.Selector) error {
return provider.openfgaServer.CheckWithTupleCreation(ctx, claims, orgID, relation, typeable, selectors, roleSelectors)
}
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, roleSelectors []coretypes.Selector) error {
return provider.openfgaServer.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, typeable, selectors, roleSelectors)
}
@@ -108,7 +108,7 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
return results, nil
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
}
@@ -159,16 +159,10 @@ func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.U
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
tuples := make([]*openfgav1.TupleKey, 0)
grantTuples, err := provider.getManagedRoleGrantTuples(orgID, userID)
if err != nil {
return err
}
grantTuples := provider.getManagedRoleGrantTuples(orgID, userID)
tuples = append(tuples, grantTuples...)
managedRoleTuples, err := provider.getManagedRoleTransactionTuples(orgID)
if err != nil {
return err
}
managedRoleTuples := provider.getManagedRoleTransactionTuples(orgID)
tuples = append(tuples, managedRoleTuples...)
return provider.Write(ctx, tuples, nil)
@@ -208,21 +202,7 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
return role, nil
}
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
resources := make([]*authtypes.Resource, 0)
for _, register := range provider.registry {
for _, typeable := range register.MustGetTypeables() {
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
}
}
for _, typeable := range provider.MustGetTypeables() {
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
}
return resources
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*coretypes.Object, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -233,16 +213,16 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return nil, err
}
objects := make([]*authtypes.Object, 0)
for _, objectType := range provider.getUniqueTypes() {
if !slices.Contains(authtypes.TypeableRelations[objectType], relation) {
objects := make([]*coretypes.Object, 0)
for _, objectType := range provider.registry.Types() {
if coretypes.ErrIfVerbNotValidForType(relation.Verb, objectType) != nil {
continue
}
resourceObjects, err := provider.
ListObjects(
ctx,
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.Name, orgID, &authtypes.RelationAssignee),
authtypes.MustNewSubject(coretypes.NewResourceRole(), storableRole.Name, orgID, &coretypes.VerbAssignee),
relation,
objectType,
)
@@ -265,7 +245,7 @@ func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *au
return provider.store.Update(ctx, orgID, role)
}
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*coretypes.Object) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -318,84 +298,63 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return provider.store.Delete(ctx, orgID, id)
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, authtypes.TypeableResourcesRoles}
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) []*openfgav1.TupleKey {
tuples := []*openfgav1.TupleKey{}
// Grant the admin role to the user
adminSubject := authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil)
adminTuple, err := authtypes.TypeableRole.Tuples(
adminSubject := authtypes.MustNewSubject(coretypes.NewResourceUser(), userID.String(), orgID, nil)
adminTuple := authtypes.NewTuples(
coretypes.NewResourceRole(),
adminSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
},
authtypes.Relation{Verb: coretypes.VerbAssignee},
[]coretypes.Selector{coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName)},
orgID,
)
if err != nil {
return nil, err
}
tuples = append(tuples, adminTuple...)
// Grant the admin role to the anonymous user
anonymousSubject := authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
anonymousTuple, err := authtypes.TypeableRole.Tuples(
anonymousSubject := authtypes.MustNewSubject(coretypes.NewResourceAnonymous(), coretypes.AnonymousUser.String(), orgID, nil)
anonymousTuple := authtypes.NewTuples(
coretypes.NewResourceRole(),
anonymousSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAnonymousRoleName),
},
authtypes.Relation{Verb: coretypes.VerbAssignee},
[]coretypes.Selector{coretypes.TypeRole.MustSelector(authtypes.SigNozAnonymousRoleName)},
orgID,
)
if err != nil {
return nil, err
}
tuples = append(tuples, anonymousTuple...)
return tuples, nil
return tuples
}
func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
transactionsByRole := make(map[string][]*authtypes.Transaction)
for _, register := range provider.registry {
for roleName, txns := range register.MustGetManagedRoleTransactions() {
transactionsByRole[roleName] = append(transactionsByRole[roleName], txns...)
}
}
func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*openfgav1.TupleKey {
tuples := make([]*openfgav1.TupleKey, 0)
for roleName, transactions := range transactionsByRole {
for roleName, transactions := range provider.registry.ManagedRoleTransactions() {
for _, txn := range transactions {
typeable := authtypes.MustNewTypeableFromType(txn.Object.Resource.Type, txn.Object.Resource.Name)
txnTuples, err := typeable.Tuples(
resource := coretypes.MustNewResourceFromTypeAndKind(txn.Object.Resource.Type, txn.Object.Resource.Kind)
txnTuples := authtypes.NewTuples(
resource,
authtypes.MustNewSubject(
authtypes.TypeableRole,
coretypes.NewResourceRole(),
roleName,
orgID,
&authtypes.RelationAssignee,
&coretypes.VerbAssignee,
),
txn.Relation,
[]authtypes.Selector{txn.Object.Selector},
[]coretypes.Selector{txn.Object.Selector},
orgID,
)
if err != nil {
return nil, err
}
tuples = append(tuples, txnTuples...)
}
}
return tuples, nil
return tuples
}
func (provider *provider) deleteTuples(ctx context.Context, roleName string, orgID valuer.UUID) error {
subject := authtypes.MustNewSubject(authtypes.TypeableRole, roleName, orgID, &authtypes.RelationAssignee)
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.getUniqueTypes() {
for _, objectType := range provider.registry.Types() {
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
@@ -424,28 +383,3 @@ func (provider *provider) deleteTuples(ctx context.Context, roleName string, org
return nil
}
func (provider *provider) getUniqueTypes() []authtypes.Type {
seen := make(map[string]struct{})
uniqueTypes := make([]authtypes.Type, 0)
for _, register := range provider.registry {
for _, typeable := range register.MustGetTypeables() {
typeKey := typeable.Type().StringValue()
if _, ok := seen[typeKey]; ok {
continue
}
seen[typeKey] = struct{}{}
uniqueTypes = append(uniqueTypes, typeable.Type())
}
}
for _, typeable := range provider.MustGetTypeables() {
typeKey := typeable.Type().StringValue()
if _, ok := seen[typeKey]; ok {
continue
}
seen[typeKey] = struct{}{}
uniqueTypes = append(uniqueTypes, typeable.Type())
}
return uniqueTypes
}

View File

@@ -1,6 +1,6 @@
module base
type organisation
type organization
relations
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
@@ -10,12 +10,14 @@ type user
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
type serviceaccount
type serviceaccount
relations
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
type anonymous
@@ -26,6 +28,7 @@ type role
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
type metaresources
relations

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
@@ -33,18 +34,18 @@ func (server *Server) Stop(ctx context.Context) error {
return server.pkgAuthzService.Stop(ctx)
}
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, _ []coretypes.Selector) error {
subject := ""
switch claims.Principal {
case authtypes.PrincipalUser:
user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
user, err := authtypes.NewSubject(coretypes.NewResourceUser(), claims.UserID, orgID, nil)
if err != nil {
return err
}
subject = user
case authtypes.PrincipalServiceAccount:
serviceAccount, err := authtypes.NewSubject(authtypes.TypeableServiceAccount, claims.ServiceAccountID, orgID, nil)
serviceAccount, err := authtypes.NewSubject(coretypes.NewResourceServiceAccount(), claims.ServiceAccountID, orgID, nil)
if err != nil {
return err
}
@@ -52,10 +53,7 @@ func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtyp
subject = serviceAccount
}
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
if err != nil {
return err
}
tupleSlice := authtypes.NewTuples(typeable, subject, relation, selectors, orgID)
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
for idx, tuple := range tupleSlice {
@@ -76,16 +74,13 @@ func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtyp
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
}
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, _ []coretypes.Selector) error {
subject, err := authtypes.NewSubject(coretypes.NewResourceAnonymous(), coretypes.AnonymousUser.String(), orgID, nil)
if err != nil {
return err
}
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
if err != nil {
return err
}
tupleSlice := authtypes.NewTuples(typeable, subject, relation, selectors, orgID)
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
for idx, tuple := range tupleSlice {
@@ -110,7 +105,7 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
}
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
return server.pkgAuthzService.ListObjects(ctx, subject, relation, objectType)
}

View File

@@ -2,6 +2,7 @@ package openfgaserver
import (
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/openfga/openfga/pkg/storage"
@@ -10,11 +11,11 @@ import (
"github.com/openfga/openfga/pkg/storage/sqlite"
)
func NewSQLStore(store sqlstore.SQLStore) (storage.OpenFGADatastore, error) {
func NewSQLStore(store sqlstore.SQLStore, config authz.Config) (storage.OpenFGADatastore, error) {
switch store.BunDB().Dialect().Name().String() {
case "sqlite":
return sqlite.NewWithDB(store.SQLDB(), &sqlcommon.Config{
MaxTuplesPerWriteField: 100,
MaxTuplesPerWriteField: config.OpenFGA.MaxTuplesPerWrite,
MaxTypesPerModelField: 100,
})
case "pg":
@@ -24,7 +25,7 @@ func NewSQLStore(store sqlstore.SQLStore) (storage.OpenFGADatastore, error) {
}
return postgres.NewWithDB(pgStore.Pool(), nil, &sqlcommon.Config{
MaxTuplesPerWriteField: 100,
MaxTuplesPerWriteField: config.OpenFGA.MaxTuplesPerWrite,
MaxTypesPerModelField: 100,
})
}

View File

@@ -14,7 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
@@ -88,7 +88,7 @@ func (module *module) GetDashboardByPublicID(ctx context.Context, id valuer.UUID
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
}
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
orgIDs := make([]string, len(orgs))
for idx, org := range orgs {
orgIDs[idx] = org.ID.StringValue()
@@ -99,9 +99,9 @@ func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id
return nil, valuer.UUID{}, err
}
return []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeMetaResource, id.StringValue()),
authtypes.MustNewSelector(authtypes.TypeMetaResource, authtypes.WildCardSelectorString),
return []coretypes.Selector{
coretypes.TypeMetaResource.MustSelector(id.StringValue()),
coretypes.TypeMetaResource.MustSelector(coretypes.WildCardSelectorString),
}, storableDashboard.OrgID, nil
}
@@ -217,28 +217,6 @@ func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valu
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) MustGetTypeables() []authtypes.Typeable {
return module.pkgDashboardModule.MustGetTypeables()
}
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
authtypes.SigNozAnonymousRoleName: {
{
ID: valuer.GenerateUUID(),
Relation: authtypes.RelationRead,
Object: *authtypes.MustNewObject(
authtypes.Resource{
Type: authtypes.TypeMetaResource,
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
},
authtypes.MustNewSelector(authtypes.TypeMetaResource, "*"),
),
},
},
}
}
func (module *module) deletePublic(ctx context.Context, _ valuer.UUID, dashboardID valuer.UUID) error {
return module.store.DeletePublic(ctx, dashboardID.StringValue())
}

View File

@@ -6,6 +6,8 @@ import (
"io"
"net/http"
"log/slog"
"github.com/SigNoz/signoz/ee/query-service/anomaly"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
@@ -15,7 +17,6 @@ import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"log/slog"
)
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
@@ -137,4 +138,3 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
aH.QueryRangeV4(w, r)
}
}

View File

@@ -42,8 +42,8 @@ import (
// Server runs HTTP, Mux and a grpc server
type Server struct {
config signoz.Config
signoz *signoz.SigNoz
config signoz.Config
signoz *signoz.SigNoz
// public http router
httpConn net.Listener
@@ -148,7 +148,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
s := &Server{
config: config,
signoz: signoz,
signoz: signoz,
httpHostPort: baseconst.HTTPHostPort,
unavailableChannel: make(chan healthcheck.Status),
usageManager: usageManager,
@@ -317,4 +317,3 @@ func (s *Server) Stop(ctx context.Context) error {
return nil
}

View File

@@ -23,6 +23,11 @@
"**/*.md",
"**/*.json",
"src/parser/**",
"src/TraceOperator/parser/**"
"src/TraceOperator/parser/**",
".claude",
".opencode",
"dist",
"playwright-report",
".temp_cache"
]
}

View File

@@ -1,53 +1,51 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// Mock for uplot library used in tests
import { type Mock, vi } from 'vitest';
export interface MockUPlotInstance {
setData: Mock;
setSize: Mock;
destroy: Mock;
redraw: Mock;
setSeries: Mock;
setData: jest.Mock;
setSize: jest.Mock;
destroy: jest.Mock;
redraw: jest.Mock;
setSeries: jest.Mock;
}
export interface MockUPlotPaths {
spline: Mock;
bars: Mock;
linear: Mock;
stepped: Mock;
spline: jest.Mock;
bars: jest.Mock;
linear: jest.Mock;
stepped: jest.Mock;
}
// Create mock instance methods
const createMockUPlotInstance = (): MockUPlotInstance => ({
setData: vi.fn(),
setSize: vi.fn(),
destroy: vi.fn(),
redraw: vi.fn(),
setSeries: vi.fn(),
setData: jest.fn(),
setSize: jest.fn(),
destroy: jest.fn(),
redraw: jest.fn(),
setSeries: jest.fn(),
});
// Path builder: (self, seriesIdx, idx0, idx1) => paths or null
const createMockPathBuilder = (name: string): Mock =>
vi.fn(() => ({
const createMockPathBuilder = (name: string): jest.Mock =>
jest.fn(() => ({
name, // To test if the correct pathBuilder is used
stroke: vi.fn(),
fill: vi.fn(),
clip: vi.fn(),
stroke: jest.fn(),
fill: jest.fn(),
clip: jest.fn(),
}));
// Create mock paths - linear, spline, stepped needed by UPlotSeriesBuilder.getPathBuilder
const mockPaths = {
spline: vi.fn(() => createMockPathBuilder('spline')),
bars: vi.fn(() => createMockPathBuilder('bars')),
linear: vi.fn(() => createMockPathBuilder('linear')),
stepped: vi.fn((opts?: { align?: number }) =>
spline: jest.fn(() => createMockPathBuilder('spline')),
bars: jest.fn(() => createMockPathBuilder('bars')),
linear: jest.fn(() => createMockPathBuilder('linear')),
stepped: jest.fn((opts?: { align?: number }) =>
createMockPathBuilder(`stepped-(${opts?.align ?? 0})`),
),
};
// Mock static methods
const mockTzDate = vi.fn(
const mockTzDate = jest.fn(
(date: Date, _timezone: string) => new Date(date.getTime()),
);

View File

@@ -1,6 +1,4 @@
// Mock for useSafeNavigate hook to avoid React Router version conflicts in tests
import { type MockedFunction, vi } from 'vitest';
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
@@ -16,15 +14,15 @@ interface SafeNavigateTo {
type SafeNavigateToType = string | SafeNavigateTo;
interface UseSafeNavigateReturn {
safeNavigate: MockedFunction<
safeNavigate: jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>;
}
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
safeNavigate: vi.fn(
safeNavigate: jest.fn(
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
) as MockedFunction<
) as jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>,
});

View File

@@ -15,15 +15,15 @@
"lint:generated": "oxlint ./src/api/generated --fix",
"lint:fix": "oxlint ./src --fix",
"lint:styles": "stylelint \"src/**/*.scss\"",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.cjs && patch-package",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.cjs",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:changedsince": "vitest run --changed HEAD~1 --coverage",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.cjs"
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
},
"engines": {
"node": ">=22.0.0"
@@ -120,10 +120,12 @@
"react-helmet-async": "1.3.0",
"react-hook-form": "7.71.2",
"react-i18next": "^11.16.1",
"react-json-tree": "^0.20.0",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-rnd": "^10.5.3",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",
@@ -170,7 +172,7 @@
"@commitlint/config-conventional": "^20.4.2",
"@faker-js/faker": "9.3.0",
"@jest/globals": "30.2.0",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
@@ -198,11 +200,9 @@
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",
"@typescript/native-preview": "7.0.0-dev.20260421.2",
"@vitest/coverage-v8": "4.1.5",
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0",
"eslint-plugin-sonarjs": "4.0.2",
"happy-dom": "20.9.0",
"husky": "^7.0.4",
"imagemin": "^8.0.1",
"imagemin-svgo": "^10.0.1",
@@ -217,7 +217,6 @@
"oxfmt": "0.47.0",
"oxlint": "1.62.0",
"oxlint-tsgolint": "0.22.1",
"patch-package": "8.0.1",
"portfinder-sync": "^0.0.2",
"postcss": "8.5.6",
"postcss-scss": "4.0.9",
@@ -234,15 +233,15 @@
"ts-jest": "29.4.6",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.2.0",
"use-sync-external-store": "1.6.0",
"vite-plugin-checker": "0.12.0",
"vite-plugin-compression": "0.5.1",
"vite-plugin-image-optimizer": "2.0.3",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.5"
"vite-tsconfig-paths": "6.1.1"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"oxlint --fix --quiet",
"oxlint --fix",
"oxfmt --write",
"sh -c tsgo --noEmit"
],

View File

@@ -1,46 +0,0 @@
diff --git a/node_modules/@mswjs/interceptors/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js b/node_modules/@mswjs/interceptors/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js
index 2480e76..67208c4 100644
--- a/node_modules/@mswjs/interceptors/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js
+++ b/node_modules/@mswjs/interceptors/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js
@@ -345,7 +345,14 @@ var createXMLHttpRequestOverride = function (options) {
});
};
XMLHttpRequestOverride.prototype.abort = function () {
- this.log('abort');
+ if (typeof this.log === 'function') {
+ this.log('abort');
+ }
+ if (typeof this.setReadyState !== 'function' ||
+ typeof this.trigger !== 'function' ||
+ typeof this.readyState !== 'number') {
+ return;
+ }
if (this.readyState > this.UNSENT && this.readyState < this.DONE) {
this.setReadyState(this.UNSENT);
this.trigger('abort');
@@ -459,14 +466,17 @@ var createXMLHttpRequestOverride = function (options) {
}
finally { if (e_2) throw e_2.error; }
}
- request.onabort = this.abort;
- request.onerror = this.onerror;
- request.ontimeout = this.ontimeout;
- request.onload = this.onload;
- request.onloadstart = this.onloadstart;
- request.onloadend = this.onloadend;
- request.onprogress = this.onprogress;
- request.onreadystatechange = this.onreadystatechange;
+ request.abort = this.abort.bind(this);
+ request.onabort = this.abort.bind(this);
+ request.onerror = this.onerror ? this.onerror.bind(this) : null;
+ request.ontimeout = this.ontimeout ? this.ontimeout.bind(this) : null;
+ request.onload = this.onload ? this.onload.bind(this) : null;
+ request.onloadstart = this.onloadstart ? this.onloadstart.bind(this) : null;
+ request.onloadend = this.onloadend ? this.onloadend.bind(this) : null;
+ request.onprogress = this.onprogress ? this.onprogress.bind(this) : null;
+ request.onreadystatechange = this.onreadystatechange
+ ? this.onreadystatechange.bind(this)
+ : null;
};
/**
* Propagates the mock XMLHttpRequest instance listeners

View File

@@ -1,199 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const axios = require('axios');
const PERMISSIONS_TYPE_FILE = path.join(
__dirname,
'../src/hooks/useAuthZ/permissions.type.ts',
);
const SIGNOZ_INTEGRATION_IMAGE = 'signoz:integration';
const LOCAL_BACKEND_URL = 'http://localhost:8080';
function log(message) {
console.log(`[generate-permissions-type] ${message}`);
}
function getBackendUrlFromDocker() {
try {
const output = execSync(
`docker ps --filter "ancestor=${SIGNOZ_INTEGRATION_IMAGE}" --format "{{.Ports}}"`,
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
).trim();
if (!output) {
return null;
}
const portMatch = output.match(/0\.0\.0\.0:(\d+)->8080\/tcp/);
if (portMatch) {
return `http://localhost:${portMatch[1]}`;
}
const ipv6Match = output.match(/:::(\d+)->8080\/tcp/);
if (ipv6Match) {
return `http://localhost:${ipv6Match[1]}`;
}
} catch (err) {
log(`Warning: Could not get port from docker: ${err.message}`);
}
return null;
}
async function checkBackendHealth(url, maxAttempts = 3, delayMs = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await axios.get(`${url}/api/v1/health`, {
timeout: 5000,
validateStatus: (status) => status === 200,
});
return true;
} catch (err) {
if (attempt < maxAttempts) {
await new Promise((r) => setTimeout(r, delayMs));
}
}
}
return false;
}
async function discoverBackendUrl() {
const dockerUrl = getBackendUrlFromDocker();
if (dockerUrl) {
log(`Found ${SIGNOZ_INTEGRATION_IMAGE} container, trying ${dockerUrl}...`);
if (await checkBackendHealth(dockerUrl)) {
log(`Backend found at ${dockerUrl} (from py-test-setup)`);
return dockerUrl;
}
log(`Backend at ${dockerUrl} is not responding`);
}
log(`Trying local backend at ${LOCAL_BACKEND_URL}...`);
if (await checkBackendHealth(LOCAL_BACKEND_URL)) {
log(`Backend found at ${LOCAL_BACKEND_URL}`);
return LOCAL_BACKEND_URL;
}
return null;
}
async function fetchResources(backendUrl) {
log('Fetching resources from API...');
const resourcesUrl = `${backendUrl}/api/v1/authz/resources`;
const { data: response } = await axios.get(resourcesUrl);
return response;
}
function transformResponse(apiResponse) {
if (!apiResponse.data) {
throw new Error('Invalid API response: missing data field');
}
const { resources, relations } = apiResponse.data;
return {
status: apiResponse.status || 'success',
data: {
resources: resources,
relations: relations,
},
};
}
function generateTypeScriptFile(data) {
const resourcesStr = data.data.resources
.map(
(r) =>
`\t\t\t{\n\t\t\t\tname: '${r.name}',\n\t\t\t\ttype: '${r.type}',\n\t\t\t},`,
)
.join('\n');
const relationsStr = Object.entries(data.data.relations)
.map(
([type, relations]) =>
`\t\t\t${type}: [${relations.map((r) => `'${r}'`).join(', ')}],`,
)
.join('\n');
return `// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY scripts/generate-permissions-type
export default {
\tstatus: '${data.status}',
\tdata: {
\t\tresources: [
${resourcesStr}
\t\t],
\t\trelations: {
${relationsStr}
\t\t},
\t},
} as const;
`;
}
async function main() {
try {
log('Starting permissions type generation...');
const backendUrl = await discoverBackendUrl();
if (!backendUrl) {
console.error('\n' + '='.repeat(80));
console.error('ERROR: No running SigNoz backend found!');
console.error('='.repeat(80));
console.error(
'\nThe permissions type generator requires a running SigNoz backend.',
);
console.error('\nFor local development, start the backend with:');
console.error(' make go-run-enterprise');
console.error(
'\nFor CI or integration testing, start the test environment with:',
);
console.error(' make py-test-setup');
console.error(
'\nIf running in CI and seeing this error, check that the py-test-setup',
);
console.error('step completed successfully before this step runs.');
console.error('='.repeat(80) + '\n');
process.exit(1);
}
log('Fetching resources...');
const apiResponse = await fetchResources(backendUrl);
log('Transforming response...');
const transformed = transformResponse(apiResponse);
log('Generating TypeScript file...');
const content = generateTypeScriptFile(transformed);
log(`Writing to ${PERMISSIONS_TYPE_FILE}...`);
fs.writeFileSync(PERMISSIONS_TYPE_FILE, content, 'utf8');
const rootDir = path.join(__dirname, '../..');
const relativePath = path.relative(
path.join(rootDir, 'frontend'),
PERMISSIONS_TYPE_FILE,
);
log('Linting generated file...');
execSync(`cd frontend && yarn oxlint ${relativePath}`, {
cwd: rootDir,
stdio: 'inherit',
});
log('Successfully generated permissions.type.ts');
} catch (error) {
log(`Error: ${error.message}`);
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = { main };

View File

@@ -1,4 +1,3 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ReactElement } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter, Route, Switch, useLocation } from 'react-router-dom';
@@ -23,13 +22,13 @@ import { ROLES, USER_ROLES } from 'types/roles';
import PrivateRoute from '../Private';
// Mock localStorage APIs
const mockLocalStorage = vi.hoisted((): Record<string, string> => ({}));
vi.mock('api/browser/localstorage/get', () => ({
const mockLocalStorage: Record<string, string> = {};
jest.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: (key: string): string | null => mockLocalStorage[key] || null,
}));
vi.mock('api/browser/localstorage/set', () => ({
jest.mock('api/browser/localstorage/set', () => ({
__esModule: true,
default: (key: string, value: string): void => {
mockLocalStorage[key] = value;
@@ -37,29 +36,27 @@ vi.mock('api/browser/localstorage/set', () => ({
}));
// Mock useGetTenantLicense hook
const mockTenantLicense = vi.hoisted(() => ({ isCloudUser: true }));
vi.mock('hooks/useGetTenantLicense', () => ({
let mockIsCloudUser = true;
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: (): {
isCloudUser: boolean;
isEnterpriseSelfHostedUser: boolean;
isCommunityUser: boolean;
isCommunityEnterpriseUser: boolean;
} => ({
isCloudUser: mockTenantLicense.isCloudUser,
isEnterpriseSelfHostedUser: !mockTenantLicense.isCloudUser,
isCloudUser: mockIsCloudUser,
isEnterpriseSelfHostedUser: !mockIsCloudUser,
isCommunityUser: false,
isCommunityEnterpriseUser: false,
}),
}));
// Mock react-query for users fetch
const mockUsers = vi.hoisted((): { data: { email: string }[] } => ({
data: [],
}));
vi.mock('api/generated/services/users', async () => ({
...(await vi.importActual('api/generated/services/users')),
useListUsers: vi.fn(() => ({
data: { data: mockUsers.data },
let mockUsersData: { email: string }[] = [];
jest.mock('api/generated/services/users', () => ({
...jest.requireActual('api/generated/services/users'),
useListUsers: jest.fn(() => ({
data: { data: mockUsersData },
isFetching: false,
})),
}));
@@ -179,13 +176,13 @@ function createMockAppContext(
orgPreferencesFetchError: null,
changelog: null,
showChangelogModal: false,
activeLicenseRefetch: vi.fn(),
updateUser: vi.fn(),
updateOrgPreferences: vi.fn(),
updateUserPreferenceInContext: vi.fn(),
updateOrg: vi.fn(),
updateChangelog: vi.fn(),
toggleChangelogModal: vi.fn(),
activeLicenseRefetch: jest.fn(),
updateUser: jest.fn(),
updateOrgPreferences: jest.fn(),
updateUserPreferenceInContext: jest.fn(),
updateOrg: jest.fn(),
updateChangelog: jest.fn(),
toggleChangelogModal: jest.fn(),
versionData: { version: '1.0.0', ee: 'Y', setupCompleted: true },
hasEditPermission: true,
...overrides,
@@ -205,7 +202,7 @@ function renderPrivateRoute(options: RenderPrivateRouteOptions = {}): void {
isCloudUser = true,
} = options;
mockTenantLicense.isCloudUser = isCloudUser;
mockIsCloudUser = isCloudUser;
const contextValue = createMockAppContext(appContext);
@@ -248,11 +245,11 @@ function assertRendersChildren(): void {
describe('PrivateRoute', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
queryClient.clear();
Object.keys(mockLocalStorage).forEach((key) => delete mockLocalStorage[key]);
mockTenantLicense.isCloudUser = true;
mockUsers.data = [];
mockIsCloudUser = true;
mockUsersData = [];
});
describe('Old Routes Handling', () => {
@@ -1017,7 +1014,7 @@ describe('PrivateRoute', () => {
describe('Onboarding Flow (Cloud Users)', () => {
it('should redirect to onboarding when first user has not completed onboarding', async () => {
// Set up exactly one user (not admin@signoz.cloud) to trigger first user check
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1056,7 +1053,7 @@ describe('PrivateRoute', () => {
it('should not redirect to onboarding when onboarding is already complete', async () => {
// Set up first user condition - this ensures the ONLY reason we don't redirect
// is because isOnboardingComplete is true
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1127,7 +1124,7 @@ describe('PrivateRoute', () => {
it('should not redirect to onboarding when workspace is blocked and accessing billing', async () => {
// This tests the scenario where admin tries to access billing to fix payment
// while workspace is blocked and onboarding is not complete
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.BILLING,
@@ -1152,7 +1149,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is blocked and accessing settings', async () => {
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.SETTINGS,
@@ -1176,7 +1173,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is suspended (DEFAULTED)', async () => {
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1203,7 +1200,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is access restricted (TERMINATED)', async () => {
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1230,7 +1227,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is access restricted (EXPIRED)', async () => {
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,

View File

@@ -65,6 +65,13 @@ export const TraceDetail = Loadable(
),
);
export const TraceDetailV3 = Loadable(
() =>
import(
/* webpackChunkName: "TraceDetailV3 Page" */ 'pages/TraceDetailV3Page/index'
),
);
export const UsageExplorerPage = Loadable(
() => import(/* webpackChunkName: "UsageExplorerPage" */ 'modules/Usage'),
);

View File

@@ -48,6 +48,7 @@ import {
StatusPage,
SupportPage,
TraceDetail,
TraceDetailV3,
TraceFilter,
TracesExplorer,
TracesFunnelDetails,
@@ -138,6 +139,9 @@ const routes: AppRoutes[] = [
exact: true,
key: 'LOGS_SAVE_VIEWS',
},
// V3 trace details is gated until release: /trace serves V2 (public),
// /trace-old serves V3 (URL-only access). Flip the two `component`
// values back to release V3.
{
path: ROUTES.TRACE_DETAIL,
exact: true,
@@ -145,6 +149,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'TRACE_DETAIL',
},
{
path: ROUTES.TRACE_DETAIL_OLD,
exact: true,
component: TraceDetailV3,
isPrivate: true,
key: 'TRACE_DETAIL_OLD',
},
{
path: ROUTES.SETTINGS,
exact: false,

View File

@@ -1,23 +1,22 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
/**
* localstorage/get — lazy migration tests.
*
* basePath is memoized at module init, so each test re-imports the module with
* a fresh DOM state via vi.resetModules and dynamic import.
* basePath is memoized at module init, so each describe block re-imports the
* module with a fresh DOM state via jest.isolateModules.
*/
type GetModule = typeof import('../get');
async function loadGetModule(href: string): Promise<GetModule> {
function loadGetModule(href: string): GetModule {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.append(base);
vi.resetModules();
const mod = await import('../get');
base.remove();
let mod!: GetModule;
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
mod = require('../get');
});
return mod;
}
@@ -29,19 +28,19 @@ afterEach(() => {
});
describe('get — root path "/"', () => {
it('reads the bare key', async () => {
const { default: get } = await loadGetModule('/');
it('reads the bare key', () => {
const { default: get } = loadGetModule('/');
localStorage.setItem('AUTH_TOKEN', 'tok');
expect(get('AUTH_TOKEN')).toBe('tok');
});
it('returns null when key is absent', async () => {
const { default: get } = await loadGetModule('/');
it('returns null when key is absent', () => {
const { default: get } = loadGetModule('/');
expect(get('MISSING')).toBeNull();
});
it('does NOT promote bare keys (no-op at root)', async () => {
const { default: get } = await loadGetModule('/');
it('does NOT promote bare keys (no-op at root)', () => {
const { default: get } = loadGetModule('/');
localStorage.setItem('THEME', 'light');
get('THEME');
// bare key must still be present — no migration at root
@@ -50,19 +49,19 @@ describe('get — root path "/"', () => {
});
describe('get — prefixed path "/signoz/"', () => {
it('reads an already-scoped key directly', async () => {
const { default: get } = await loadGetModule('/signoz/');
it('reads an already-scoped key directly', () => {
const { default: get } = loadGetModule('/signoz/');
localStorage.setItem('/signoz/AUTH_TOKEN', 'scoped-tok');
expect(get('AUTH_TOKEN')).toBe('scoped-tok');
});
it('returns null when neither scoped nor bare key exists', async () => {
const { default: get } = await loadGetModule('/signoz/');
it('returns null when neither scoped nor bare key exists', () => {
const { default: get } = loadGetModule('/signoz/');
expect(get('MISSING')).toBeNull();
});
it('lazy-migrates bare key to scoped key on first read', async () => {
const { default: get } = await loadGetModule('/signoz/');
it('lazy-migrates bare key to scoped key on first read', () => {
const { default: get } = loadGetModule('/signoz/');
localStorage.setItem('AUTH_TOKEN', 'old-tok');
const result = get('AUTH_TOKEN');
@@ -72,8 +71,8 @@ describe('get — prefixed path "/signoz/"', () => {
expect(localStorage.getItem('AUTH_TOKEN')).toBeNull();
});
it('scoped key takes precedence over bare key', async () => {
const { default: get } = await loadGetModule('/signoz/');
it('scoped key takes precedence over bare key', () => {
const { default: get } = loadGetModule('/signoz/');
localStorage.setItem('AUTH_TOKEN', 'bare-tok');
localStorage.setItem('/signoz/AUTH_TOKEN', 'scoped-tok');
@@ -82,8 +81,8 @@ describe('get — prefixed path "/signoz/"', () => {
expect(localStorage.getItem('AUTH_TOKEN')).toBe('bare-tok');
});
it('subsequent reads after migration use scoped key (no double-write)', async () => {
const { default: get } = await loadGetModule('/signoz/');
it('subsequent reads after migration use scoped key (no double-write)', () => {
const { default: get } = loadGetModule('/signoz/');
localStorage.setItem('THEME', 'dark');
get('THEME'); // triggers migration
@@ -95,15 +94,31 @@ describe('get — prefixed path "/signoz/"', () => {
});
describe('get — two-prefix isolation', () => {
it('/signoz/ and /testing/ do not share migrated values', async () => {
it('/signoz/ and /testing/ do not share migrated values', () => {
localStorage.setItem('THEME', 'light');
const { default: getSignoz } = await loadGetModule('/signoz/');
const base1 = document.createElement('base');
base1.setAttribute('href', '/signoz/');
document.head.append(base1);
let getSignoz!: GetModule['default'];
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
getSignoz = require('../get').default;
});
base1.remove();
// migrate bare → /signoz/THEME
getSignoz('THEME');
const { default: getTesting } = await loadGetModule('/testing/');
const base2 = document.createElement('base');
base2.setAttribute('href', '/testing/');
document.head.append(base2);
let getTesting!: GetModule['default'];
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
getTesting = require('../get').default;
});
base2.remove();
// /testing/ prefix: bare key already gone, scoped key does not exist
expect(getTesting('THEME')).toBeNull();

View File

@@ -1,5 +1,3 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
/**
* sessionstorage/get — lazy migration tests.
* Mirrors the localStorage get tests; same logic, different storage.
@@ -7,13 +5,17 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
type GetModule = typeof import('../get');
async function loadGetModule(href: string): Promise<GetModule> {
function loadGetModule(href: string): GetModule {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.append(base);
vi.resetModules();
return import('../get');
let mod!: GetModule;
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
mod = require('../get');
});
return mod;
}
afterEach(() => {
@@ -24,19 +26,19 @@ afterEach(() => {
});
describe('get — root path "/"', () => {
it('reads the bare key', async () => {
const { default: get } = await loadGetModule('/');
it('reads the bare key', () => {
const { default: get } = loadGetModule('/');
sessionStorage.setItem('retry-lazy-refreshed', 'true');
expect(get('retry-lazy-refreshed')).toBe('true');
});
it('returns null when key is absent', async () => {
const { default: get } = await loadGetModule('/');
it('returns null when key is absent', () => {
const { default: get } = loadGetModule('/');
expect(get('MISSING')).toBeNull();
});
it('does NOT promote bare keys at root', async () => {
const { default: get } = await loadGetModule('/');
it('does NOT promote bare keys at root', () => {
const { default: get } = loadGetModule('/');
sessionStorage.setItem('retry-lazy-refreshed', 'true');
get('retry-lazy-refreshed');
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBe('true');
@@ -44,19 +46,19 @@ describe('get — root path "/"', () => {
});
describe('get — prefixed path "/signoz/"', () => {
it('reads an already-scoped key directly', async () => {
const { default: get } = await loadGetModule('/signoz/');
it('reads an already-scoped key directly', () => {
const { default: get } = loadGetModule('/signoz/');
sessionStorage.setItem('/signoz/retry-lazy-refreshed', 'true');
expect(get('retry-lazy-refreshed')).toBe('true');
});
it('returns null when neither scoped nor bare key exists', async () => {
const { default: get } = await loadGetModule('/signoz/');
it('returns null when neither scoped nor bare key exists', () => {
const { default: get } = loadGetModule('/signoz/');
expect(get('MISSING')).toBeNull();
});
it('lazy-migrates bare key to scoped key on first read', async () => {
const { default: get } = await loadGetModule('/signoz/');
it('lazy-migrates bare key to scoped key on first read', () => {
const { default: get } = loadGetModule('/signoz/');
sessionStorage.setItem('retry-lazy-refreshed', 'true');
const result = get('retry-lazy-refreshed');
@@ -66,8 +68,8 @@ describe('get — prefixed path "/signoz/"', () => {
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBeNull();
});
it('scoped key takes precedence over bare key', async () => {
const { default: get } = await loadGetModule('/signoz/');
it('scoped key takes precedence over bare key', () => {
const { default: get } = loadGetModule('/signoz/');
sessionStorage.setItem('retry-lazy-refreshed', 'bare');
sessionStorage.setItem('/signoz/retry-lazy-refreshed', 'scoped');

View File

@@ -1,18 +1,15 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import axios from 'api';
import { getFieldKeys } from '../getFieldKeys';
// Mock the API instance
vi.mock('api', () => ({
default: {
get: vi.fn(),
},
jest.mock('api', () => ({
get: jest.fn(),
}));
describe('getFieldKeys API', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
const mockSuccessResponse = {
@@ -31,7 +28,7 @@ describe('getFieldKeys API', () => {
it('should call API with correct parameters when no args provided', async () => {
// Mock successful API response
vi.mocked(axios.get).mockResolvedValueOnce(mockSuccessResponse);
(axios.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call function with no parameters
await getFieldKeys();
@@ -44,7 +41,7 @@ describe('getFieldKeys API', () => {
it('should call API with signal parameter when provided', async () => {
// Mock successful API response
vi.mocked(axios.get).mockResolvedValueOnce(mockSuccessResponse);
(axios.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call function with signal parameter
await getFieldKeys('traces');
@@ -57,7 +54,7 @@ describe('getFieldKeys API', () => {
it('should call API with name parameter when provided', async () => {
// Mock successful API response
vi.mocked(axios.get).mockResolvedValueOnce({
(axios.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -79,7 +76,7 @@ describe('getFieldKeys API', () => {
it('should call API with both signal and name when provided', async () => {
// Mock successful API response
vi.mocked(axios.get).mockResolvedValueOnce({
(axios.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -101,7 +98,7 @@ describe('getFieldKeys API', () => {
it('should return properly formatted response', async () => {
// Mock API to return our response
vi.mocked(axios.get).mockResolvedValueOnce(mockSuccessResponse);
(axios.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call the function
const result = await getFieldKeys('traces');

View File

@@ -1,23 +1,20 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import axios from 'api';
import { getFieldValues } from '../getFieldValues';
// Mock the API instance
vi.mock('api', () => ({
default: {
get: vi.fn(),
},
jest.mock('api', () => ({
get: jest.fn(),
}));
describe('getFieldValues API', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('should call the API with correct parameters (no options)', async () => {
// Mock API response
vi.mocked(axios.get).mockResolvedValueOnce({
(axios.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -41,7 +38,7 @@ describe('getFieldValues API', () => {
it('should call the API with signal parameter', async () => {
// Mock API response
vi.mocked(axios.get).mockResolvedValueOnce({
(axios.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -65,7 +62,7 @@ describe('getFieldValues API', () => {
it('should call the API with name parameter', async () => {
// Mock API response
vi.mocked(axios.get).mockResolvedValueOnce({
(axios.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -89,7 +86,7 @@ describe('getFieldValues API', () => {
it('should call the API with value parameter', async () => {
// Mock API response
vi.mocked(axios.get).mockResolvedValueOnce({
(axios.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -113,7 +110,7 @@ describe('getFieldValues API', () => {
it('should call the API with time range parameters', async () => {
// Mock API response
vi.mocked(axios.get).mockResolvedValueOnce({
(axios.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -165,7 +162,7 @@ describe('getFieldValues API', () => {
},
};
vi.mocked(axios.get).mockResolvedValueOnce(mockResponse);
(axios.get as jest.Mock).mockResolvedValueOnce(mockResponse);
// Call the function
const result = await getFieldValues('traces', 'mixed.values');
@@ -196,7 +193,7 @@ describe('getFieldValues API', () => {
};
// Mock API to return our response
vi.mocked(axios.get).mockResolvedValueOnce(mockApiResponse);
(axios.get as jest.Mock).mockResolvedValueOnce(mockApiResponse);
// Call the function
const result = await getFieldValues('traces', 'service.name');

View File

@@ -19,9 +19,11 @@ import type {
import type {
AuthtypesPostableAuthDomainDTO,
AuthtypesUpdateableAuthDomainDTO,
CreateAuthDomain200,
AuthtypesUpdatableAuthDomainDTO,
CreateAuthDomain201,
DeleteAuthDomainPathParameters,
GetAuthDomain200,
GetAuthDomainPathParameters,
ListAuthDomains200,
RenderErrorResponseDTO,
UpdateAuthDomainPathParameters,
@@ -124,7 +126,7 @@ export const createAuthDomain = (
authtypesPostableAuthDomainDTO: BodyType<AuthtypesPostableAuthDomainDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAuthDomain200>({
return GeneratedAPIInstance<CreateAuthDomain201>({
url: `/api/v1/domains`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -277,19 +279,122 @@ export const useDeleteAuthDomain = <
return useMutation(mutationOptions);
};
/**
* This endpoint returns an auth domain by ID
* @summary Get auth domain by ID
*/
export const getAuthDomain = (
{ id }: GetAuthDomainPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetAuthDomain200>({
url: `/api/v1/domains/${id}`,
method: 'GET',
signal,
});
};
export const getGetAuthDomainQueryKey = ({
id,
}: GetAuthDomainPathParameters) => {
return [`/api/v1/domains/${id}`] as const;
};
export const getGetAuthDomainQueryOptions = <
TData = Awaited<ReturnType<typeof getAuthDomain>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetAuthDomainPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAuthDomain>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAuthDomainQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAuthDomain>>> = ({
signal,
}) => getAuthDomain({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getAuthDomain>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAuthDomainQueryResult = NonNullable<
Awaited<ReturnType<typeof getAuthDomain>>
>;
export type GetAuthDomainQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get auth domain by ID
*/
export function useGetAuthDomain<
TData = Awaited<ReturnType<typeof getAuthDomain>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetAuthDomainPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAuthDomain>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAuthDomainQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get auth domain by ID
*/
export const invalidateGetAuthDomain = async (
queryClient: QueryClient,
{ id }: GetAuthDomainPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAuthDomainQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates an auth domain
* @summary Update auth domain
*/
export const updateAuthDomain = (
{ id }: UpdateAuthDomainPathParameters,
authtypesUpdateableAuthDomainDTO: BodyType<AuthtypesUpdateableAuthDomainDTO>,
authtypesUpdatableAuthDomainDTO: BodyType<AuthtypesUpdatableAuthDomainDTO>,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/domains/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: authtypesUpdateableAuthDomainDTO,
data: authtypesUpdatableAuthDomainDTO,
});
};
@@ -302,7 +407,7 @@ export const getUpdateAuthDomainMutationOptions = <
TError,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
},
TContext
>;
@@ -311,7 +416,7 @@ export const getUpdateAuthDomainMutationOptions = <
TError,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
},
TContext
> => {
@@ -328,7 +433,7 @@ export const getUpdateAuthDomainMutationOptions = <
Awaited<ReturnType<typeof updateAuthDomain>>,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -343,7 +448,7 @@ export type UpdateAuthDomainMutationResult = NonNullable<
Awaited<ReturnType<typeof updateAuthDomain>>
>;
export type UpdateAuthDomainMutationBody =
BodyType<AuthtypesUpdateableAuthDomainDTO>;
BodyType<AuthtypesUpdatableAuthDomainDTO>;
export type UpdateAuthDomainMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -358,7 +463,7 @@ export const useUpdateAuthDomain = <
TError,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
},
TContext
>;
@@ -367,7 +472,7 @@ export const useUpdateAuthDomain = <
TError,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
},
TContext
> => {

View File

@@ -4,23 +4,16 @@
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import { useMutation } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
AuthtypesTransactionDTO,
AuthzCheck200,
AuthzResources200,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -110,88 +103,3 @@ export const useAuthzCheck = <
return useMutation(mutationOptions);
};
/**
* Gets all the available resources
* @summary Get resources
*/
export const authzResources = (signal?: AbortSignal) => {
return GeneratedAPIInstance<AuthzResources200>({
url: `/api/v1/authz/resources`,
method: 'GET',
signal,
});
};
export const getAuthzResourcesQueryKey = () => {
return [`/api/v1/authz/resources`] as const;
};
export const getAuthzResourcesQueryOptions = <
TData = Awaited<ReturnType<typeof authzResources>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof authzResources>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getAuthzResourcesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof authzResources>>> = ({
signal,
}) => authzResources(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof authzResources>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type AuthzResourcesQueryResult = NonNullable<
Awaited<ReturnType<typeof authzResources>>
>;
export type AuthzResourcesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get resources
*/
export function useAuthzResources<
TData = Awaited<ReturnType<typeof authzResources>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof authzResources>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getAuthzResourcesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get resources
*/
export const invalidateAuthzResources = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getAuthzResourcesQueryKey() },
options,
);
return queryClient;
};

View File

@@ -18,6 +18,7 @@ import type {
} from 'react-query';
import type {
AlertmanagertypesPostableChannelDTO,
ConfigReceiverDTO,
CreateChannel201,
DeleteChannelByIDPathParameters,
@@ -122,14 +123,14 @@ export const invalidateListChannels = async (
* @summary Create notification channel
*/
export const createChannel = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
alertmanagertypesPostableChannelDTO: BodyType<AlertmanagertypesPostableChannelDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateChannel201>({
url: `/api/v1/channels`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
data: alertmanagertypesPostableChannelDTO,
signal,
});
};
@@ -141,13 +142,13 @@ export const getCreateChannelMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
TContext
> => {
const mutationKey = ['createChannel'];
@@ -161,7 +162,7 @@ export const getCreateChannelMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createChannel>>,
{ data: BodyType<ConfigReceiverDTO> }
{ data: BodyType<AlertmanagertypesPostableChannelDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -174,7 +175,8 @@ export const getCreateChannelMutationOptions = <
export type CreateChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof createChannel>>
>;
export type CreateChannelMutationBody = BodyType<ConfigReceiverDTO>;
export type CreateChannelMutationBody =
BodyType<AlertmanagertypesPostableChannelDTO>;
export type CreateChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -187,13 +189,13 @@ export const useCreateChannel = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
TContext
> => {
const mutationOptions = getCreateChannelMutationOptions(options);

View File

@@ -18,9 +18,9 @@ import type {
} from 'react-query';
import type {
AuthtypesPatchableObjectsDTO,
AuthtypesPatchableRoleDTO,
AuthtypesPostableRoleDTO,
CoretypesPatchableObjectsDTO,
CreateRole201,
DeleteRolePathParameters,
GetObjects200,
@@ -571,13 +571,13 @@ export const invalidateGetObjects = async (
*/
export const patchObjects = (
{ id, relation }: PatchObjectsPathParameters,
authtypesPatchableObjectsDTO: BodyType<AuthtypesPatchableObjectsDTO>,
coretypesPatchableObjectsDTO: BodyType<CoretypesPatchableObjectsDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}/relations/${relation}/objects`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: authtypesPatchableObjectsDTO,
data: coretypesPatchableObjectsDTO,
});
};
@@ -590,7 +590,7 @@ export const getPatchObjectsMutationOptions = <
TError,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<AuthtypesPatchableObjectsDTO>;
data: BodyType<CoretypesPatchableObjectsDTO>;
},
TContext
>;
@@ -599,7 +599,7 @@ export const getPatchObjectsMutationOptions = <
TError,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<AuthtypesPatchableObjectsDTO>;
data: BodyType<CoretypesPatchableObjectsDTO>;
},
TContext
> => {
@@ -616,7 +616,7 @@ export const getPatchObjectsMutationOptions = <
Awaited<ReturnType<typeof patchObjects>>,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<AuthtypesPatchableObjectsDTO>;
data: BodyType<CoretypesPatchableObjectsDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -630,7 +630,7 @@ export const getPatchObjectsMutationOptions = <
export type PatchObjectsMutationResult = NonNullable<
Awaited<ReturnType<typeof patchObjects>>
>;
export type PatchObjectsMutationBody = BodyType<AuthtypesPatchableObjectsDTO>;
export type PatchObjectsMutationBody = BodyType<CoretypesPatchableObjectsDTO>;
export type PatchObjectsMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -645,7 +645,7 @@ export const usePatchObjects = <
TError,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<AuthtypesPatchableObjectsDTO>;
data: BodyType<CoretypesPatchableObjectsDTO>;
},
TContext
>;
@@ -654,7 +654,7 @@ export const usePatchObjects = <
TError,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<AuthtypesPatchableObjectsDTO>;
data: BodyType<CoretypesPatchableObjectsDTO>;
},
TContext
> => {

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,6 @@ import { getBasePath } from 'utils/basePath';
import { eventEmitter } from 'utils/getEventEmitter';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
import { retryRequestAfterAuth } from 'api/interceptors';
import { Logout } from './utils';
const RESPONSE_TIMEOUT_THRESHOLD = 5000; // 5 seconds
@@ -130,10 +129,13 @@ export const interceptorRejected = async (
afterLogin(response.data.accessToken, response.data.refreshToken, true);
try {
const reResponse = await retryRequestAfterAuth(
value.config,
response.data.accessToken,
);
const reResponse = await axios({
...value.config,
headers: {
...value.config.headers,
Authorization: `Bearer ${response.data.accessToken}`,
},
});
return await Promise.resolve(reResponse);
} catch (error) {

View File

@@ -1,65 +1,46 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import axios, { AxiosHeaders, AxiosResponse } from 'axios';
const { retryRequestMock, postRotateMock } = vi.hoisted(() => ({
retryRequestMock: vi.fn(),
postRotateMock: vi.fn(() =>
import { interceptorRejected } from './index';
jest.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: jest.fn(() => 'mock-token'),
}));
jest.mock('api/v2/sessions/rotate/post', () => ({
__esModule: true,
default: jest.fn(() =>
Promise.resolve({
data: { accessToken: 'new-token', refreshToken: 'new-refresh' },
}),
),
}));
vi.mock('api/interceptors', () => ({
jest.mock('AppRoutes/utils', () => ({
__esModule: true,
retryRequestAfterAuth: retryRequestMock,
default: jest.fn(),
}));
vi.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: vi.fn(() => 'mock-token'),
}));
jest.mock('axios', () => {
const actualAxios = jest.requireActual('axios');
const mockAxios = jest.fn().mockResolvedValue({ data: 'success' });
vi.mock('api/v2/sessions/rotate/post', () => ({
__esModule: true,
default: postRotateMock,
}));
vi.mock('AppRoutes/utils', () => ({
__esModule: true,
default: vi.fn(),
}));
vi.mock('axios', async () => {
const actual = await vi.importActual<typeof import('axios')>('axios');
return {
...actual,
default: Object.assign(actual.default, {
isAxiosError: vi.fn(() => true),
...actualAxios,
default: Object.assign(mockAxios, {
...actualAxios.default,
isAxiosError: jest.fn().mockReturnValue(true),
create: actualAxios.create,
}),
__esModule: true,
};
});
describe('interceptorRejected', () => {
let interceptorRejected: (value: AxiosResponse) => Promise<AxiosResponse>;
beforeAll(async () => {
vi.resetModules();
const mod = await import('./index');
interceptorRejected = mod.interceptorRejected;
});
beforeEach(() => {
vi.clearAllMocks();
retryRequestMock.mockResolvedValue({
data: 'success',
} as unknown as AxiosResponse<{ data: string }>);
(
axios.isAxiosError as unknown as {
mockReturnValue: (value: boolean) => void;
}
).mockReturnValue(true);
jest.clearAllMocks();
(axios as unknown as jest.Mock).mockResolvedValue({ data: 'success' });
(axios.isAxiosError as unknown as jest.Mock).mockReturnValue(true);
});
it('should preserve array payload structure when retrying a 401 request', async () => {
@@ -94,12 +75,11 @@ describe('interceptorRejected', () => {
// Expected to reject after retry
}
expect(retryRequestMock).toHaveBeenCalledTimes(1);
const retryCallConfig = retryRequestMock.mock.calls[0][0];
expect(Array.isArray(JSON.parse(retryCallConfig.data as string))).toBe(true);
expect(JSON.parse(retryCallConfig.data as string)).toStrictEqual(
arrayPayload,
);
const mockAxiosFn = axios as unknown as jest.Mock;
expect(mockAxiosFn.mock.calls).toHaveLength(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(Array.isArray(JSON.parse(retryCallConfig.data))).toBe(true);
expect(JSON.parse(retryCallConfig.data)).toStrictEqual(arrayPayload);
});
it('should preserve object payload structure when retrying a 401 request', async () => {
@@ -131,11 +111,10 @@ describe('interceptorRejected', () => {
// Expected to reject after retry
}
expect(retryRequestMock).toHaveBeenCalledTimes(1);
const retryCallConfig = retryRequestMock.mock.calls[0][0];
expect(JSON.parse(retryCallConfig.data as string)).toStrictEqual(
objectPayload,
);
const mockAxiosFn = axios as unknown as jest.Mock;
expect(mockAxiosFn.mock.calls).toHaveLength(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(JSON.parse(retryCallConfig.data)).toStrictEqual(objectPayload);
});
it('should handle undefined data gracefully when retrying', async () => {
@@ -165,8 +144,9 @@ describe('interceptorRejected', () => {
// Expected to reject after retry
}
expect(retryRequestMock).toHaveBeenCalledTimes(1);
const retryCallConfig = retryRequestMock.mock.calls[0][0];
const mockAxiosFn = axios as unknown as jest.Mock;
expect(mockAxiosFn.mock.calls).toHaveLength(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(retryCallConfig.data).toBeUndefined();
});
});

View File

@@ -1,17 +0,0 @@
import axios, {
AxiosHeaders,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
export async function retryRequestAfterAuth(
valueConfig: InternalAxiosRequestConfig,
accessToken: string,
): Promise<AxiosResponse> {
const headers = new AxiosHeaders(valueConfig.headers);
headers.set('Authorization', `Bearer ${accessToken}`);
return axios({
...valueConfig,
headers,
});
}

View File

@@ -0,0 +1,72 @@
import { ApiV3Instance as axios } from 'api';
import { omit } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
GetTraceV3PayloadProps,
GetTraceV3SuccessResponse,
SpanV3,
} from 'types/api/trace/getTraceV3';
const getTraceV3 = async (
props: GetTraceV3PayloadProps,
): Promise<SuccessResponse<GetTraceV3SuccessResponse> | ErrorResponse> => {
let uncollapsedSpans = [...props.uncollapsedSpans];
if (!props.isSelectedSpanIDUnCollapsed) {
uncollapsedSpans = uncollapsedSpans.filter(
(node) => node !== props.selectedSpanId,
);
} else if (
props.selectedSpanId &&
!uncollapsedSpans.includes(props.selectedSpanId)
) {
// V3 backend only uses uncollapsedSpans list (unlike V2 which also interprets
// isSelectedSpanIDUnCollapsed server-side), so explicitly add the selected span
uncollapsedSpans.push(props.selectedSpanId);
}
const postData: GetTraceV3PayloadProps = {
...props,
uncollapsedSpans,
limit: 10000,
};
const response = await axios.post<GetTraceV3SuccessResponse>(
`/traces/${props.traceId}/waterfall`,
omit(postData, 'traceId'),
);
// V3 API wraps response in { status, data }
const rawPayload = (response.data as any).data || response.data;
// Derive 'service.name' from resource for convenience — only derived field
const spans: SpanV3[] = (rawPayload.spans || []).map((span: any) => ({
...span,
'service.name': span.resource?.['service.name'] || '',
}));
// V3 API returns startTimestampMillis/endTimestampMillis as relative durations (ms from epoch offset),
// not absolute unix millis like V2. The span timestamps are absolute unix millis.
// Convert by using the first span's timestamp as the base if there's a mismatch.
let { startTimestampMillis, endTimestampMillis } = rawPayload;
if (
spans.length > 0 &&
spans[0].timestamp > 0 &&
startTimestampMillis < spans[0].timestamp / 10
) {
const durationMillis = endTimestampMillis - startTimestampMillis;
startTimestampMillis = spans[0].timestamp;
endTimestampMillis = startTimestampMillis + durationMillis;
}
return {
statusCode: 200,
error: null,
message: 'Success',
payload: {
...rawPayload,
spans,
startTimestampMillis,
endTimestampMillis,
},
};
};
export default getTraceV3;

View File

@@ -1,4 +1,3 @@
import { describe, expect, it } from 'vitest';
import { SuccessResponse } from 'types/api';
import {
MetricRangePayloadV5,

View File

@@ -1,7 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderFormula,
@@ -22,9 +20,9 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { prepareQueryRangePayloadV5 } from './prepareQueryRangePayloadV5';
vi.mock('lib/getStartEndRangeTime', () => ({
jest.mock('lib/getStartEndRangeTime', () => ({
__esModule: true,
default: vi.fn(() => ({ start: '100', end: '200' })),
default: jest.fn(() => ({ start: '100', end: '200' })),
}));
describe('prepareQueryRangePayloadV5', () => {
@@ -517,7 +515,9 @@ describe('prepareQueryRangePayloadV5', () => {
});
it('maps groupBy, order, having, aggregations and filter for logs builder query', () => {
vi.mocked(getStartEndRangeTime).mockReturnValueOnce({
const getStartEndRangeTime = jest.requireMock('lib/getStartEndRangeTime')
.default as jest.Mock;
getStartEndRangeTime.mockReturnValueOnce({
start: '1754623641',
end: '1754645241',
});

View File

@@ -1,16 +1,15 @@
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import getLocal from '../../../api/browser/localstorage/get';
import AppLoading from '../AppLoading';
vi.mock('../../../api/browser/localstorage/get', () => ({
jest.mock('../../../api/browser/localstorage/get', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
// Access the mocked function
const mockGet = vi.mocked(getLocal);
const mockGet = getLocal as unknown as jest.Mock;
describe('AppLoading', () => {
const SIGNOZ_TEXT = 'SigNoz';
@@ -19,12 +18,12 @@ describe('AppLoading', () => {
const CONTAINER_SELECTOR = '.app-loading-container';
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('should render loading screen with dark theme by default', () => {
// Mock localStorage to return dark theme (or undefined for default)
mockGet.mockReturnValue(null);
mockGet.mockReturnValue(undefined);
render(<AppLoading />);
@@ -41,17 +40,14 @@ describe('AppLoading', () => {
it('should have proper structure and content', () => {
// Mock localStorage to return dark theme
mockGet.mockReturnValue(null);
mockGet.mockReturnValue(undefined);
render(<AppLoading />);
// Check for brand logo
const logo = screen.getByAltText(SIGNOZ_TEXT);
expect(logo).toBeInTheDocument();
expect(logo).toHaveAttribute(
'src',
expect.stringContaining('data:image/svg+xml'),
);
expect(logo).toHaveAttribute('src', 'test-file-stub');
// Check for brand title
const title = screen.getByText(SIGNOZ_TEXT);

View File

@@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { fireEvent, render, screen } from '@testing-library/react';
import { USER_PREFERENCES } from 'constants/userPreferences';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import {
ChangelogSchema,
DeploymentType,
} from 'types/api/changelog/getChangelogByVersion';
import { describe, expect, it, vi } from 'vitest';
import ChangelogModal from '../ChangelogModal';
@@ -37,25 +37,27 @@ const mockChangelog: ChangelogSchema = {
};
// Mock react-markdown to just render children as plain text
vi.mock('react-markdown', () => ({
default: function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
}));
jest.mock(
'react-markdown',
() =>
function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
);
// mock useAppContext
vi.mock('providers/App/App', () => ({
useAppContext: vi.fn(() => ({
updateUserPreferenceInContext: vi.fn(),
jest.mock('providers/App/App', () => ({
useAppContext: jest.fn(() => ({
updateUserPreferenceInContext: jest.fn(),
userPreferences: [
{
name: 'last_seen_changelog_version',
name: USER_PREFERENCES.LAST_SEEN_CHANGELOG_VERSION,
value: 'v1.0.0',
},
],
})),
}));
function renderChangelog(onClose: () => void = vi.fn()): void {
function renderChangelog(onClose: () => void = jest.fn()): void {
render(
<MockQueryClientProvider>
<ChangelogModal changelog={mockChangelog} onClose={onClose} />
@@ -76,14 +78,14 @@ describe('ChangelogModal', () => {
});
it('calls onClose when Skip for now is clicked', () => {
const onClose = vi.fn();
const onClose = jest.fn();
renderChangelog(onClose);
fireEvent.click(screen.getByText('Skip for now'));
expect(onClose).toHaveBeenCalled();
});
it('opens migration docs when Update my workspace is clicked', () => {
window.open = vi.fn();
window.open = jest.fn();
renderChangelog();
fireEvent.click(screen.getByText('Update my workspace'));
expect(window.open).toHaveBeenCalledWith(
@@ -98,7 +100,7 @@ describe('ChangelogModal', () => {
const scrollBtn = screen.getByTestId('scroll-more-btn');
const contentDiv = screen.getByTestId('changelog-content');
if (contentDiv) {
contentDiv.scrollTo = vi.fn();
contentDiv.scrollTo = jest.fn();
}
fireEvent.click(scrollBtn);
if (contentDiv) {

View File

@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import {
ChangelogSchema,
DeploymentType,
@@ -10,11 +9,13 @@ import {
import ChangelogRenderer from '../components/ChangelogRenderer';
// Mock react-markdown to just render children as plain text
vi.mock('react-markdown', () => ({
default: function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
}));
jest.mock(
'react-markdown',
() =>
function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
);
const mockChangelog: ChangelogSchema = {
id: 1,

View File

@@ -1,40 +1,16 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import CodeBlock from './CodeBlock';
const { mockCopyToClipboard } = vi.hoisted(() => ({
mockCopyToClipboard: vi.fn(),
}));
const mockCopyToClipboard = jest.fn();
vi.mock('react-use', () => ({
jest.mock('react-use', () => ({
useCopyToClipboard: (): [unknown, (text: string) => void] => [
undefined,
mockCopyToClipboard,
],
}));
vi.mock('@signozhq/icons', () => ({
Check: (): null => null,
Copy: (): null => null,
}));
vi.mock('@signozhq/ui', async () => {
const React = await vi.importActual<typeof import('react')>('react');
return {
Button: ({
prefix,
...props
}: {
prefix?: ReactNode;
[key: string]: unknown;
}): ReturnType<typeof React.createElement> =>
React.createElement('button', props, prefix),
};
});
describe('CodeBlock', () => {
beforeEach(() => {
mockCopyToClipboard.mockReset();
@@ -57,7 +33,7 @@ describe('CodeBlock', () => {
});
it('copies code and triggers callback', async () => {
const onCopy = vi.fn();
const onCopy = jest.fn();
render(<CodeBlock code="SELECT * FROM logs;" onCopy={onCopy} />);
fireEvent.click(screen.getByRole('button', { name: /copy code/i }));

View File

@@ -1,49 +1,28 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { toast } from '@signozhq/ui';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import {
render,
screen,
userEvent,
waitFor,
waitForElementToBeRemoved,
} from 'tests/test-utils';
import CreateServiceAccountModal from '../CreateServiceAccountModal';
vi.mock('@signozhq/icons', () => ({
X: ({ size: _size }: any): JSX.Element => <span aria-hidden="true" />,
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
}));
vi.mock('@signozhq/ui', () => ({
Button: ({
children,
loading: _loading,
variant: _variant,
color: _color,
...props
}: any): JSX.Element => <button {...props}>{children}</button>,
DialogFooter: ({ children, ...props }: any): JSX.Element => (
<div {...props}>{children}</div>
),
DialogWrapper: ({ title, open, children }: any): JSX.Element | null =>
open ? (
<div role="dialog" aria-label={title}>
{children}
</div>
) : null,
Input: (props: any): JSX.Element => <input {...props} />,
toast: { success: vi.fn(), error: vi.fn() },
}));
const mockToast = jest.mocked(toast);
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
const mockToast = vi.mocked(toast);
const showErrorModal = vi.hoisted(() => vi.fn());
vi.mock('providers/ErrorModalProvider', async () => ({
const showErrorModal = jest.fn();
jest.mock('providers/ErrorModalProvider', () => ({
__esModule: true,
...(await vi.importActual('providers/ErrorModalProvider')),
useErrorModal: vi.fn(() => ({
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal,
isErrorModalVisible: false,
})),
@@ -61,7 +40,7 @@ function renderModal(): ReturnType<typeof render> {
describe('CreateServiceAccountModal', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
server.use(
rest.post(SERVICE_ACCOUNTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(201), ctx.json({ status: 'success', data: {} })),
@@ -147,16 +126,12 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await screen.findByRole('dialog', {
const dialog = await screen.findByRole('dialog', {
name: /New Service Account/i,
});
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(dialog);
});
it('shows "Name is required" after clearing the name field', async () => {

View File

@@ -1,37 +1,36 @@
import { useState } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import dayjs from 'dayjs';
import { describe, expect, it, vi } from 'vitest';
import * as timeUtils from 'utils/timeUtils';
import CustomTimePicker from './CustomTimePicker';
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useLocation: vi.fn().mockReturnValue({
useLocation: jest.fn().mockReturnValue({
pathname: '/test-path',
}),
};
});
vi.mock('react-redux', async () => ({
...(await vi.importActual('react-redux')),
useDispatch: vi.fn(() => vi.fn()),
useSelector: vi.fn(() => ({
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn(() => jest.fn()),
useSelector: jest.fn(() => ({
minTime: 0,
maxTime: Date.now(),
})),
}));
vi.mock('providers/Timezone', async () => {
const actual = await vi.importActual('providers/Timezone');
jest.mock('providers/Timezone', () => {
const actual = jest.requireActual('providers/Timezone');
return {
...actual,
useTimezone: vi.fn().mockReturnValue({
useTimezone: jest.fn().mockReturnValue({
timezone: {
value: 'UTC',
offset: '+00:00',
@@ -46,30 +45,6 @@ vi.mock('providers/Timezone', async () => {
};
});
vi.mock('@signozhq/ui', () => ({
Button: ({
children,
prefix,
variant: _variant,
color: _color,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
prefix?: React.ReactNode;
variant?: string;
color?: string;
}): JSX.Element => (
<button type="button" {...props}>
{prefix}
{children}
</button>
),
Calendar: (): JSX.Element => <div data-testid="mock-calendar" />,
}));
vi.mock('hooks/useZoomOut', () => ({
useZoomOut: vi.fn(() => vi.fn()),
}));
interface WrapperProps {
initialValue?: string;
showLiveLogs?: boolean;
@@ -148,8 +123,8 @@ describe('CustomTimePicker', () => {
});
it('applies valid shorthand on Enter', () => {
const onValid = vi.fn();
const onError = vi.fn();
const onValid = jest.fn();
const onError = jest.fn();
render(<Wrapper onValidCustomDateChange={onValid} onError={onError} />);
@@ -166,9 +141,9 @@ describe('CustomTimePicker', () => {
});
it('sets error and updates custom time status for invalid shorthand exceeding max allowed window', () => {
const onValid = vi.fn();
const onError = vi.fn();
const onCustomTimeStatusUpdate = vi.fn();
const onValid = jest.fn();
const onError = jest.fn();
const onCustomTimeStatusUpdate = jest.fn();
render(
<Wrapper
@@ -191,8 +166,8 @@ describe('CustomTimePicker', () => {
});
it('treats close after change like pressing Enter (blur + chevron)', () => {
const onValid = vi.fn();
const onError = vi.fn();
const onValid = jest.fn();
const onError = jest.fn();
render(<Wrapper onValidCustomDateChange={onValid} onError={onError} />);
@@ -216,8 +191,8 @@ describe('CustomTimePicker', () => {
});
it('applies epoch start/end range on Enter via onCustomDateHandler', () => {
const onCustomDateHandler = vi.fn();
const onError = vi.fn();
const onCustomDateHandler = jest.fn();
const onError = jest.fn();
render(
<Wrapper onCustomDateHandler={onCustomDateHandler} onError={onError} />,
@@ -238,9 +213,9 @@ describe('CustomTimePicker', () => {
});
it('uses validateTimeRange result for generic formatted ranges (valid case)', () => {
const validateTimeRangeSpy = vi.spyOn(timeUtils, 'validateTimeRange');
const onCustomDateHandler = vi.fn();
const onError = vi.fn();
const validateTimeRangeSpy = jest.spyOn(timeUtils, 'validateTimeRange');
const onCustomDateHandler = jest.fn();
const onError = jest.fn();
validateTimeRangeSpy.mockReturnValue({
isValid: true,
@@ -269,9 +244,9 @@ describe('CustomTimePicker', () => {
});
it('uses validateTimeRange result for generic formatted ranges (invalid case)', () => {
const validateTimeRangeSpy = vi.spyOn(timeUtils, 'validateTimeRange');
const onValid = vi.fn();
const onError = vi.fn();
const validateTimeRangeSpy = jest.spyOn(timeUtils, 'validateTimeRange');
const onValid = jest.fn();
const onError = jest.fn();
validateTimeRangeSpy.mockReturnValue({
isValid: false,

View File

@@ -2,33 +2,23 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryParams } from 'constants/query';
import { GlobalReducer } from 'types/reducer/globalTime';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import CustomTimePicker from '../CustomTimePicker';
const {
MS_PER_MIN,
NOW_MS,
mockDispatch,
mockSafeNavigate,
mockUrlQueryDelete,
mockUrlQuerySet,
} = vi.hoisted(() => ({
MS_PER_MIN: 60 * 1000,
NOW_MS: 1705312800000,
mockDispatch: vi.fn(),
mockSafeNavigate: vi.fn(),
mockUrlQueryDelete: vi.fn(),
mockUrlQuerySet: vi.fn(),
}));
const MS_PER_MIN = 60 * 1000;
const NOW_MS = 1705312800000;
const mockDispatch = jest.fn();
const mockSafeNavigate = jest.fn();
const mockUrlQueryDelete = jest.fn();
const mockUrlQuerySet = jest.fn();
interface MockAppState {
globalTime: Pick<GlobalReducer, 'minTime' | 'maxTime'>;
}
vi.mock('react-redux', () => ({
useDispatch: (): Mock => mockDispatch,
jest.mock('react-redux', () => ({
useDispatch: (): jest.Mock => mockDispatch,
useSelector: (selector: (state: MockAppState) => unknown): unknown => {
const mockState: MockAppState = {
globalTime: {
@@ -40,8 +30,8 @@ vi.mock('react-redux', () => ({
},
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: Mock } => ({
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: mockSafeNavigate,
}),
}));
@@ -53,7 +43,7 @@ interface MockUrlQuery {
toString: () => string;
}
vi.mock('hooks/useUrlQuery', () => ({
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: (): MockUrlQuery => ({
delete: mockUrlQueryDelete,
@@ -63,46 +53,26 @@ vi.mock('hooks/useUrlQuery', () => ({
}),
}));
vi.mock('providers/Timezone', () => ({
jest.mock('providers/Timezone', () => ({
useTimezone: (): { timezone: { value: string; offset: string } } => ({
timezone: { value: 'UTC', offset: 'UTC' },
}),
}));
vi.mock('react-router-dom', () => ({
jest.mock('react-router-dom', () => ({
useLocation: (): { pathname: string } => ({ pathname: '/logs-explorer' }),
}));
vi.mock('@signozhq/ui', () => ({
Button: ({
children,
prefix,
variant: _variant,
color: _color,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
prefix?: React.ReactNode;
variant?: string;
color?: string;
}): JSX.Element => (
<button type="button" {...props}>
{prefix}
{children}
</button>
),
Calendar: (): JSX.Element => <div data-testid="mock-calendar" />,
}));
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const now = Date.now();
const defaultProps = {
onSelect: vi.fn(),
onError: vi.fn(),
onSelect: jest.fn(),
onError: jest.fn(),
selectedValue: '15m',
selectedTime: '15m',
onValidCustomDateChange: vi.fn(),
onValidCustomDateChange: jest.fn(),
open: false,
setOpen: vi.fn(),
setOpen: jest.fn(),
items: [
{ value: '15m', label: 'Last 15 minutes' },
{ value: '1h', label: 'Last 1 hour' },
@@ -113,12 +83,12 @@ const defaultProps = {
describe('CustomTimePicker - zoom out button', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(Date, 'now').mockReturnValue(NOW_MS);
jest.clearAllMocks();
jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
});
afterEach(() => {
vi.restoreAllMocks();
jest.restoreAllMocks();
});
it('should render zoom out button when showLiveLogs is false', () => {

View File

@@ -0,0 +1,37 @@
.details-header {
// ghost + secondary missing hover bg token in @signozhq/button
--button-ghost-hover-background: var(--l3-background);
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
padding: 0 12px;
border-bottom: 1px solid var(--l1-border);
height: 36px;
background: var(--l2-background);
&__title {
font-size: 13px;
font-weight: 500;
color: var(--l1-foreground);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
&__nav {
display: flex;
align-items: center;
gap: 2px;
}
}

View File

@@ -0,0 +1,57 @@
import { ReactNode } from 'react';
import { Button } from '@signozhq/ui';
import { X } from '@signozhq/icons';
import './DetailsHeader.styles.scss';
export interface HeaderAction {
key: string;
component: ReactNode; // check later if we can use direct btn itself or not.
}
export interface DetailsHeaderProps {
title: string;
onClose: () => void;
actions?: HeaderAction[];
closePosition?: 'left' | 'right';
className?: string;
}
function DetailsHeader({
title,
onClose,
actions,
closePosition = 'right',
className,
}: DetailsHeaderProps): JSX.Element {
const closeButton = (
<Button
variant="ghost"
size="icon"
color="secondary"
onClick={onClose}
aria-label="Close"
prefix={<X size={14} />}
></Button>
);
return (
<div className={`details-header ${className || ''}`}>
{closePosition === 'left' && closeButton}
<span className="details-header__title">{title}</span>
{actions && (
<div className="details-header__actions">
{actions.map((action) => (
<div key={action.key}>{action.component}</div>
))}
</div>
)}
{closePosition === 'right' && closeButton}
</div>
);
}
export default DetailsHeader;

View File

@@ -0,0 +1,7 @@
.details-panel-drawer {
&__body {
display: flex;
flex-direction: column;
height: 100%;
}
}

View File

@@ -0,0 +1,35 @@
import { DrawerWrapper } from '@signozhq/ui';
import './DetailsPanelDrawer.styles.scss';
interface DetailsPanelDrawerProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
className?: string;
}
function DetailsPanelDrawer({
isOpen,
onClose,
children,
className,
}: DetailsPanelDrawerProps): JSX.Element {
return (
<DrawerWrapper
open={isOpen}
onOpenChange={(open): void => {
if (!open) {
onClose();
}
}}
direction="right"
showOverlay={false}
className={`details-panel-drawer ${className || ''}`}
>
<div className="details-panel-drawer__body">{children}</div>
</DrawerWrapper>
);
}
export default DetailsPanelDrawer;

View File

@@ -0,0 +1,8 @@
export type {
DetailsHeaderProps,
HeaderAction,
} from './DetailsHeader/DetailsHeader';
export { default as DetailsHeader } from './DetailsHeader/DetailsHeader';
export { default as DetailsPanelDrawer } from './DetailsPanelDrawer';
export type { DetailsPanelState, UseDetailsPanelOptions } from './types';
export { default as useDetailsPanel } from './useDetailsPanel';

View File

@@ -0,0 +1,10 @@
export interface DetailsPanelState {
isOpen: boolean;
open: () => void;
close: () => void;
}
export interface UseDetailsPanelOptions {
entityId: string | undefined;
onClose?: () => void;
}

View File

@@ -0,0 +1,29 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { DetailsPanelState, UseDetailsPanelOptions } from './types';
function useDetailsPanel({
entityId,
onClose,
}: UseDetailsPanelOptions): DetailsPanelState {
const [isOpen, setIsOpen] = useState<boolean>(false);
const prevEntityIdRef = useRef<string>('');
useEffect(() => {
const currentId = entityId || '';
if (currentId && currentId !== prevEntityIdRef.current) {
setIsOpen(true);
}
prevEntityIdRef.current = currentId;
}, [entityId]);
const open = useCallback(() => setIsOpen(true), []);
const close = useCallback(() => {
setIsOpen(false);
onClose?.();
}, [onClose]);
return { isOpen, open, close };
}
export default useDetailsPanel;

View File

@@ -1,6 +1,3 @@
import type { Mock } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
@@ -16,28 +13,25 @@ import '@testing-library/jest-dom';
import { DownloadFormats, DownloadRowCounts } from './constants';
import DownloadOptionsMenu from './DownloadOptionsMenu';
const { mockDownloadExportData, mockUseQueryBuilder } = vi.hoisted(() => ({
mockDownloadExportData: vi.fn().mockResolvedValue(undefined),
mockUseQueryBuilder: vi.fn(),
}));
vi.mock('api/v1/download/downloadExportData', () => ({
const mockDownloadExportData = jest.fn().mockResolvedValue(undefined);
jest.mock('api/v1/download/downloadExportData', () => ({
downloadExportData: (...args: any[]): any => mockDownloadExportData(...args),
default: (...args: any[]): any => mockDownloadExportData(...args),
}));
vi.mock('antd', async () => {
const actual = await vi.importActual<typeof import('antd')>('antd');
jest.mock('antd', () => {
const actual = jest.requireActual('antd');
return {
...actual,
message: {
success: vi.fn(),
error: vi.fn(),
success: jest.fn(),
error: jest.fn(),
},
};
});
vi.mock('hooks/queryBuilder/useQueryBuilder', () => ({
const mockUseQueryBuilder = jest.fn();
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): any => mockUseQueryBuilder(),
}));
@@ -101,8 +95,8 @@ describe.each([
beforeEach(() => {
mockDownloadExportData.mockReset().mockResolvedValue(undefined);
(message.success as Mock).mockReset();
(message.error as Mock).mockReset();
(message.success as jest.Mock).mockReset();
(message.error as jest.Mock).mockReset();
mockUseQueryBuilder.mockReturnValue({
stagedQuery: createMockStagedQuery(dataSource),
});
@@ -313,11 +307,7 @@ describe.each([
fireEvent.click(screen.getByText('Export'));
expect(screen.getByTestId(testId)).toBeDisabled();
await waitFor(() => {
expect(screen.getByRole('dialog').closest('.ant-popover')).toHaveStyle({
pointerEvents: 'none',
});
});
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
resolveDownload!();
@@ -333,7 +323,7 @@ describe('DownloadOptionsMenu for traces with queryTraceOperator', () => {
beforeEach(() => {
mockDownloadExportData.mockReset().mockResolvedValue(undefined);
(message.success as Mock).mockReset();
(message.success as jest.Mock).mockReset();
});
it('applies limit and clears groupBy on queryTraceOperator entries', async () => {

View File

@@ -1,28 +1,27 @@
import { render } from '@testing-library/react';
import { Table } from 'antd';
import { beforeAll, describe, expect, it, vi } from 'vitest';
import DraggableTableRow from '..';
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
});
vi.mock('react-dnd', () => ({
useDrop: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), vi.fn()]),
useDrag: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), vi.fn()]),
jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
}));
describe('DraggableTableRow Snapshot test', () => {

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`DraggableTableRow Snapshot test > should render DraggableTableRow 1`] = `
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
<DocumentFragment>
<div
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap"

View File

@@ -1,16 +1,14 @@
import { describe, expect, it, vi } from 'vitest';
import { dragHandler, dropHandler } from '../utils';
vi.mock('react-dnd', () => ({
useDrop: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), vi.fn()]),
useDrag: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), vi.fn()]),
jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
}));
describe('Utils testing of DraggableTableRow component', () => {
it('Should dropHandler return true', () => {
const monitor = {
isOver: vi.fn().mockReturnValueOnce(true),
isOver: jest.fn().mockReturnValueOnce(true),
} as never;
const dropDataTruthy = dropHandler(monitor);
@@ -19,7 +17,7 @@ describe('Utils testing of DraggableTableRow component', () => {
it('Should dropHandler return false', () => {
const monitor = {
isOver: vi.fn().mockReturnValueOnce(false),
isOver: jest.fn().mockReturnValueOnce(false),
} as never;
const dropDataFalsy = dropHandler(monitor);
@@ -28,7 +26,7 @@ describe('Utils testing of DraggableTableRow component', () => {
it('Should dragHandler return true', () => {
const monitor = {
isDragging: vi.fn().mockReturnValueOnce(true),
isDragging: jest.fn().mockReturnValueOnce(true),
} as never;
const dragDataTruthy = dragHandler(monitor);
@@ -37,7 +35,7 @@ describe('Utils testing of DraggableTableRow component', () => {
it('Should dragHandler return false', () => {
const monitor = {
isDragging: vi.fn().mockReturnValueOnce(false),
isDragging: jest.fn().mockReturnValueOnce(false),
} as never;
const dragDataFalsy = dragHandler(monitor);

View File

@@ -1,9 +1,6 @@
import type { ChangeEventHandler, ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import type { ReactNode } from 'react';
import { toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRoles } from 'api/generated/services/role';
import {
useCreateResetPasswordToken,
useDeleteUser,
@@ -18,138 +15,88 @@ import {
listRolesSuccessResponse,
managedRoles,
} from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
vi.mock('api/generated/services/role', async () => {
const actual = await vi.importActual<
typeof import('api/generated/services/role')
>('api/generated/services/role');
return {
...actual,
useListRoles: vi.fn(),
};
});
vi.mock('api/generated/services/users', () => ({
useDeleteUser: vi.fn(),
useGetUser: vi.fn(),
useUpdateUser: vi.fn(),
useUpdateMyUserV2: vi.fn(),
useSetRoleByUserID: vi.fn(),
useGetResetPasswordToken: vi.fn(),
useCreateResetPasswordToken: vi.fn(),
jest.mock('api/generated/services/users', () => ({
useDeleteUser: jest.fn(),
useGetUser: jest.fn(),
useUpdateUser: jest.fn(),
useUpdateMyUserV2: jest.fn(),
useSetRoleByUserID: jest.fn(),
useGetResetPasswordToken: jest.fn(),
useCreateResetPasswordToken: jest.fn(),
}));
vi.mock('api/ErrorResponseHandlerForGeneratedAPIs', () => ({
convertToApiError: vi.fn(),
jest.mock('api/ErrorResponseHandlerForGeneratedAPIs', () => ({
convertToApiError: jest.fn(),
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): Mock => vi.fn(),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footer}
</div>
) : null,
DialogWrapper: ({
children,
footer,
open,
title,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
title?: string;
}): JSX.Element | null =>
open ? (
<div role="dialog" aria-label={title}>
{children}
{footer}
</div>
) : null,
DialogFooter: ({ children }: { children?: ReactNode }): JSX.Element => (
<div>{children}</div>
),
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));
vi.mock('@signozhq/ui', async () => {
const React = await vi.importActual<typeof import('react')>('react');
const mockCopyToClipboard = jest.fn();
const mockCopyState = { value: undefined, error: undefined };
return {
Badge: ({ children }: { children?: ReactNode }): JSX.Element =>
React.createElement('span', null, children),
Button: ({
children,
disabled,
onClick,
prefix,
}: {
children?: ReactNode;
disabled?: boolean;
onClick?: () => void;
prefix?: ReactNode;
}): JSX.Element =>
React.createElement('button', { disabled, onClick }, prefix, children),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? React.createElement('div', null, children, footer) : null,
DialogWrapper: ({
children,
footer,
open,
title,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
title?: string;
}): JSX.Element | null =>
open
? React.createElement(
'div',
{ role: 'dialog', 'aria-label': title },
children,
footer,
)
: null,
DialogFooter: ({ children }: { children?: ReactNode }): JSX.Element =>
React.createElement('div', null, children),
Input: ({
disabled,
id,
onChange,
placeholder,
value,
}: {
disabled?: boolean;
id?: string;
onChange?: ChangeEventHandler<HTMLInputElement>;
placeholder?: string;
value?: string;
}): JSX.Element =>
React.createElement('input', {
disabled,
id,
onChange,
placeholder,
value,
}),
toast: {
success: vi.fn(),
error: vi.fn(),
},
};
});
const { mockCopyToClipboard, mockCopyState, showErrorModal } = vi.hoisted(
() => ({
mockCopyToClipboard: vi.fn(),
mockCopyState: { value: undefined, error: undefined },
showErrorModal: vi.fn(),
}),
);
vi.mock('react-use', () => ({
jest.mock('react-use', () => ({
useCopyToClipboard: (): [typeof mockCopyState, typeof mockCopyToClipboard] => [
mockCopyState,
mockCopyToClipboard,
],
}));
const mockDeleteMutate = vi.fn();
const mockCreateTokenMutateAsync = vi.fn();
const ROLES_ENDPOINT = '*/api/v1/roles';
vi.mock('providers/ErrorModalProvider', async () => ({
const mockDeleteMutate = jest.fn();
const mockCreateTokenMutateAsync = jest.fn();
const showErrorModal = jest.fn();
jest.mock('providers/ErrorModalProvider', () => ({
__esModule: true,
...(await vi.importActual<typeof import('providers/ErrorModalProvider')>(
'providers/ErrorModalProvider',
)),
useErrorModal: vi.fn(() => ({
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal,
isErrorModalVisible: false,
})),
@@ -208,8 +155,8 @@ function renderDrawer(
<EditMemberDrawer
member={activeMember}
open
onClose={vi.fn()}
onComplete={vi.fn()}
onClose={jest.fn()}
onComplete={jest.fn()}
{...props}
/>,
);
@@ -217,43 +164,38 @@ function renderDrawer(
describe('EditMemberDrawer', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
mockCopyState.value = undefined;
mockCopyState.error = undefined;
showErrorModal.mockClear();
(useListRoles as Mock).mockReturnValue({
data: listRolesSuccessResponse,
isLoading: false,
isError: false,
error: null,
refetch: vi.fn(),
isFetching: false,
isSuccess: true,
status: 'success',
});
(useGetUser as Mock).mockReturnValue({
server.use(
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
(useGetUser as jest.Mock).mockReturnValue({
data: mockFetchedUser,
isLoading: false,
refetch: vi.fn(),
refetch: jest.fn(),
});
(useUpdateUser as Mock).mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({}),
(useUpdateUser as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockResolvedValue({}),
isLoading: false,
});
(useUpdateMyUserV2 as Mock).mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({}),
(useUpdateMyUserV2 as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockResolvedValue({}),
isLoading: false,
});
(useSetRoleByUserID as Mock).mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({}),
(useSetRoleByUserID as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockResolvedValue({}),
isLoading: false,
});
(useDeleteUser as Mock).mockReturnValue({
(useDeleteUser as jest.Mock).mockReturnValue({
mutate: mockDeleteMutate,
isLoading: false,
});
// Token query: valid token for invited members
(useGetResetPasswordToken as Mock).mockReturnValue({
(useGetResetPasswordToken as jest.Mock).mockReturnValue({
data: {
data: {
token: 'invite-tok-valid',
@@ -273,18 +215,20 @@ describe('EditMemberDrawer', () => {
expiresAt: new Date(Date.now() + 86400000).toISOString(),
},
});
(useCreateResetPasswordToken as Mock).mockReturnValue({
(useCreateResetPasswordToken as jest.Mock).mockReturnValue({
mutateAsync: mockCreateTokenMutateAsync,
isLoading: false,
});
});
it('renders active member details and disables Save when form is not dirty', async () => {
afterEach(() => {
server.resetHandlers();
});
it('renders active member details and disables Save when form is not dirty', () => {
renderDrawer();
await expect(
screen.findByDisplayValue('Alice Smith'),
).resolves.toBeInTheDocument();
expect(screen.getByDisplayValue('Alice Smith')).toBeInTheDocument();
expect(screen.getByText('alice@signoz.io')).toBeInTheDocument();
expect(screen.getByText('ACTIVE')).toBeInTheDocument();
expect(
@@ -293,11 +237,11 @@ describe('EditMemberDrawer', () => {
});
it('enables Save after editing name and calls updateUser on confirm', async () => {
const onComplete = vi.fn();
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockMutateAsync = vi.fn().mockResolvedValue({});
const mockMutateAsync = jest.fn().mockResolvedValue({});
(useUpdateUser as Mock).mockReturnValue({
(useUpdateUser as jest.Mock).mockReturnValue({
mutateAsync: mockMutateAsync,
isLoading: false,
});
@@ -323,7 +267,7 @@ describe('EditMemberDrawer', () => {
});
it('does not close the drawer after a successful save', async () => {
const onClose = vi.fn();
const onClose = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer({ onClose });
@@ -345,18 +289,18 @@ describe('EditMemberDrawer', () => {
});
it('selecting a different role calls setRole with the new role name', async () => {
const onComplete = vi.fn();
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockSet = vi.fn().mockResolvedValue({});
const mockSet = jest.fn().mockResolvedValue({});
(useSetRoleByUserID as Mock).mockReturnValue({
(useSetRoleByUserID as jest.Mock).mockReturnValue({
mutateAsync: mockSet,
isLoading: false,
});
renderDrawer({ onComplete });
await screen.findByTitle('signoz-admin');
// Open the roles dropdown and select signoz-editor
await user.click(screen.getByLabelText('Roles'));
await user.click(await screen.findByTitle('signoz-editor'));
@@ -374,18 +318,18 @@ describe('EditMemberDrawer', () => {
});
it('does not call removeRole when the role is changed', async () => {
const onComplete = vi.fn();
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockSet = vi.fn().mockResolvedValue({});
const mockSet = jest.fn().mockResolvedValue({});
(useSetRoleByUserID as Mock).mockReturnValue({
(useSetRoleByUserID as jest.Mock).mockReturnValue({
mutateAsync: mockSet,
isLoading: false,
});
renderDrawer({ onComplete });
await screen.findByTitle('signoz-admin');
// Switch from signoz-admin to signoz-viewer using single-select
await user.click(screen.getByLabelText('Roles'));
await user.click(await screen.findByTitle('signoz-viewer'));
@@ -403,10 +347,10 @@ describe('EditMemberDrawer', () => {
});
it('shows delete confirm dialog and calls deleteUser for active members', async () => {
const onComplete = vi.fn();
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as Mock).mockImplementation((options) => ({
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onSuccess?.();
}),
@@ -449,7 +393,7 @@ describe('EditMemberDrawer', () => {
});
it('shows "Regenerate Invite Link" when token is expired', () => {
(useGetResetPasswordToken as Mock).mockReturnValue({
(useGetResetPasswordToken as jest.Mock).mockReturnValue({
data: {
data: {
token: 'old-tok',
@@ -469,7 +413,7 @@ describe('EditMemberDrawer', () => {
});
it('shows "Generate Invite Link" when no token exists', () => {
(useGetResetPasswordToken as Mock).mockReturnValue({
(useGetResetPasswordToken as jest.Mock).mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
@@ -483,10 +427,10 @@ describe('EditMemberDrawer', () => {
});
it('calls deleteUser after confirming revoke invite for invited members', async () => {
const onComplete = vi.fn();
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as Mock).mockImplementation((options) => ({
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onSuccess?.();
}),
@@ -513,11 +457,11 @@ describe('EditMemberDrawer', () => {
});
it('calls updateUser when saving name change for an invited member', async () => {
const onComplete = vi.fn();
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockMutateAsync = vi.fn().mockResolvedValue({});
const mockMutateAsync = jest.fn().mockResolvedValue({});
(useGetUser as Mock).mockReturnValue({
(useGetUser as jest.Mock).mockReturnValue({
data: {
data: {
...mockFetchedUser.data,
@@ -533,9 +477,9 @@ describe('EditMemberDrawer', () => {
},
},
isLoading: false,
refetch: vi.fn(),
refetch: jest.fn(),
});
(useUpdateUser as Mock).mockReturnValue({
(useUpdateUser as jest.Mock).mockReturnValue({
mutateAsync: mockMutateAsync,
isLoading: false,
});
@@ -560,7 +504,7 @@ describe('EditMemberDrawer', () => {
});
describe('error handling', () => {
const mockConvertToApiError = vi.mocked(convertToApiError);
const mockConvertToApiError = jest.mocked(convertToApiError);
beforeEach(() => {
mockConvertToApiError.mockReturnValue({
@@ -571,8 +515,8 @@ describe('EditMemberDrawer', () => {
it('shows SaveErrorItem when updateUser fails for name change', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useUpdateUser as Mock).mockReturnValue({
mutateAsync: vi.fn().mockRejectedValue(new Error('server error')),
(useUpdateUser as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockRejectedValue(new Error('server error')),
isLoading: false,
});
@@ -596,7 +540,7 @@ describe('EditMemberDrawer', () => {
it('shows API error message when deleteUser fails for active member', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as Mock).mockImplementation((options) => ({
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onError?.({});
}),
@@ -627,7 +571,7 @@ describe('EditMemberDrawer', () => {
it('shows API error message when deleteUser fails for invited member', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as Mock).mockImplementation((options) => ({
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onError?.({});
}),
@@ -690,10 +634,10 @@ describe('EditMemberDrawer', () => {
describe('root user', () => {
beforeEach(() => {
(useGetUser as Mock).mockReturnValue({
(useGetUser as jest.Mock).mockReturnValue({
data: rootMockFetchedUser,
isLoading: false,
refetch: vi.fn(),
refetch: jest.fn(),
});
});
@@ -773,7 +717,7 @@ describe('EditMemberDrawer', () => {
it('copies the link to clipboard and shows "Copied!" on the button', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockToast = vi.mocked(toast);
const mockToast = jest.mocked(toast);
renderDrawer();

View File

@@ -1,39 +1,10 @@
import { render, screen } from '@testing-library/react';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import Editor from './index';
vi.mock('hooks/useDarkMode', () => ({
useIsDarkMode: vi.fn(),
}));
vi.mock('@monaco-editor/react', () => ({
default: ({ height }: { height?: string }): JSX.Element => (
<section
style={{
display: 'flex',
position: 'relative',
textAlign: 'initial',
width: '100%',
height,
}}
>
<div
style={{
display: 'flex',
height: '100%',
width: '100%',
justifyContent: 'center',
alignItems: 'center',
}}
>
Loading...
</div>
<div style={{ width: '100%', display: 'none' }} />
</section>
),
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: jest.fn(),
}));
describe('Editor', () => {
@@ -63,7 +34,7 @@ describe('Editor', () => {
});
it('renders with dark mode theme', () => {
(useIsDarkMode as Mock).mockImplementation(() => true);
(useIsDarkMode as jest.Mock).mockImplementation(() => true);
const { container } = render(<Editor value="dark mode text" />);
@@ -71,7 +42,7 @@ describe('Editor', () => {
});
it('renders with light mode theme', () => {
(useIsDarkMode as Mock).mockImplementation(() => false);
(useIsDarkMode as jest.Mock).mockImplementation(() => false);
const { container } = render(<Editor value="light mode text" />);

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Editor > renders correctly with custom props 1`] = `
exports[`Editor renders correctly with custom props 1`] = `
<div>
<section
style="display: flex; position: relative; text-align: initial; width: 100%; height: 50vh;"
@@ -17,7 +17,7 @@ exports[`Editor > renders correctly with custom props 1`] = `
</div>
`;
exports[`Editor > renders correctly with default props 1`] = `
exports[`Editor renders correctly with default props 1`] = `
<div>
<section
style="display: flex; position: relative; text-align: initial; width: 100%; height: 40vh;"
@@ -34,7 +34,7 @@ exports[`Editor > renders correctly with default props 1`] = `
</div>
`;
exports[`Editor > renders with dark mode theme 1`] = `
exports[`Editor renders with dark mode theme 1`] = `
<div>
<section
style="display: flex; position: relative; text-align: initial; width: 100%; height: 40vh;"
@@ -51,7 +51,7 @@ exports[`Editor > renders with dark mode theme 1`] = `
</div>
`;
exports[`Editor > renders with light mode theme 1`] = `
exports[`Editor renders with light mode theme 1`] = `
<div>
<section
style="display: flex; position: relative; text-align: initial; width: 100%; height: 40vh;"

View File

@@ -1,22 +1,13 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import withErrorBoundary, {
WithErrorBoundaryOptions,
} from '../withErrorBoundary';
// Mock dependencies before imports
vi.mock('@sentry/react', async () => {
const ReactMock = await vi.importActual<typeof import('react')>('react');
jest.mock('@sentry/react', () => {
const ReactMock = jest.requireActual('react');
class MockErrorBoundary extends ReactMock.Component<
{
@@ -43,8 +34,8 @@ vi.mock('@sentry/react', async () => {
const { beforeCapture, onError } = this.props;
if (beforeCapture) {
const mockScope = {
setTag: vi.fn(),
setLevel: vi.fn(),
setTag: jest.fn(),
setLevel: jest.fn(),
};
beforeCapture(mockScope);
}
@@ -73,11 +64,15 @@ vi.mock('@sentry/react', async () => {
};
});
vi.mock('../../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback', () => ({
default: function MockErrorBoundaryFallback(): JSX.Element {
return <div data-testid="default-error-fallback">Default Error Fallback</div>;
},
}));
jest.mock(
'../../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback',
() =>
function MockErrorBoundaryFallback(): JSX.Element {
return (
<div data-testid="default-error-fallback">Default Error Fallback</div>
);
},
);
// Test component that can throw errors
interface TestComponentProps {
@@ -110,7 +105,7 @@ describe('withErrorBoundary', () => {
// Suppress console errors for cleaner test output
const originalError = console.error;
beforeAll(() => {
console.error = vi.fn();
console.error = jest.fn();
});
afterAll(() => {
@@ -118,7 +113,7 @@ describe('withErrorBoundary', () => {
});
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('should wrap component with ErrorBoundary and render successfully', () => {
@@ -167,7 +162,7 @@ describe('withErrorBoundary', () => {
it('should call custom error handler when error occurs', () => {
// Arrange
const mockErrorHandler = vi.fn();
const mockErrorHandler = jest.fn();
const options: WithErrorBoundaryOptions = {
onError: mockErrorHandler,
};

View File

@@ -1,26 +1,19 @@
import { describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import APIError from 'types/api/error';
import ErrorModal from './ErrorModal';
// Mock the query client to return version data
const mockVersionData = vi.hoisted(() => ({
const mockVersionData = {
payload: {
ee: 'Y',
version: '1.0.0',
},
}));
vi.mock('react-query', async () => ({
...(await vi.importActual('react-query')),
};
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueryClient: (): { getQueryData: () => typeof mockVersionData } => ({
getQueryData: vi.fn(() => mockVersionData),
}),
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
getQueryData: jest.fn(() => mockVersionData),
}),
}));
const mockError: APIError = new APIError({
@@ -38,7 +31,7 @@ const mockError: APIError = new APIError({
});
describe('ErrorModal Component', () => {
it('should render the modal when open is true', () => {
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the error message is displayed
expect(screen.getByText('An error occurred')).toBeInTheDocument();
@@ -48,14 +41,14 @@ describe('ErrorModal Component', () => {
});
it('should not render the modal when open is false', () => {
render(<ErrorModal error={mockError} open={false} onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open={false} onClose={jest.fn()} />);
// Check that the modal content is not in the document
expect(screen.queryByText('An error occurred')).not.toBeInTheDocument();
});
it('should call onClose when the close button is clicked', async () => {
const onCloseMock = vi.fn();
const onCloseMock = jest.fn();
render(<ErrorModal error={mockError} open onClose={onCloseMock} />);
// Click the close button
@@ -68,14 +61,14 @@ describe('ErrorModal Component', () => {
});
it('should display version data if available', async () => {
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the version data is displayed
expect(screen.getByText('ENTERPRISE')).toBeInTheDocument();
expect(screen.getByText('1.0.0')).toBeInTheDocument();
});
it('should render the messages count badge when there are multiple errors', () => {
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the messages count badge is displayed
expect(screen.getByText('MESSAGES')).toBeInTheDocument();
@@ -89,7 +82,7 @@ describe('ErrorModal Component', () => {
});
it('should render the open docs button when URL is provided', async () => {
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the open docs button is displayed
const openDocsButton = screen.getByTestId('error-docs-button');
@@ -102,7 +95,7 @@ describe('ErrorModal Component', () => {
});
it('should not display scroll for more if there are less than 10 messages', () => {
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
expect(screen.queryByText('Scroll for more')).not.toBeInTheDocument();
});
@@ -120,7 +113,7 @@ describe('ErrorModal Component', () => {
},
});
render(<ErrorModal error={longError} open onClose={vi.fn()} />);
render(<ErrorModal error={longError} open onClose={jest.fn()} />);
// Check if the scroll hint is displayed
expect(screen.getByText('Scroll for more')).toBeInTheDocument();
@@ -132,7 +125,7 @@ it('should render the trigger component if provided', () => {
<ErrorModal
error={mockError}
triggerComponent={mockTrigger}
onClose={vi.fn()}
onClose={jest.fn()}
/>,
);
@@ -146,7 +139,7 @@ it('should open the modal when the trigger component is clicked', async () => {
<ErrorModal
error={mockError}
triggerComponent={mockTrigger}
onClose={vi.fn()}
onClose={jest.fn()}
/>,
);
@@ -160,14 +153,14 @@ it('should open the modal when the trigger component is clicked', async () => {
});
it('should render the default trigger tag if no trigger component is provided', () => {
render(<ErrorModal error={mockError} onClose={vi.fn()} />);
render(<ErrorModal error={mockError} onClose={jest.fn()} />);
// Check if the default trigger tag is rendered
expect(screen.getByText('error')).toBeInTheDocument();
});
it('should close the modal when the onCancel event is triggered', async () => {
const onCloseMock = vi.fn();
const onCloseMock = jest.fn();
render(<ErrorModal error={mockError} onClose={onCloseMock} />);
// Click the trigger component
@@ -186,7 +179,9 @@ it('should close the modal when the onCancel event is triggered', async () => {
expect(onCloseMock).toHaveBeenCalledTimes(1);
await waitFor(() => {
// check if the modal is not visible
const modal = document.getElementsByClassName('ant-modal');
expect(modal[0]).toHaveClass('ant-zoom-leave');
const style = window.getComputedStyle(modal[0]);
expect(style.display).toBe('none');
});
});

View File

@@ -3,63 +3,57 @@ import ROUTES from 'constants/routes';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
import { describe, expect, it, vi } from 'vitest';
import { viewMockData } from '../__mock__/viewData';
import ExplorerCard from '../ExplorerCard';
const historyReplace = vi.hoisted(() => vi.fn());
const historyReplace = jest.fn();
vi.mock('react-router-dom', async () => {
const actual =
await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
return {
...actual,
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
useHistory: (): any => ({
...actual.useHistory(),
replace: historyReplace,
}),
};
});
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: vi.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
useHistory: (): any => ({
...jest.requireActual('react-router-dom').useHistory(),
replace: historyReplace,
}),
}));
vi.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: vi.fn(() => 'mockedPanelType'),
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
vi.mock('hooks/saveViews/useGetAllViews', () => ({
useGetAllViews: vi.fn(() => ({
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useGetAllViews', () => ({
useGetAllViews: jest.fn(() => ({
data: { data: { data: viewMockData } },
isLoading: false,
error: null,
isRefetching: false,
refetch: vi.fn(),
refetch: jest.fn(),
})),
}));
vi.mock('hooks/saveViews/useUpdateView', () => ({
useUpdateView: vi.fn(() => ({
mutateAsync: vi.fn(),
jest.mock('hooks/saveViews/useUpdateView', () => ({
useUpdateView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
vi.mock('hooks/saveViews/useDeleteView', () => ({
useDeleteView: vi.fn(() => ({
mutateAsync: vi.fn(),
jest.mock('hooks/saveViews/useDeleteView', () => ({
useDeleteView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
// Mock usePreferenceSync
vi.mock('providers/preferences/sync/usePreferenceSync', () => ({
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
@@ -72,8 +66,8 @@ vi.mock('providers/preferences/sync/usePreferenceSync', () => ({
},
loading: false,
error: null,
updateColumns: vi.fn(),
updateFormatting: vi.fn(),
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));

View File

@@ -2,22 +2,21 @@ import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
import { describe, expect, it, vi } from 'vitest';
import { viewMockData } from '../__mock__/viewData';
import MenuItemGenerator from '../MenuItemGenerator';
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
vi.mock('antd', async () => ({
...(await vi.importActual('antd')),
useForm: vi.fn().mockReturnValue({
onFinish: vi.fn(),
jest.mock('antd', () => ({
...jest.requireActual('antd'),
useForm: jest.fn().mockReturnValue({
onFinish: jest.fn(),
}),
}));
@@ -30,7 +29,7 @@ describe('MenuItemGenerator', () => {
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={vi.fn()}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>
@@ -48,7 +47,7 @@ describe('MenuItemGenerator', () => {
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={vi.fn()}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>

View File

@@ -1,16 +1,14 @@
import { describe, expect, it, vi } from 'vitest';
import { QueryClient, QueryClientProvider } from 'react-query';
import { fireEvent, render } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
import SaveViewWithName from '../SaveViewWithName';
vi.mock('react-router-dom', async () => ({
...(await vi.importActual<typeof import('react-router-dom')>(
'react-router-dom',
)),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/services/`,
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
@@ -22,13 +20,13 @@ const queryClient = new QueryClient({
},
});
vi.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: vi.fn(() => 'mockedPanelType'),
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
vi.mock('hooks/saveViews/useSaveView', () => ({
useSaveView: vi.fn(() => ({
mutateAsync: vi.fn(),
jest.mock('hooks/saveViews/useSaveView', () => ({
useSaveView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
@@ -38,8 +36,8 @@ describe('SaveViewWithName', () => {
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={vi.fn()}
refetchAllView={vi.fn()}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);
@@ -52,8 +50,8 @@ describe('SaveViewWithName', () => {
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={vi.fn()}
refetchAllView={vi.fn()}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);

View File

@@ -7,7 +7,6 @@ import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore';
import { createCustomTimeRange } from 'store/globalTime/utils';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { beforeEach, describe, expect, it } from 'vitest';
import { GlobalTimeStoreAdapter } from '../GlobalTimeStoreAdapter';

View File

@@ -1,5 +1,3 @@
import { describe, expect, it } from 'vitest';
import dayjs from 'dayjs';
import { convertTimeRange, TIME_UNITS } from '../xAxisConfig';

View File

@@ -1,5 +1,3 @@
import { describe, expect, it } from 'vitest';
import { PrecisionOptionsEnum } from '../types';
import { getYAxisFormattedValue } from '../yAxisConfig';

View File

@@ -1,5 +1,4 @@
import { ReactElement } from 'react';
import { describe, expect, it, vi } from 'vitest';
import {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
@@ -13,12 +12,6 @@ import { render, screen, waitFor } from 'tests/test-utils';
import { GuardAuthZ } from './GuardAuthZ';
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: () => void } => ({
safeNavigate: (): void => {},
}),
}));
const BASE_URL = ENVIRONMENT.baseURL || '';
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
@@ -62,7 +55,7 @@ describe('GuardAuthZ', () => {
);
render(
<GuardAuthZ relation="read" object="dashboard:*">
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
@@ -86,7 +79,7 @@ describe('GuardAuthZ', () => {
render(
<GuardAuthZ
relation="read"
object="dashboard:*"
object="role:*"
fallbackOnLoading={<LoadingFallback />}
>
<TestChild />
@@ -109,7 +102,7 @@ describe('GuardAuthZ', () => {
);
const { container } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
@@ -128,11 +121,7 @@ describe('GuardAuthZ', () => {
);
render(
<GuardAuthZ
relation="read"
object="dashboard:*"
fallbackOnError={ErrorFallback}
>
<GuardAuthZ relation="read" object="role:*" fallbackOnError={ErrorFallback}>
<TestChild />
</GuardAuthZ>,
);
@@ -162,7 +151,7 @@ describe('GuardAuthZ', () => {
render(
<GuardAuthZ
relation="read"
object="dashboard:*"
object="role:*"
fallbackOnError={errorFallbackWithCapture}
>
<TestChild />
@@ -185,7 +174,7 @@ describe('GuardAuthZ', () => {
);
const { container } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
@@ -208,7 +197,7 @@ describe('GuardAuthZ', () => {
render(
<GuardAuthZ
relation="update"
object="dashboard:123"
object="role:123"
fallbackOnNoPermissions={NoPermissionFallback}
>
<TestChild />
@@ -231,7 +220,7 @@ describe('GuardAuthZ', () => {
);
const { container } = render(
<GuardAuthZ relation="update" object="dashboard:123">
<GuardAuthZ relation="update" object="role:123">
<TestChild />
</GuardAuthZ>,
);
@@ -251,7 +240,7 @@ describe('GuardAuthZ', () => {
);
const { container } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
@@ -264,7 +253,7 @@ describe('GuardAuthZ', () => {
});
it('should pass requiredPermissionName to fallbackOnNoPermissions', async () => {
const permission = buildPermission('update', 'dashboard:123');
const permission = buildPermission('update', 'role:123');
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
@@ -276,7 +265,7 @@ describe('GuardAuthZ', () => {
render(
<GuardAuthZ
relation="update"
object="dashboard:123"
object="role:123"
fallbackOnNoPermissions={NoPermissionFallbackWithSuggestions}
>
<TestChild />
@@ -306,7 +295,7 @@ describe('GuardAuthZ', () => {
);
const { rerender } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
@@ -316,7 +305,7 @@ describe('GuardAuthZ', () => {
});
rerender(
<GuardAuthZ relation="delete" object="dashboard:456">
<GuardAuthZ relation="delete" object="role:456">
<TestChild />
</GuardAuthZ>,
);

View File

@@ -1,4 +1,3 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import AnnouncementsModal from '../AnnouncementsModal';

View File

@@ -1,6 +1,4 @@
// Mock dependencies before imports
import { describe, expect, beforeEach, it, vi } from 'vitest';
import type { Mock, Mocked, MockedFunction } from 'vitest';
import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui';
import { render, screen } from '@testing-library/react';
@@ -11,38 +9,39 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import FeedbackModal from '../FeedbackModal';
vi.mock('api/common/logEvent', () => ({
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: vi.fn(() => Promise.resolve()),
default: jest.fn(() => Promise.resolve()),
}));
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
vi.mock('@signozhq/ui', () => ({
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: {
success: vi.fn(),
error: vi.fn(),
success: jest.fn(),
error: jest.fn(),
},
}));
vi.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: vi.fn(),
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: jest.fn(),
}));
vi.mock('container/Integrations/utils', () => ({
handleContactSupport: vi.fn(),
jest.mock('container/Integrations/utils', () => ({
handleContactSupport: jest.fn(),
}));
const mockLogEvent = logEvent as MockedFunction<typeof logEvent>;
const mockUseLocation = useLocation as Mock;
const mockUseGetTenantLicense = useGetTenantLicense as Mock;
const mockHandleContactSupport = handleContactSupport as Mock;
const mockToast = toast as Mocked<typeof toast>;
const mockLogEvent = logEvent as jest.MockedFunction<typeof logEvent>;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const mockHandleContactSupport = handleContactSupport as jest.Mock;
const mockToast = toast as jest.Mocked<typeof toast>;
const mockOnClose = vi.fn();
const mockOnClose = jest.fn();
const mockLocation = {
pathname: '/test-path',
@@ -50,7 +49,7 @@ const mockLocation = {
describe('FeedbackModal', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
mockUseLocation.mockReturnValue(mockLocation);
mockUseGetTenantLicense.mockReturnValue({
isCloudUser: false,

View File

@@ -1,47 +1,23 @@
// Mock dependencies before imports
import type { ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import { useLocation } from 'react-router-dom';
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import HeaderRightSection from '../HeaderRightSection';
vi.mock('api/common/logEvent', () => ({
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
vi.mock('antd', async () => {
const actual = await vi.importActual<typeof import('antd')>('antd');
return {
...actual,
Popover: ({
children,
content,
open,
}: {
children: ReactNode;
content: ReactNode;
open?: boolean;
}): JSX.Element => (
<>
{children}
{open ? content : null}
</>
),
};
});
vi.mock('../FeedbackModal', () => ({
jest.mock('../FeedbackModal', () => ({
__esModule: true,
default: ({ onClose }: { onClose: () => void }): JSX.Element => (
<div data-testid="feedback-modal">
@@ -52,27 +28,27 @@ vi.mock('../FeedbackModal', () => ({
),
}));
vi.mock('../ShareURLModal', () => ({
jest.mock('../ShareURLModal', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="share-modal">Share URL Modal</div>
),
}));
vi.mock('../AnnouncementsModal', () => ({
jest.mock('../AnnouncementsModal', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="announcements-modal">Announcements Modal</div>
),
}));
vi.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: vi.fn(),
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: jest.fn(),
}));
const mockLogEvent = logEvent as Mock;
const mockUseLocation = useLocation as Mock;
const mockUseGetTenantLicense = useGetTenantLicense as Mock;
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const defaultProps = {
enableAnnouncements: true,
@@ -86,7 +62,7 @@ const mockLocation = {
describe('HeaderRightSection', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
mockUseLocation.mockReturnValue(mockLocation);
// Default to licensed user (Enterprise or Cloud)
mockUseGetTenantLicense.mockReturnValue({
@@ -201,9 +177,7 @@ describe('HeaderRightSection', () => {
// Close feedback modal
const closeFeedbackButton = screen.getByText('Close Feedback');
await user.click(closeFeedbackButton);
await waitFor(() => {
expect(screen.queryByTestId('feedback-modal')).not.toBeInTheDocument();
});
expect(screen.queryByTestId('feedback-modal')).not.toBeInTheDocument();
});
it('should close other modals when opening feedback modal', async () => {
@@ -223,9 +197,7 @@ describe('HeaderRightSection', () => {
await user.click(feedbackButton!);
expect(screen.getByTestId('feedback-modal')).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByTestId('share-modal')).not.toBeInTheDocument();
});
expect(screen.queryByTestId('share-modal')).not.toBeInTheDocument();
});
it('should show feedback button for Cloud users when feedback is enabled', () => {

View File

@@ -1,6 +1,6 @@
// Mock dependencies before imports
import { describe, expect, it, beforeEach, vi } from 'vitest';
import type { Mock } from 'vitest';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { render, screen } from '@testing-library/react';
@@ -12,39 +12,35 @@ import GetMinMax from 'lib/getMinMax';
import ShareURLModal from '../ShareURLModal';
const hoistedReduxMocks = vi.hoisted(() => ({
useSelectorMock: vi.fn(),
}));
vi.mock('api/common/logEvent', () => ({
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
matchPath: vi.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
matchPath: jest.fn(),
}));
vi.mock('hooks/useUrlQuery', () => ({
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
vi.mock('react-redux', async () => ({
...(await vi.importActual<typeof import('react-redux')>('react-redux')),
useSelector: hoistedReduxMocks.useSelectorMock,
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}));
vi.mock('lib/getMinMax', () => ({
jest.mock('lib/getMinMax', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
vi.mock('react-use', async () => ({
...(await vi.importActual('react-use')),
useCopyToClipboard: vi.fn(),
jest.mock('react-use', () => ({
...jest.requireActual('react-use'),
useCopyToClipboard: jest.fn(),
}));
// Mock window.location
@@ -57,29 +53,29 @@ Object.defineProperty(window, 'location', {
writable: true,
});
const mockLogEvent = logEvent as Mock;
const mockUseLocation = useLocation as Mock;
const mockUseUrlQuery = useUrlQuery as Mock;
const mockUseSelector = hoistedReduxMocks.useSelectorMock as Mock;
const mockGetMinMax = GetMinMax as Mock;
const mockUseCopyToClipboard = useCopyToClipboard as Mock;
const mockMatchPath = matchPath as Mock;
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseUrlQuery = useUrlQuery as jest.Mock;
const mockUseSelector = useSelector as jest.Mock;
const mockGetMinMax = GetMinMax as jest.Mock;
const mockUseCopyToClipboard = useCopyToClipboard as jest.Mock;
const mockMatchPath = matchPath as jest.Mock;
const mockUrlQuery = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
toString: vi.fn(() => 'param=value'),
get: jest.fn(),
set: jest.fn(),
delete: jest.fn(),
toString: jest.fn(() => 'param=value'),
};
const mockHandleCopyToClipboard = vi.fn();
const mockHandleCopyToClipboard = jest.fn();
const TEST_PATH = '/test-path';
const ENABLE_ABSOLUTE_TIME_TEXT = 'Enable absolute time';
describe('ShareURLModal', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
mockUseLocation.mockReturnValue({
pathname: TEST_PATH,

View File

@@ -1,6 +1,3 @@
import type { ChangeEventHandler, ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import inviteUsers from 'api/v1/invite/bulk/create';
import sendInvite from 'api/v1/invite/create';
import { StatusCodes } from 'http-status-codes';
@@ -15,114 +12,38 @@ const makeApiError = (message: string, code = StatusCodes.CONFLICT): APIError =>
error: { code: 'already_exists', message, url: '', errors: [] },
});
type MockButtonProps = {
children?: ReactNode;
disabled?: boolean;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
'aria-label'?: string;
};
type MockInputProps = {
autoComplete?: string;
className?: string;
name?: string;
onChange?: ChangeEventHandler<HTMLInputElement>;
placeholder?: string;
type?: string;
value?: string;
};
type MockDialogProps = {
children?: ReactNode;
className?: string;
open?: boolean;
};
const showErrorModal = vi.hoisted(() => vi.fn());
vi.mock('api/v1/invite/create');
vi.mock('api/v1/invite/bulk/create');
vi.mock('@signozhq/ui', async () => {
const React = await vi.importActual<typeof import('react')>('react');
return {
Button: ({
children,
disabled,
onClick,
type = 'button',
'aria-label': ariaLabel,
}: MockButtonProps): JSX.Element =>
React.createElement(
'button',
{ 'aria-label': ariaLabel, disabled, onClick, type },
children,
),
Callout: ({ title }: { title?: ReactNode }): JSX.Element =>
React.createElement('div', null, title),
DialogFooter: ({ children, className }: MockDialogProps): JSX.Element =>
React.createElement('div', { className }, children),
DialogWrapper: ({
children,
className,
open,
}: MockDialogProps): JSX.Element | null =>
open ? React.createElement('div', { className }, children) : null,
Input: ({
autoComplete,
className,
name,
onChange,
placeholder,
type,
value,
}: MockInputProps): JSX.Element =>
React.createElement('input', {
autoComplete,
className,
name,
onChange,
placeholder,
type,
value,
}),
toast: {
success: vi.fn(),
error: vi.fn(),
},
};
});
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
jest.mock('api/v1/invite/create');
jest.mock('api/v1/invite/bulk/create');
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));
vi.mock('providers/ErrorModalProvider', async () => ({
const showErrorModal = jest.fn();
jest.mock('providers/ErrorModalProvider', () => ({
__esModule: true,
...(await vi.importActual<typeof import('providers/ErrorModalProvider')>(
'providers/ErrorModalProvider',
)),
useErrorModal: vi.fn(() => ({
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal,
isErrorModalVisible: false,
})),
}));
const mockSendInvite = vi.mocked(sendInvite);
const mockInviteUsers = vi.mocked(inviteUsers);
const mockSendInvite = jest.mocked(sendInvite);
const mockInviteUsers = jest.mocked(inviteUsers);
const defaultProps = {
open: true,
onClose: vi.fn(),
onComplete: vi.fn(),
onClose: jest.fn(),
onComplete: jest.fn(),
};
describe('InviteMembersModal', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
showErrorModal.mockClear();
mockSendInvite.mockResolvedValue({
httpStatusCode: 200,
@@ -217,7 +138,7 @@ describe('InviteMembersModal', () => {
it('uses sendInvite (single) when only one row is filled', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onComplete = vi.fn();
const onComplete = jest.fn();
render(<InviteMembersModal {...defaultProps} onComplete={onComplete} />);
@@ -322,7 +243,7 @@ describe('InviteMembersModal', () => {
it('uses inviteUsers (bulk) when multiple rows are filled', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onComplete = vi.fn();
const onComplete = jest.fn();
render(<InviteMembersModal {...defaultProps} onComplete={onComplete} />);

View File

@@ -1,5 +1,4 @@
import { ComponentType, lazy as reactLazy, Suspense } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import React, { ComponentType, Suspense } from 'react';
import {
render,
screen,
@@ -8,16 +7,6 @@ import {
import Loadable from './index';
vi.mock('react', async (importOriginal) => {
const actual = await importOriginal<typeof import('react')>();
const lazy = vi.fn(actual.lazy);
return {
...actual,
lazy,
};
});
// Sample component to be loaded lazily
function SampleComponent(): JSX.Element {
return <div>Sample Component</div>;
@@ -33,10 +22,6 @@ const loadSampleComponent = (): Promise<{
});
describe('Loadable', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('should render the lazily loaded component', async () => {
const LoadableSampleComponent = Loadable(loadSampleComponent);
@@ -53,9 +38,12 @@ describe('Loadable', () => {
});
it('should call lazy with the provided import path', () => {
const reactLazySpy = jest.spyOn(React, 'lazy');
Loadable(loadSampleComponent);
expect(vi.mocked(reactLazy)).toHaveBeenCalledTimes(1);
expect(vi.mocked(reactLazy)).toHaveBeenCalledWith(expect.any(Function));
expect(reactLazySpy).toHaveBeenCalledTimes(1);
expect(reactLazySpy).toHaveBeenCalledWith(expect.any(Function));
reactLazySpy.mockRestore();
});
});

View File

@@ -17,7 +17,6 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import {
aggregateAttributesResourcesToString,
@@ -47,6 +46,7 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { JsonView } from 'periscope/components/JsonView';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@@ -562,7 +562,9 @@ function LogDetailInner({
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.JSON && (
<JsonView data={LogJsonData} height="68vh" />
)}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView

View File

@@ -1,6 +1,5 @@
import { render } from '@testing-library/react';
import { FontSize } from 'container/OptionsMenu/types';
import { describe, expect, it } from 'vitest';
import LogStateIndicator from './LogStateIndicator';

View File

@@ -1,5 +1,3 @@
import { describe, expect, it } from 'vitest';
import { ILog } from 'types/api/logs/log';
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';

View File

@@ -1,13 +1,11 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FontSize } from 'container/OptionsMenu/types';
import { fireEvent, render, waitFor } from 'tests/test-utils';
import LogsFormatOptionsMenu from '../LogsFormatOptionsMenu';
const mockUpdateFormatting = vi.hoisted(() => vi.fn());
const mockUpdateFormatting = jest.fn();
vi.mock('providers/preferences/sync/usePreferenceSync', () => ({
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
@@ -20,17 +18,11 @@ vi.mock('providers/preferences/sync/usePreferenceSync', () => ({
},
loading: false,
error: null,
updateColumns: vi.fn(),
updateColumns: jest.fn(),
updateFormatting: mockUpdateFormatting,
}),
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
describe('LogsFormatOptionsMenu (unit)', () => {
beforeEach(() => {
mockUpdateFormatting.mockClear();
@@ -39,9 +31,9 @@ describe('LogsFormatOptionsMenu (unit)', () => {
function setup(): {
getByTestId: ReturnType<typeof render>['getByTestId'];
findItemByLabel: (label: string) => Element | undefined;
formatOnChange: ReturnType<typeof vi.fn>;
maxLinesOnChange: ReturnType<typeof vi.fn>;
fontSizeOnChange: ReturnType<typeof vi.fn>;
formatOnChange: jest.Mock<any, any>;
maxLinesOnChange: jest.Mock<any, any>;
fontSizeOnChange: jest.Mock<any, any>;
} {
const items = [
{ key: 'raw', label: 'Raw', data: { title: 'max lines per row' } },
@@ -49,9 +41,9 @@ describe('LogsFormatOptionsMenu (unit)', () => {
{ key: 'table', label: 'Column', data: { title: 'columns' } },
];
const formatOnChange = vi.fn();
const maxLinesOnChange = vi.fn();
const fontSizeOnChange = vi.fn();
const formatOnChange = jest.fn();
const maxLinesOnChange = jest.fn();
const fontSizeOnChange = jest.fn();
const { getByTestId } = render(
<LogsFormatOptionsMenu
@@ -65,11 +57,11 @@ describe('LogsFormatOptionsMenu (unit)', () => {
isFetching: false,
value: [],
options: [],
onFocus: vi.fn(),
onBlur: vi.fn(),
onSearch: vi.fn(),
onSelect: vi.fn(),
onRemove: vi.fn(),
onFocus: jest.fn(),
onBlur: jest.fn(),
onSearch: jest.fn(),
onSelect: jest.fn(),
onRemove: jest.fn(),
},
}}
/>,

View File

@@ -1,27 +1,8 @@
import type { ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { MockedFunction } from 'vitest';
import { MemberStatus } from 'container/MembersSettings/utils';
import { render, screen, userEvent } from 'tests/test-utils';
import MembersTable, { MemberRow } from '../MembersTable';
vi.mock('@signozhq/ui', async () => {
const React = await vi.importActual<typeof import('react')>('react');
return {
Badge: ({ children }: { children?: ReactNode }): JSX.Element =>
React.createElement('span', null, children),
};
});
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
const mockActiveMembers: MemberRow[] = [
{
id: 'user-1',
@@ -53,13 +34,13 @@ const defaultProps = {
currentPage: 1,
pageSize: 20,
searchQuery: '',
onPageChange: vi.fn(),
onRowClick: vi.fn(),
onPageChange: jest.fn(),
onRowClick: jest.fn(),
};
describe('MembersTable', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('renders member rows with name, email, and ACTIVE status', () => {
@@ -84,7 +65,9 @@ describe('MembersTable', () => {
});
it('calls onRowClick with the member data when a row is clicked', async () => {
const onRowClick = vi.fn() as MockedFunction<(member: MemberRow) => void>;
const onRowClick = jest.fn() as jest.MockedFunction<
(member: MemberRow) => void
>;
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
@@ -104,7 +87,7 @@ describe('MembersTable', () => {
});
it('renders DELETED badge and calls onRowClick when a deleted member row is clicked', async () => {
const onRowClick = vi.fn();
const onRowClick = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const deletedMember: MemberRow = {
id: 'user-del',

View File

@@ -1,4 +1,3 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import MessageTip from './index';

View File

@@ -1,8 +1,15 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`MessageTip custom action 1`] = `
.c0 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
exports[`MessageTip > custom action 1`] = `
<div
class="ant-alert ant-alert-info ant-alert-with-description sc-aXZVg bzzGSj css-dev-only-do-not-override-2i2tap"
class="ant-alert ant-alert-info ant-alert-with-description c0 css-dev-only-do-not-override-2i2tap"
data-show="true"
role="alert"
>

View File

@@ -1,14 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { VirtuosoMockContext } from 'react-virtuoso';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CustomMultiSelect from '../CustomMultiSelect';
import type { CustomMultiSelectProps } from '../types';
import type { MockedFunction } from 'vitest';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = vi.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (
@@ -20,18 +17,10 @@ const renderWithVirtuoso = (
</VirtuosoMockContext.Provider>,
);
const expectDropdownToBeClosingOrHidden = (dropdown: Element | null): void => {
expect(dropdown).toBeInTheDocument();
expect(dropdown?.className).toMatch(
/ant-select-dropdown-hidden|ant-slide-up-leave/,
);
};
// Mock clipboard API
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: vi.fn(() => Promise.resolve()),
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve()),
},
});
@@ -62,18 +51,12 @@ const mockGroupedOptions = [
describe('CustomMultiSelect - Comprehensive Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnChange: MockedFunction<
NonNullable<CustomMultiSelectProps['onChange']>
>;
let mockOnChange: jest.Mock;
beforeEach(() => {
user = userEvent.setup();
mockOnChange = vi.fn();
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
mockOnChange = jest.fn();
jest.clearAllMocks();
});
// ===== 1. CUSTOM VALUES SUPPORT =====
@@ -822,7 +805,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 7. SAVE AND SELECTION TRIGGERS =====
describe('Save and Selection Triggers (ST)', () => {
it('ST-01: ESC triggers save action', async () => {
const mockDropdownChange = vi.fn();
const mockDropdownChange = jest.fn();
renderWithVirtuoso(
<CustomMultiSelect
@@ -849,7 +832,8 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Dropdown should be hidden (not completely removed from DOM)
const dropdown = document.querySelector('.ant-select-dropdown');
expectDropdownToBeClosingOrHidden(dropdown);
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(dropdown).toHaveStyle('pointer-events: none');
});
});
@@ -940,7 +924,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// Dropdown should close and search text should be cleared
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expectDropdownToBeClosingOrHidden(dropdown);
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(searchInput).toHaveValue('');
});
});
@@ -1173,7 +1157,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
// The dropdown should be hidden with the hidden class
expectDropdownToBeClosingOrHidden(dropdown);
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
});
@@ -1284,7 +1268,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 11. ADVANCED CLEAR ACTIONS =====
describe('Advanced Clear Actions (ACA)', () => {
it('ACA-01: Clear action waiting behavior', async () => {
const mockOnChangeWithDelay = vi.fn().mockImplementation(
const mockOnChangeWithDelay = jest.fn().mockImplementation(
() =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), 100);
@@ -1507,7 +1491,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expectDropdownToBeClosingOrHidden(dropdown);
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
});

View File

@@ -1,4 +1,3 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { VirtuosoMockContext } from 'react-virtuoso';
import {
fireEvent,
@@ -11,7 +10,7 @@ import {
import CustomMultiSelect from '../CustomMultiSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = vi.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
@@ -35,11 +34,11 @@ const RETRY_BUTTON_SELECTOR = '.navigation-icons .anticon-reload';
describe('CustomMultiSelect - Retry Functionality', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('should show retry button when 5xx error occurs and error message is displayed', async () => {
const mockOnRetry = vi.fn();
const mockOnRetry = jest.fn();
const errorMessage = 'Internal Server Error (500)';
renderWithVirtuoso(
@@ -67,7 +66,7 @@ describe('CustomMultiSelect - Retry Functionality', () => {
});
it('should show retry button when 4xx error occurs and error message is displayed (current behavior)', async () => {
const mockOnRetry = vi.fn();
const mockOnRetry = jest.fn();
const errorMessage = 'Bad Request (400)';
renderWithVirtuoso(
@@ -94,7 +93,7 @@ describe('CustomMultiSelect - Retry Functionality', () => {
});
it('should call onRetry function when retry button is clicked', async () => {
const mockOnRetry = vi.fn();
const mockOnRetry = jest.fn();
const errorMessage = 'Internal Server Error (500)';
renderWithVirtuoso(

View File

@@ -1,4 +1,3 @@
import { describe, expect, it, vi } from 'vitest';
import { VirtuosoMockContext } from 'react-virtuoso';
import {
fireEvent,
@@ -12,7 +11,7 @@ import userEvent from '@testing-library/user-event';
import CustomMultiSelect from '../CustomMultiSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = vi.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
@@ -50,7 +49,7 @@ const mockGroupedOptions = [
describe('CustomMultiSelect Component', () => {
it('renders with placeholder', () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
renderWithVirtuoso(
<CustomMultiSelect
placeholder="Select multiple options"
@@ -65,7 +64,7 @@ describe('CustomMultiSelect Component', () => {
});
it('opens dropdown when clicked', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={handleChange} />,
);
@@ -84,7 +83,7 @@ describe('CustomMultiSelect Component', () => {
});
it('selects multiple options', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
// Start with option1 already selected
renderWithVirtuoso(
@@ -113,7 +112,7 @@ describe('CustomMultiSelect Component', () => {
});
it('selects ALL options when ALL is clicked', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -157,7 +156,7 @@ describe('CustomMultiSelect Component', () => {
});
it('removes a tag when clicked', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}

View File

@@ -1,18 +1,15 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CustomSelect from '../CustomSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = vi.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Mock clipboard API
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: vi.fn(() => Promise.resolve()),
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve()),
},
});
@@ -43,12 +40,12 @@ const mockGroupedOptions = [
describe('CustomSelect - Comprehensive Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnChange: Mock;
let mockOnChange: jest.Mock;
beforeEach(() => {
user = userEvent.setup();
mockOnChange = vi.fn();
vi.clearAllMocks();
mockOnChange = jest.fn();
jest.clearAllMocks();
});
// ===== 1. CUSTOM VALUES SUPPORT =====
@@ -682,7 +679,8 @@ describe('CustomSelect - Comprehensive Tests', () => {
// Dropdown should close
await waitFor(() => {
expect(combobox).toHaveAttribute('aria-expanded', 'false');
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
@@ -833,7 +831,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 13. ADVANCED CLEAR ACTIONS =====
describe('Advanced Clear Actions (ACA)', () => {
it('ACA-01: Clear action waiting behavior', async () => {
const mockOnChangeWithDelay = vi.fn().mockImplementation(
const mockOnChangeWithDelay = jest.fn().mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(resolve, 100);
@@ -1078,7 +1076,8 @@ describe('CustomSelect - Comprehensive Tests', () => {
// Dropdown should close after selection in single select
await waitFor(() => {
expect(combobox).toHaveAttribute('aria-expanded', 'false');
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
});

View File

@@ -1,10 +1,9 @@
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import CustomSelect from '../CustomSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = vi.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Mock options data
const mockOptions = [
@@ -32,7 +31,7 @@ const mockGroupedOptions = [
describe('CustomSelect Component', () => {
it('renders with placeholder and options', () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
render(
<CustomSelect
placeholder="Test placeholder"
@@ -47,7 +46,7 @@ describe('CustomSelect Component', () => {
});
it('opens dropdown when clicked', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
// Click to open the dropdown
@@ -63,7 +62,7 @@ describe('CustomSelect Component', () => {
});
it('calls onChange when option is selected', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
// Open dropdown
@@ -115,7 +114,7 @@ describe('CustomSelect Component', () => {
});
it('renders grouped options correctly', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
render(<CustomSelect options={mockGroupedOptions} onChange={handleChange} />);
// Open dropdown
@@ -169,7 +168,7 @@ describe('CustomSelect Component', () => {
});
it('supports keyboard navigation', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
// Open dropdown using keyboard
@@ -186,7 +185,7 @@ describe('CustomSelect Component', () => {
});
it('handles selection via keyboard', async () => {
const handleChange = vi.fn();
const handleChange = jest.fn();
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
// Open dropdown

View File

@@ -1,4 +1,3 @@
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
@@ -11,9 +10,9 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import VariableItem from '../../../container/DashboardContainer/DashboardVariablesSelection/VariableItem';
// Mock the dashboard variables query
vi.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
__esModule: true,
default: vi.fn(() =>
default: jest.fn(() =>
Promise.resolve({
payload: {
variableValues: ['option1', 'option2', 'option3', 'option4'],
@@ -23,7 +22,7 @@ vi.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
}));
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = vi.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Constants
const TEST_VARIABLE_NAME = 'test_variable';
@@ -77,12 +76,12 @@ function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element {
describe('VariableItem Integration Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnValueUpdate: Mock;
let mockOnValueUpdate: jest.Mock;
beforeEach(() => {
user = userEvent.setup();
mockOnValueUpdate = vi.fn();
vi.clearAllMocks();
mockOnValueUpdate = jest.fn();
jest.clearAllMocks();
});
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
@@ -409,7 +408,8 @@ describe('VariableItem Integration Tests', () => {
// Dropdown should close and search text should be cleared
await waitFor(() => {
expect(combobox).toHaveAttribute('aria-expanded', 'false');
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(searchInput).toHaveValue('');
});
});
@@ -577,7 +577,8 @@ describe('VariableItem Integration Tests', () => {
await user.keyboard('{Escape}');
await waitFor(() => {
expect(combobox).toHaveAttribute('aria-expanded', 'false');
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
});

View File

@@ -1,11 +1,20 @@
import { describe, expect, it } from 'vitest';
import { render } from 'tests/test-utils';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
import store from 'store';
import NotFound from './index';
describe('Not Found page test', () => {
it('should render Not Found page without errors', () => {
const { asFragment } = render(<NotFound />);
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<NotFound />
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -1,31 +1,125 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Not Found page test > should render Not Found page without errors 1`] = `
exports[`Not Found page test should render Not Found page without errors 1`] = `
<DocumentFragment>
<div
class="sc-gEvEer jnIQEo"
.c3 {
border: 2px solid #2f80ed;
box-sizing: border-box;
border-radius: 10px;
width: 400px;
background: inherit;
font-style: normal;
font-weight: normal;
font-size: 24px;
line-height: 20px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
padding-top: 14px;
padding-bottom: 14px;
color: #2f80ed;
}
.c0 {
min-height: 80vh;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c2 {
font-style: normal;
font-weight: 300;
font-size: 18px;
line-height: 20px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
color: #828282;
text-align: center;
margin: 0;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c1 {
min-height: 50px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
margin-bottom: 30px;
margin-top: 20px;
}
<div
class="c0"
>
<img
alt="not-found"
src="/src/assets/Images/notFound404.png"
src="test-file-stub"
style="max-height: 480px; max-width: 480px;"
/>
<div
class="sc-fqkvVR dmgRTJ"
class="c1"
>
<p
class="sc-eqUAAy keriGu"
class="c2"
>
Ah, seems like we reached a dead end!
</p>
<p
class="sc-eqUAAy keriGu"
class="c2"
>
Page Not Found
</p>
</div>
<a
class="sc-aXZVg hSWmhs"
class="c3"
href="/home"
tabindex="0"
>

View File

@@ -1,15 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import OverflowInputToolTip from './OverflowInputToolTip';
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
const TOOLTIP_INNER_SELECTOR = '.ant-tooltip-inner';
// Utility to mock overflow behaviour on inputs / elements.
// Stubs HTMLElement.prototype.clientWidth, scrollWidth and offsetWidth used by component.
@@ -49,7 +41,7 @@ function queryTooltipInner(): HTMLElement | null {
describe('OverflowInputToolTip', () => {
beforeEach(() => {
vi.restoreAllMocks();
jest.restoreAllMocks();
});
it('shows tooltip when content overflows and input is clamped at maxAutoWidth', async () => {

View File

@@ -0,0 +1,126 @@
import { ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
import { parseAsString, useQueryState } from 'nuqs';
import { useStore } from 'zustand';
import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from '../utils';
import { QuerySearchV2Context } from './context';
import type { QuerySearchV2ContextValue } from './QuerySearchV2.store';
import { createExpressionStore } from './QuerySearchV2.store';
export interface QuerySearchV2ProviderProps {
queryParamKey: string;
initialExpression?: string;
/**
* @default false
*/
persistOnUnmount?: boolean;
children: ReactNode;
}
/**
* Provider component that creates a scoped zustand store and exposes
* expression state to children via context.
*/
export function QuerySearchV2Provider({
initialExpression = '',
persistOnUnmount = false,
queryParamKey,
children,
}: QuerySearchV2ProviderProps): JSX.Element {
const storeRef = useRef(createExpressionStore());
const store = storeRef.current;
const [urlExpression, setUrlExpression] = useQueryState(
queryParamKey,
parseAsString,
);
const committedExpression = useStore(store, (s) => s.committedExpression);
const setInputExpression = useStore(store, (s) => s.setInputExpression);
const commitExpression = useStore(store, (s) => s.commitExpression);
const initializeFromUrl = useStore(store, (s) => s.initializeFromUrl);
const resetExpression = useStore(store, (s) => s.resetExpression);
const isInitialized = useRef(false);
useEffect(() => {
if (!isInitialized.current && urlExpression) {
const cleanedExpression = getUserExpressionFromCombined(
initialExpression,
urlExpression,
);
initializeFromUrl(cleanedExpression);
isInitialized.current = true;
}
}, [urlExpression, initialExpression, initializeFromUrl]);
useEffect(() => {
if (isInitialized.current || !urlExpression) {
setUrlExpression(committedExpression || null);
}
}, [committedExpression, setUrlExpression, urlExpression]);
useEffect(() => {
return (): void => {
if (!persistOnUnmount) {
setUrlExpression(null);
resetExpression();
}
};
}, [persistOnUnmount, setUrlExpression, resetExpression]);
const handleChange = useCallback(
(expression: string): void => {
const userOnly = getUserExpressionFromCombined(
initialExpression,
expression,
);
setInputExpression(userOnly);
},
[initialExpression, setInputExpression],
);
const handleRun = useCallback(
(expression: string): void => {
const userOnly = getUserExpressionFromCombined(
initialExpression,
expression,
);
commitExpression(userOnly);
},
[initialExpression, commitExpression],
);
const combinedExpression = useMemo(
() => combineInitialAndUserExpression(initialExpression, committedExpression),
[initialExpression, committedExpression],
);
const contextValue = useMemo<QuerySearchV2ContextValue>(
() => ({
expression: combinedExpression,
userExpression: committedExpression,
initialExpression,
querySearchProps: {
initialExpression: initialExpression.trim() ? initialExpression : undefined,
onChange: handleChange,
onRun: handleRun,
},
}),
[
combinedExpression,
committedExpression,
initialExpression,
handleChange,
handleRun,
],
);
return (
<QuerySearchV2Context.Provider value={contextValue}>
{children}
</QuerySearchV2Context.Provider>
);
}

View File

@@ -0,0 +1,60 @@
import { createStore, StoreApi } from 'zustand';
export type QuerySearchV2Store = {
/**
* User-typed expression (local state, updates on typing)
*/
inputExpression: string;
/**
* Committed expression (synced to URL, updates on submit)
*/
committedExpression: string;
setInputExpression: (expression: string) => void;
commitExpression: (expression: string) => void;
resetExpression: () => void;
initializeFromUrl: (urlExpression: string) => void;
};
export interface QuerySearchProps {
initialExpression: string | undefined;
onChange: (expression: string) => void;
onRun: (expression: string) => void;
}
export interface QuerySearchV2ContextValue {
/**
* Combined expression: "initialExpression AND (userExpression)"
*/
expression: string;
userExpression: string;
initialExpression: string;
querySearchProps: QuerySearchProps;
}
export function createExpressionStore(): StoreApi<QuerySearchV2Store> {
return createStore<QuerySearchV2Store>((set) => ({
inputExpression: '',
committedExpression: '',
setInputExpression: (expression: string): void => {
set({ inputExpression: expression });
},
commitExpression: (expression: string): void => {
set({
inputExpression: expression,
committedExpression: expression,
});
},
resetExpression: (): void => {
set({
inputExpression: '',
committedExpression: '',
});
},
initializeFromUrl: (urlExpression: string): void => {
set({
inputExpression: urlExpression,
committedExpression: urlExpression,
});
},
}));
}

View File

@@ -0,0 +1,95 @@
import { ReactNode } from 'react';
import { act, renderHook } from '@testing-library/react';
import { useQuerySearchV2Context } from '../context';
import {
QuerySearchV2Provider,
QuerySearchV2ProviderProps,
} from '../QuerySearchV2.provider';
const mockSetQueryState = jest.fn();
let mockUrlValue: string | null = null;
jest.mock('nuqs', () => ({
parseAsString: {},
useQueryState: jest.fn(() => [mockUrlValue, mockSetQueryState]),
}));
function createWrapper(
props: Partial<QuerySearchV2ProviderProps> = {},
): ({ children }: { children: ReactNode }) => JSX.Element {
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
return (
<QuerySearchV2Provider queryParamKey="testExpression" {...props}>
{children}
</QuerySearchV2Provider>
);
};
}
describe('QuerySearchExpressionProvider', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUrlValue = null;
});
it('should provide initial context values', () => {
const { result } = renderHook(() => useQuerySearchV2Context(), {
wrapper: createWrapper(),
});
expect(result.current.expression).toBe('');
expect(result.current.userExpression).toBe('');
expect(result.current.initialExpression).toBe('');
});
it('should combine initialExpression with userExpression', () => {
const { result } = renderHook(() => useQuerySearchV2Context(), {
wrapper: createWrapper({ initialExpression: 'k8s.pod.name = "my-pod"' }),
});
expect(result.current.expression).toBe('k8s.pod.name = "my-pod"');
expect(result.current.initialExpression).toBe('k8s.pod.name = "my-pod"');
act(() => {
result.current.querySearchProps.onChange('service = "api"');
});
act(() => {
result.current.querySearchProps.onRun('service = "api"');
});
expect(result.current.expression).toBe(
'k8s.pod.name = "my-pod" AND (service = "api")',
);
expect(result.current.userExpression).toBe('service = "api"');
});
it('should provide querySearchProps with correct callbacks', () => {
const { result } = renderHook(() => useQuerySearchV2Context(), {
wrapper: createWrapper({ initialExpression: 'initial' }),
});
expect(result.current.querySearchProps.initialExpression).toBe('initial');
expect(typeof result.current.querySearchProps.onChange).toBe('function');
expect(typeof result.current.querySearchProps.onRun).toBe('function');
});
it('should initialize from URL value on mount', () => {
mockUrlValue = 'status = 500';
const { result } = renderHook(() => useQuerySearchV2Context(), {
wrapper: createWrapper(),
});
expect(result.current.userExpression).toBe('status = 500');
expect(result.current.expression).toBe('status = 500');
});
it('should throw error when used outside provider', () => {
expect(() => {
renderHook(() => useQuerySearchV2Context());
}).toThrow(
'useQuerySearchV2Context must be used within a QuerySearchV2Provider',
);
});
});

View File

@@ -0,0 +1,61 @@
import { createExpressionStore } from '../QuerySearchV2.store';
describe('createExpressionStore', () => {
it('should create a store with initial state', () => {
const store = createExpressionStore();
const state = store.getState();
expect(state.inputExpression).toBe('');
expect(state.committedExpression).toBe('');
});
it('should update inputExpression via setInputExpression', () => {
const store = createExpressionStore();
store.getState().setInputExpression('service.name = "api"');
expect(store.getState().inputExpression).toBe('service.name = "api"');
expect(store.getState().committedExpression).toBe('');
});
it('should update both expressions via commitExpression', () => {
const store = createExpressionStore();
store.getState().setInputExpression('service.name = "api"');
store.getState().commitExpression('service.name = "api"');
expect(store.getState().inputExpression).toBe('service.name = "api"');
expect(store.getState().committedExpression).toBe('service.name = "api"');
});
it('should reset all state via resetExpression', () => {
const store = createExpressionStore();
store.getState().setInputExpression('service.name = "api"');
store.getState().commitExpression('service.name = "api"');
store.getState().resetExpression();
expect(store.getState().inputExpression).toBe('');
expect(store.getState().committedExpression).toBe('');
});
it('should initialize from URL value', () => {
const store = createExpressionStore();
store.getState().initializeFromUrl('status = 500');
expect(store.getState().inputExpression).toBe('status = 500');
expect(store.getState().committedExpression).toBe('status = 500');
});
it('should create isolated store instances', () => {
const store1 = createExpressionStore();
const store2 = createExpressionStore();
store1.getState().setInputExpression('expr1');
store2.getState().setInputExpression('expr2');
expect(store1.getState().inputExpression).toBe('expr1');
expect(store2.getState().inputExpression).toBe('expr2');
});
});

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