Compare commits

..

27 Commits

Author SHA1 Message Date
Vinícius Lourenço
f456aa07b0 test(span-login): split huge file tests in smaller ones 2026-05-07 14:24:13 -03:00
Aditya Singh
f065edf53f Feat: Trace Details Revamp (#10973)
* feat: flamegraph canvas init

* feat: add text to spans

* feat: added timeline v3

* feat: zoom and drag added

* feat: update span colors

* feat: handle click and hover with tooltip

* feat: temp change

* feat: fix timerange unit selection when zoomed

* feat: scroll to selected span

* feat: fix style

* feat: fix style

* feat: reduce timeline intervals

* fix: style fix

* fix: update color

* feat: bg color for selected and hover spans

* feat: remove unnecessary props

* feat: minor comment added

* feat: add test cases for flamegraph

* feat: add test utils

* feat: waterfall init

* 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>

* 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>

* feat: match span style

* feat: fix hover option overflow

* feat: span hover popover sync

* feat: row based flamegraph

* feat: subtree segregated tree

* feat: subtree segregated tree

* feat: subtree segregated tree

* feat: move to service worker

* feat: connector line ux

* feat: event dots in trace details

* feat: waterfall resizable

* feat: span details init

* feat: span details header

* feat: details field component

* feat: added span percentile

* feat: key attr section added

* feat: added pretty view

* feat: update yarn lock

* feat: minor change

* feat: search in pretty view

* feat: refactor

* feat: style fix

* feat: json viewer with select dropdown added

* feat: span details floating drawer added

* feat: span details folder rename

* feat: replace draggable package

* feat: fix pinning. fix drag on top

* feat: add bound to drags while floating

* feat: add collapsible sections in trace details

* feat: use resizable for waterfall table as well

* feat: copy link change and url clear on span close

* feat: fix span details headr

* feat: key value label style fixes

* feat: linked spans

* feat: style fixes

* 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

* refactor: move type conversion logic to types pkg

* chore: add reason for using snake case in response

* fix: update span.attributes to map of string to any

To support otel format of diffrent types of attributes

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* refactor: convert waterfall api to modules format

* chore: add same test cases as for old waterfall api

* chore: avoid sorting on every traversal

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* fix: rename timestamp to milli for readability

* fix: add timeout to module context

* fix: use typed paramter field in logs

* feat: api integration

* feat: add limit

* feat: minor change

* feat: supress click

* chore: generate openapi spec for v3 waterfall

* feat: fix test

* feat: fix test

* feat: lint fix

* feat: span details ux

* feat: analytics

* feat: add icons

* feat: added loading to flamegraph and timeout to webworker

* feat: sync error and loading state for flamegraph for n/w and computation logic

* feat: auto scroll horizontally to span

* feat: show total span count

* feat: disable anaytics span tab for now

* feat: add span details loader

* feat: prevent api call on closing span detail

* fix: remove timeout since waterfall take longer

* fix: use int16 for status code as per db schema

* fix: update openapi specs

* feat: make filter and search work with flamegraph

* feat: filter ui fix

* feat: remove trace header

* feat: new filter ui

* 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

* refactor: move type conversion logic to types pkg

* chore: add reason for using snake case in response

* fix: update span.attributes to map of string to any

To support otel format of diffrent types of attributes

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* refactor: convert waterfall api to modules format

* chore: add same test cases as for old waterfall api

* chore: avoid sorting on every traversal

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* fix: rename timestamp to milli for readability

* fix: add timeout to module context

* fix: use typed paramter field in logs

* chore: generate openapi spec for v3 waterfall

* fix: remove timeout since waterfall take longer

* fix: use int16 for status code as per db schema

* fix: update openapi specs

* feat: api integration

* feat: automatically scroll left on vertical scroll

* feat: reduce time

* feat: set limit to 100k for flamegraph

* feat: show child count in waterfall

* fix: align timeline and span length in flamegraph and waterfall

* feat: fix flamegraph and waterfall bg color

* feat: show caution on sampled flamegraph

* feat: api integration v3

* feat: disable scroll to view for collapse and uncollapse

* 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

* refactor: move type conversion logic to types pkg

* chore: add reason for using snake case in response

* fix: update span.attributes to map of string to any

To support otel format of diffrent types of attributes

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* refactor: convert waterfall api to modules format

* chore: add same test cases as for old waterfall api

* chore: avoid sorting on every traversal

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* fix: rename timestamp to milli for readability

* fix: add timeout to module context

* fix: use typed paramter field in logs

* chore: generate openapi spec for v3 waterfall

* fix: remove timeout since waterfall take longer

* fix: use int16 for status code as per db schema

* fix: update openapi specs

* refactor: break down GetWaterfall method for readability

* chore: avoid returning nil, nil

* 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

* 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.

* refactor: move error to types as well

* refactor: separate out store calls and computations

* refactor: breakdown GetSelectedSpans for readability

* refactor: return 404 on missing trace and other cleanup

* refactor: use same method for cache key creation

* chore: remove unused duration nano field

* chore: use sqlbuilder in clickhouse store where possible

* feat: dropdown added to span details

* feat: fix color duplications

* feat: no data screen

* feat: old trace btn added

* feat: minor fix

* feat: rename copy to copy value

* feat: delete unused file

* feat: use semantic tokens

* feat: use semantic tokens

* feat: add crosshair

* feat: fix test

* feat: disable crosshair in waterfall

* feat: fix colors

* feat: minor fix

* feat: add status codes

* feat: load all spans in waterfall under limit

* feat: uncollapse spans on select from flamegraph

* feat: style fix

* feat: add service name

* feat: open in new tab

* feat: delete waterfall go

* feat: minor change

* feat: minor change

* feat: minor refactors

* feat: minor refactors

* feat: v3 behind feature flag

* feat: minor refactors

* feat: packages remove

* feat: packages remove

* feat: remove common component

* feat: remove antd component usage

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Nikhil Soni <nikhil.soni@signoz.io>
2026-05-07 13:10:49 +00:00
Vinicius Lourenço
382cd57a6a feat(infra-monitoring): show total items count on list page (#11212) 2026-05-07 12:06:04 +00:00
Vinicius Lourenço
fefef70d84 fix(lint-staged): run format for css files & parallel tsgo check (#11213) 2026-05-07 11:02:21 +00:00
Nikhil Mantri
9e94ee30b9 feat(infra-monitoring): v2 nodes list api (#11128)
* chore: baseline setup

* chore: endpoint detail update

* chore: added logic for hosts v3 api

* fix: bug fix

* chore: disk usage

* chore: added validate function

* chore: added some unit tests

* chore: return status as a string

* chore: yarn generate api

* chore: removed isSendingK8sAgentsMetricsCode

* chore: moved funcs

* chore: added validation on order by

* chore: added pods list logic

* chore: updated openapi yml

* chore: updated spec

* chore: pods api meta start time

* chore: nil pointer check

* chore: nil pointer dereference fix in req.Filter

* chore: added temporalities of metrics

* chore: added pods metrics temporality

* chore: unified composite key function

* chore: code improvements

* chore: added pods list api updates

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

* chore: yarn generate api

* chore: return errors from getMetadata and lint fix

* chore: return errors from getMetadata and lint fix

* chore: added hostName logic

* chore: modified getMetadata query

* chore: add type for response and files rearrange

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

* chore: added better metrics existence check

* chore: added a TODO remark

* chore: added required metrics check

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

* chore: frontend fix

* chore: endpoint correction

* chore: endpoint modification openapi

* chore: escape backtick to prevent sql injection

* chore: rearrage

* chore: improvements

* chore: validate order by to validate function

* chore: improved description

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

* chore: ignore empty string hosts in get active hosts

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

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

* chore: bug fix

* chore: added subquery for active and total count

* chore: ignore empty string hosts in get active hosts

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

* chore: refactor code

* chore: rename HostsList -> ListHosts

* chore: rearrangement

* chore: inframonitoring types renaming

* chore: added types package

* chore: file structure further breakdown for clarity

* chore: comments correction

* chore: removed temporalities

* chore: pods code restructuring

* chore: comments resolve

* chore: added json tag required: true

* chore: removed pod metric temporalities

* chore: removed internal server error

* chore: added status unauthorized

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

* chore: cleanup and rename

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

* chore: regen api client for inframonitoring

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

* chore: added phase counts feature

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

* chore: added required tags

* chore: added support for pod phase unknown

* chore: removed pods - order by phase

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

* fix: rebase fixes

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

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

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types

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

* chore: added phase counts feature

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

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

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

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types

* chore: nodes list v2 full blown

* chore: metadata fix

* chore: updated comment

* chore: v2 nodes api

* chore: added pod phase counts

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

* chore: node and pod counts structs added

* chore: strongly type meta

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-05-07 10:46:02 +00:00
Deepak Mardi
078b82e957 fix(frontend): simplify password matching logic (#11174)
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
* fix(frontend): simplify password matching logic

* fix(frontend): address review comments on validation feedback

* refactor(signup): remove redundant password mismatch check
2026-05-06 21:51:08 +00:00
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
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
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
898 changed files with 41041 additions and 18963 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:
@@ -2424,7 +2547,8 @@ components:
format: double
type: number
meta:
additionalProperties: {}
additionalProperties:
type: string
nullable: true
type: object
status:
@@ -2474,6 +2598,103 @@ components:
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesNodeCondition:
enum:
- ready
- not_ready
- no_data
type: string
InframonitoringtypesNodeCountsByReadiness:
properties:
notReady:
type: integer
ready:
type: integer
required:
- ready
- notReady
type: object
InframonitoringtypesNodeRecord:
properties:
condition:
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
meta:
additionalProperties:
type: string
nullable: true
type: object
nodeCPU:
format: double
type: number
nodeCPUAllocatable:
format: double
type: number
nodeCountsByReadiness:
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
nodeMemory:
format: double
type: number
nodeMemoryAllocatable:
format: double
type: number
nodeName:
type: string
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
required:
- nodeName
- condition
- nodeCountsByReadiness
- podCountsByPhase
- nodeCPU
- nodeCPUAllocatable
- nodeMemory
- nodeMemoryAllocatable
- meta
type: object
InframonitoringtypesNodes:
properties:
endTimeBeforeRetention:
type: boolean
records:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
$ref: '#/components/schemas/InframonitoringtypesResponseType'
warning:
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
required:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesPodCountsByPhase:
properties:
failed:
type: integer
pending:
type: integer
running:
type: integer
succeeded:
type: integer
unknown:
type: integer
required:
- pending
- running
- succeeded
- failed
- unknown
type: object
InframonitoringtypesPodPhase:
enum:
- pending
@@ -2481,18 +2702,15 @@ components:
- succeeded
- failed
- unknown
- ""
- no_data
type: string
InframonitoringtypesPodRecord:
properties:
failedPodCount:
type: integer
meta:
additionalProperties: {}
additionalProperties:
type: string
nullable: true
type: object
pendingPodCount:
type: integer
podAge:
format: int64
type: integer
@@ -2505,6 +2723,8 @@ components:
podCPURequest:
format: double
type: number
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podMemory:
format: double
type: number
@@ -2518,12 +2738,6 @@ components:
$ref: '#/components/schemas/InframonitoringtypesPodPhase'
podUID:
type: string
runningPodCount:
type: integer
succeededPodCount:
type: integer
unknownPodCount:
type: integer
required:
- podUID
- podCPU
@@ -2533,11 +2747,7 @@ components:
- podMemoryRequest
- podMemoryLimit
- podPhase
- pendingPodCount
- runningPodCount
- succeededPodCount
- failedPodCount
- unknownPodCount
- podCountsByPhase
- podAge
- meta
type: object
@@ -2591,6 +2801,32 @@ components:
- end
- limit
type: object
InframonitoringtypesPostableNodes:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
orderBy:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
InframonitoringtypesPostablePods:
properties:
end:
@@ -5200,6 +5436,9 @@ components:
sub_tree_node_count:
minimum: 0
type: integer
time_unix:
minimum: 0
type: integer
trace_id:
type: string
trace_state:
@@ -5580,35 +5819,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 +5875,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ConfigReceiver'
$ref: '#/components/schemas/AlertmanagertypesPostableChannel'
responses:
"201":
content:
@@ -6944,20 +7154,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 +7252,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 +7323,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesUpdateableAuthDomain'
$ref: '#/components/schemas/AuthtypesUpdatableAuthDomain'
responses:
"204":
description: No Content
@@ -8775,7 +9042,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/AuthtypesGettableObjects'
$ref: '#/components/schemas/CoretypesObjectGroup'
type: array
status:
type: string
@@ -8848,7 +9115,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesPatchableObjects'
$ref: '#/components/schemas/CoretypesPatchableObjects'
responses:
"204":
content:
@@ -11464,12 +11731,83 @@ paths:
summary: List Hosts for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/nodes:
post:
deprecated: false
description: 'Returns a paginated list of Kubernetes nodes with key metrics:
CPU usage, CPU allocatable, memory working set, memory allocatable, per-group
nodeCountsByReadiness ({ ready, notReady } from each node''s latest k8s.node.condition_ready
in the window) and per-group podCountsByPhase ({ pending, running, succeeded,
failed, unknown } for pods scheduled on the listed nodes). Each node includes
metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is
''list'' for the default k8s.node.name grouping (each row is one node with
its current condition string: ready / not_ready / no_data) or ''grouped_list''
for custom groupBy keys (each row aggregates nodes in the group; condition
stays no_data). Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
via offset/limit. Also reports missing required metrics and whether the requested
time range falls before the data retention boundary. Numeric metric fields
(nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1
as a sentinel when no data is available for that field.'
operationId: ListNodes
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InframonitoringtypesPostableNodes'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/InframonitoringtypesNodes'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List Nodes for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/pods:
post:
deprecated: false
description: 'Returns a paginated list of Kubernetes pods with key metrics:
CPU usage, CPU request/limit utilization, memory working set, memory request/limit
utilization, current pod phase (pending/running/succeeded/failed/unknown),
utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data),
and pod age (ms since start time). Each pod includes metadata attributes (namespace,
node, workload owner such as deployment/statefulset/daemonset/job/cronjob,
cluster). Supports filtering via a filter expression, custom groupBy to aggregate
@@ -11477,13 +11815,13 @@ paths:
cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit.
The response type is ''list'' for the default k8s.pod.uid grouping (each row
is one pod with its current phase) or ''grouped_list'' for custom groupBy
keys (each row aggregates pods in the group with per-phase counts: pendingPodCount,
runningPodCount, succeededPodCount, failedPodCount, unknownPodCount derived
from each pod''s latest phase in the window). Also reports missing required
metrics and whether the requested time range falls before the data retention
boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory,
podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no
data is available for that field.'
keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase:
{ pending, running, succeeded, failed, unknown } derived from each pod''s
latest phase in the window). Also reports missing required metrics and whether
the requested time range falls before the data retention boundary. Numeric
metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest,
podMemoryLimit, podAge) return -1 as a sentinel when no data is available
for that field.'
operationId: ListPods
requestBody:
content:

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

@@ -3,6 +3,7 @@ import type { Config } from '@jest/types';
const USE_SAFE_NAVIGATE_MOCK_PATH = '<rootDir>/__mocks__/useSafeNavigate.ts';
const config: Config.InitialOptions = {
maxWorkers: '50%',
silent: true,
clearMocks: true,
coverageDirectory: 'coverage',

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,18 +233,20 @@
"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",
"oxfmt --write",
"sh -c tsgo --noEmit"
],
"*.(js|jsx|ts|tsx|scss|css)": [
"oxlint --fix --quiet --no-error-on-unmatched-pattern",
"oxfmt --write"
],
"*.(scss|css)": [
"stylelint"
]

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

@@ -13,8 +13,10 @@ import type {
import type {
InframonitoringtypesPostableHostsDTO,
InframonitoringtypesPostableNodesDTO,
InframonitoringtypesPostablePodsDTO,
ListHosts200,
ListNodes200,
ListPods200,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -107,7 +109,91 @@ export const useListHosts = <
return useMutation(mutationOptions);
};
/**
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts: pendingPodCount, runningPodCount, succeededPodCount, failedPodCount, unknownPodCount derived from each pod's latest phase in the window). Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Nodes for Infra Monitoring
*/
export const listNodes = (
inframonitoringtypesPostableNodesDTO: BodyType<InframonitoringtypesPostableNodesDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListNodes200>({
url: `/api/v2/infra_monitoring/nodes`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: inframonitoringtypesPostableNodesDTO,
signal,
});
};
export const getListNodesMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listNodes>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof listNodes>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> },
TContext
> => {
const mutationKey = ['listNodes'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof listNodes>>,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> }
> = (props) => {
const { data } = props ?? {};
return listNodes(data);
};
return { mutationFn, ...mutationOptions };
};
export type ListNodesMutationResult = NonNullable<
Awaited<ReturnType<typeof listNodes>>
>;
export type ListNodesMutationBody =
BodyType<InframonitoringtypesPostableNodesDTO>;
export type ListNodesMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List Nodes for Infra Monitoring
*/
export const useListNodes = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listNodes>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof listNodes>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> },
TContext
> => {
const mutationOptions = getListNodesMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* @summary List Pods for Infra Monitoring
*/
export const listPods = (

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,
});
},
}));
}

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