Compare commits

..

7 Commits

Author SHA1 Message Date
Abhi Kumar
053d306e99 chore: pr review fixes 2026-07-05 01:58:57 +05:30
Abhi Kumar
6693bb7a06 chore: removed duplicate tests 2026-07-05 01:58:57 +05:30
Abhi Kumar
8f4c00849c chore: fixed lint issue 2026-07-05 01:58:57 +05:30
Abhi Kumar
77390d5a5e chore: cleaned up duplicate tests 2026-07-05 01:58:57 +05:30
Abhi Kumar
cc94b87357 chore: ran oxfmt 2026-07-05 01:58:57 +05:30
Abhi Kumar
adb60c4683 chore: added more tests 2026-07-05 01:58:57 +05:30
Abhi Kumar
7d8f896a5f test: added e2e tests for dashboard create flow 2026-07-05 01:58:56 +05:30
1649 changed files with 23991 additions and 131575 deletions

View File

@@ -1,6 +1,6 @@
services:
init-clickhouse:
image: clickhouse/clickhouse-server:25.12.5
image: clickhouse/clickhouse-server:25.5.6
container_name: init-clickhouse
command:
- bash
@@ -18,7 +18,7 @@ services:
volumes:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
clickhouse:
image: clickhouse/clickhouse-server:25.12.5
image: clickhouse/clickhouse-server:25.5.6
container_name: clickhouse
volumes:
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
@@ -67,7 +67,7 @@ services:
timeout: 5s
retries: 3
telemetrystore-migrator:
image: signoz/signoz-otel-collector:v0.144.6
image: signoz/signoz-otel-collector:v0.142.0
container_name: telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

2
.github/CODEOWNERS vendored
View File

@@ -189,9 +189,7 @@ go.mod @therealpandey
## Infrastructure Monitoring
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHosts/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHostsV2/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8s/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8sV2/ @SigNoz/pulse-frontend
## Alerts
/frontend/src/pages/AlertList/ @SigNoz/pulse-frontend

View File

@@ -48,31 +48,24 @@ jobs:
- logspipelines
- passwordauthn
- preference
- querierlogs
- queriertraces
- queriermetrics
- querierscalar
- queriercommon
- querier
- rawexportdata
- querierauthz
- role
- rootuser
- serviceaccount
- spanmapper
- querier_json_body
- querier_skip_resource_fingerprint
- ttl
- clickhousecluster
- metricreduction
sqlstore-provider:
- postgres
- sqlite
sqlite-mode:
- wal
clickhouse-version:
- 25.5.6
- 25.12.5
schema-migrator-version:
- v0.144.6
- v0.144.3
postgres-version:
- 15
if: |

View File

@@ -9,11 +9,6 @@ global:
# the path component (e.g. /signoz in https://example.com/signoz) is used
# as the base path for all HTTP routes (both API and web frontend).
external_url: <unset>
# origins (scheme://host[:port], no path) allowed as login redirect targets. include
# the origin the signoz ui is served on. when not configured, redirect targets are
# not validated.
# allowed_origins:
# - https://signoz.example.com
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
ingestion_url: <unset>
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.
@@ -134,7 +129,7 @@ sqlstore:
# The timeout for the sqlite database to wait for a lock.
busy_timeout: 10s
# The default transaction locking behavior. Supported values: deferred, immediate, exclusive.
transaction_mode: immediate
transaction_mode: deferred
##################### APIServer #####################
apiserver:
@@ -160,8 +155,8 @@ querier:
cache_ttl: 168h
# The interval for recent data that should not be cached.
flux_interval: 5m
# The maximum number of queries a single query range request runs at once.
max_concurrent_queries: 8
# The maximum number of concurrent queries for missing ranges.
max_concurrent_queries: 4
# When filtering logs by trace_id, clamp the query window to the trace time
# range with padding to include slightly delayed log exports. Logs only; set
# to 0 to disable.

File diff suppressed because it is too large Load Diff

View File

@@ -1,149 +0,0 @@
# Authz
SigNoz uses [OpenFGA](https://openfga.dev/), a relationship-based access control (ReBAC) system, to authorize every request. Transactions are never attached to users directly — they are attached to **roles** (as relationship tuples in OpenFGA), and users or service accounts (principals) are made **assignees** of those roles. The central interface is `AuthZ` in [pkg/authz/authz.go](/pkg/authz/authz.go), backed by an embedded OpenFGA server in [pkg/authz/openfgaserver](/pkg/authz/openfgaserver/server.go).
As a feature author, you will rarely touch OpenFGA directly. You interact with two layers:
1. **The registries** in [pkg/types/coretypes](/pkg/types/coretypes) — where every resource, verb, and managed-role permission is declared in code.
2. **The route wiring** in [pkg/apiserver/signozapiserver](/pkg/apiserver/signozapiserver) — where each route declares what resource it touches and which verb it needs, and the middleware turns that into a check.
## What are the building blocks?
### Type
A `Type` is the coarse FGA type of a resource. The set is finite and defined in [pkg/types/coretypes/registry_type.go](/pkg/types/coretypes/registry_type.go): `user`, `serviceaccount`, `anonymous`, `role`, `organization`, `metaresource`, and `telemetryresource`. Each type carries a selector regex (what a valid ID looks like) and the verbs allowed on it.
Almost every feature resource is a `metaresource` (dashboards, rules, pipelines, ...) or a `telemetryresource` (logs, traces, metrics). You should almost never need a new type.
### Verb
A `Verb` is the action being authorized. All verbs are defined in [pkg/types/coretypes/registry_verb.go](/pkg/types/coretypes/registry_verb.go): `create`, `read`, `update`, `delete`, `list`, `assignee`, `attach`, and `detach`.
- `assignee` is special: it is the membership relation between a subject and a role ("user X is an assignee of role Y"), not an action a route checks for.
- `attach`/`detach` authorize linking two resources together (e.g. assigning a role to a service account).
### Kind
A `Kind` is the fine-grained name of your resource — `dashboard`, `rule`, `quick-filter`. Kinds are registered in [pkg/types/coretypes/registry_kind.go](/pkg/types/coretypes/registry_kind.go). A kind rides on top of an existing type, so adding one does **not** require any OpenFGA schema change.
### Resource
A `Resource` ties a type and a kind together and knows how to render itself as an FGA object string. The interface lives in [pkg/types/coretypes/resource.go](/pkg/types/coretypes/resource.go):
```go
type Resource interface {
Type() Type
Kind() Kind
Prefix(orgId valuer.UUID) string // metaresource:organization/<orgID>/dashboard
Object(orgId valuer.UUID, selector string) string
Scope(verb Verb) string // dashboard:read
AllowedVerbs() []Verb
}
```
All resources are registered in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) using constructors like `NewResourceMetaResource(KindDashboard)` or `NewResourceTelemetryResource(KindLogs)`.
### Selector
A `Selector` identifies *which* instance(s) of a resource a check is about — a UUID, a role name, or the wildcard `*`. A `SelectorFunc` maps the extracted resource ID to selectors at request time. Two prebuilt ones cover most routes ([pkg/types/coretypes/selector.go](/pkg/types/coretypes/selector.go)):
- `WildcardSelector` — the check is against all instances of the resource (`create`, `list`).
- `IDSelector` — the check is against the specific instance *or* the wildcard (`read`, `update`, `delete` of one object). A subject authorized on `*` is authorized on every instance.
When the ID in the request is not what FGA needs (e.g. routes receive a role UUID but FGA objects use role names), write a custom `SelectorFunc` — see `roleSelector` in [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go).
### Roles, transactions, and tuples
SigNoz ships four managed roles, declared in [pkg/types/coretypes/registry_managed_role.go](/pkg/types/coretypes/registry_managed_role.go): `signoz-admin`, `signoz-editor`, `signoz-viewer`, and `signoz-anonymous`. Their permissions are declared in code as `Transaction`s (a verb on an object) in `ManagedRoleToTransactions` — this map is the single source of truth for what each managed role can do.
At organization bootstrap, `CreateManagedRoles` and `CreateManagedUserRoleTransactions` (see [pkg/authz/authz.go](/pkg/authz/authz.go)) persist the role rows and write one OpenFGA tuple per transaction, linking `role:organization/<orgID>/role/<name>#assignee` to each permitted object. Users and service accounts are then granted roles via `Grant`/`Revoke`, which writes `assignee` tuples. Custom roles (enterprise) are managed through the roles API in [pkg/authz/signozauthzapi/handler.go](/pkg/authz/signozauthzapi/handler.go).
### Schema
The OpenFGA authorization model is a hand-written DSL, embedded at build time: [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) for community and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) for enterprise. The community model only supports role assignment; the enterprise model defines per-verb relations on every type, enabling genuine per-resource checks. This split is why `CheckWithTupleCreation` behaves differently per edition (see [How does a check work at runtime?](#how-does-a-check-work-at-runtime)). You only touch these files when introducing a brand-new **type** — never for a new kind.
## How do I add authz to my feature?
### 1. Register the kind
Add your kind in [pkg/types/coretypes/registry_kind.go](/pkg/types/coretypes/registry_kind.go) and append it to `Kinds`:
```go
KindThing = MustNewKind("thing")
```
### 2. Register the resource
Add the resource in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) and append it to `Resources`:
```go
ResourceMetaResourceThing = NewResourceMetaResource(KindThing)
```
Pass an explicit verb list to `NewResourceMetaResource` only if your resource supports fewer verbs than the type default.
### 3. Grant permissions to managed roles
Decide what each managed role can do with your resource and add the transactions in [pkg/types/coretypes/registry_managed_role.go](/pkg/types/coretypes/registry_managed_role.go):
```go
// thing — editors manage, viewers read
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindThing}, WildCardSelectorString)},
```
The convention so far: admin gets everything, editor gets CRUD on day-to-day observability resources, viewer gets `read`/`list`, anonymous gets nothing (except public dashboards).
### 4. Wire the route
Register the route in [pkg/apiserver/signozapiserver](/pkg/apiserver/signozapiserver), wrapping your handler with `CheckResources` and declaring what the route touches via a `ResourceDef` ([pkg/http/handler/resourcedef.go](/pkg/http/handler/resourcedef.go)). A complete example from [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go):
```go
router.Handle("/api/v1/service_accounts", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.Create, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "CreateServiceAccount",
// ...
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryAccessControl,
ID: coretypes.ResponseJSONPath("data.id"),
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost)
```
The pieces:
- **`CheckResources(handlerFn, roles...)`** — the resource-aware authorization wrapper from [pkg/http/middleware/authz.go](/pkg/http/middleware/authz.go). The role list is the community-edition fallback: which managed roles may call this route when per-resource checks are unavailable.
- **`ResourceDef`** — declares the resource, verb, audit category, how to extract the instance ID, and how to turn that ID into selectors. ID extractors live in [pkg/types/coretypes/extractor.go](/pkg/types/coretypes/extractor.go): `PathParam("id")`, `BodyJSONPath("data.id")`, `BodyJSONArray("ids")`, and `ResponseJSONPath("data.id")` for IDs only known after the handler runs (e.g. `create`).
- **`SecuritySchemes`** — advertises the required scope (`resource.Scope(verb)`, e.g. `serviceaccount:create`) in the OpenAPI spec.
For routes that link two resources, use `AttachDetachSiblingResourceDef` (both sides are authz-checked, e.g. attaching a role to a service account requires `attach` on **both** the service account and the role). For parent-child routes (e.g. creating an API key under a service account), both sides are checked too, but with different verbs: declare a `BasicResourceDef` checking the child with `create`/`delete`, alongside an `AttachDetachParentChildResourceDef` checking the parent with `attach`/`detach` (within that def the child is only recorded for audit) — see the `/api/v1/service_accounts/{id}/keys` route in [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go).
Prefer `CheckResources` with a `ResourceDef` for anything resource-shaped. The older coarse gates `ViewAccess`/`EditAccess`/`AdminAccess` only check "does the caller hold one of these roles" and give up per-resource granularity; `OpenAccess` performs no authorization (authentication still applies); `CheckWithoutClaims` serves anonymous routes such as public dashboards.
### 5. Backfill existing organizations (only if needed)
Managed-role tuples are written from the registry at organization creation, so **new resources need no migration for new organizations**. If existing organizations must get the new permissions, add a migration in [pkg/sqlmigration](/pkg/sqlmigration) that inserts the tuples — see [083_add_role_crud_tuples.go](/pkg/sqlmigration/083_add_role_crud_tuples.go) for the pattern.
## How does a check work at runtime?
1. The resource middleware ([pkg/http/middleware/resource.go](/pkg/http/middleware/resource.go)) runs on every request. It reads the matched handler's `ResourceDef`s, extracts the resource IDs from path/body, and stores the resolved resources in the request context.
2. `CheckResources` reads them back, runs each `SelectorFunc`, and calls `AuthZ.CheckWithTupleCreation(ctx, claims, orgID, relation, resource, selectors, roleSelectors)`.
3. What happens next depends on the edition:
- **Community** ([pkg/authz/openfgaserver/server.go](/pkg/authz/openfgaserver/server.go)) ignores the resource and selectors and only checks whether the subject is an `assignee` of one of the allowed roles — a plain role gate.
- **Enterprise** ([ee/authz/openfgaserver/server.go](/ee/authz/openfgaserver/server.go)) builds tuples via `authtypes.NewTuples` — subject `user:organization/<orgID>/user/<userID>`, relation `create`, object `serviceaccount:organization/<orgID>/serviceaccount/*` — and batch-checks them against OpenFGA: genuine per-resource authorization, including custom roles.
Because both paths go through the same middleware and the same `ResourceDef` declarations, a route wired once works correctly in both editions.
## What should I remember?
- Declare authz in the registries ([pkg/types/coretypes](/pkg/types/coretypes)), not in migrations — tuples for new organizations are derived from code at bootstrap.
- A new kind never needs an OpenFGA schema change; only a new type does, and then **both** [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) must be updated together.
- Prefer `CheckResources` + `ResourceDef` over the coarse `ViewAccess`/`EditAccess`/`AdminAccess` gates for new routes.
- Use `WildcardSelector` for `create`/`list`, `IDSelector` for instance operations, and a custom `SelectorFunc` when the request ID is not the FGA selector.
- Linking routes check **both** sides: peers via `AttachDetachSiblingResourceDef` (same verb on both), parent-child via a `BasicResourceDef` on the child (`create`/`delete`) paired with an `AttachDetachParentChildResourceDef` on the parent (`attach`/`detach`).
- Changing `ManagedRoleToTransactions` only affects organizations created afterwards — add a [pkg/sqlmigration](/pkg/sqlmigration) migration to backfill existing ones.

View File

@@ -11,7 +11,6 @@ We adhere to three primary style guides as our foundation:
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
- [Abstractions](abstractions.md) - When to introduce new types and intermediate representations
- [Authz](authz.md) - Authorization, roles, and access control
- [Errors](errors.md) - Structured error handling
- [Endpoint](endpoint.md) - HTTP endpoint patterns
- [Flagger](flagger.md) - Feature flag patterns

View File

@@ -119,6 +119,10 @@ 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 coretypes.Type) ([]*coretypes.Object, error) {
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
}
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
return provider.openfgaServer.Write(ctx, additions, deletions)
}
@@ -127,6 +131,10 @@ func (provider *provider) ReadTuples(ctx context.Context, tupleKey *openfgav1.Re
return provider.openfgaServer.ReadTuples(ctx, tupleKey)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.pkgAuthzService.Get(ctx, orgID, id)
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
}
@@ -175,7 +183,7 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.RoleWithTransactionGroups) 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())
@@ -200,42 +208,86 @@ func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *a
return err
}
return provider.store.Create(ctx, role)
if err := provider.store.Create(ctx, role.Role); err != nil {
return err
}
return nil
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.store.Get(ctx, orgID, id)
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, 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())
}
existingRole, err := provider.store.GetByOrgIDAndName(ctx, role.OrgID, role.Name)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
}
if existingRole != nil {
return existingRole, nil
}
err = provider.store.Create(ctx, role)
if err != nil {
return nil, err
}
return role, nil
}
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.Role) error {
func (provider *provider) GetWithTransactionGroups(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.RoleWithTransactionGroups, 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())
}
role, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
if err != nil {
return nil, err
}
transactionGroups := authtypes.MustNewTransactionGroupsFromTuples(tuples)
return authtypes.MakeRoleWithTransactionGroups(role, transactionGroups), nil
}
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.RoleWithTransactionGroups) 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())
}
existingRole, err := provider.Get(ctx, orgID, updatedRole.ID)
existingRole, err := provider.GetWithTransactionGroups(ctx, orgID, updatedRole.ID)
if err != nil {
return err
}
existingTuples, err := provider.readAllTuplesForRole(ctx, existingRole.Name, orgID)
additions, deletions := existingRole.TransactionGroups.Diff(updatedRole.TransactionGroups)
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
if err != nil {
return err
}
desiredTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, updatedRole.TransactionGroups)
deletionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, deletions)
if err != nil {
return err
}
additionTuples, deletionTuples := authtypes.DiffTuples(existingTuples, desiredTuples)
err = provider.Write(ctx, additionTuples, deletionTuples)
if err != nil {
return err
}
return provider.store.Update(ctx, orgID, updatedRole)
return provider.store.Update(ctx, orgID, updatedRole.Role)
}
func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
@@ -244,7 +296,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.Get(ctx, orgID, id)
role, err := provider.GetWithTransactionGroups(ctx, orgID, id)
if err != nil {
return err
}
@@ -260,7 +312,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
}
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
tuples, err := authtypes.NewTuplesFromTransactionGroups(role.Name, orgID, role.TransactionGroups)
if err != nil {
return err
}
@@ -272,24 +324,6 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return provider.store.Delete(ctx, orgID, id)
}
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.registry.Types() {
typeTuples, err := provider.openfgaServer.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
})
if err != nil {
return nil, err
}
tuples = append(tuples, typeTuples...)
}
return tuples, nil
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) []*openfgav1.TupleKey {
tuples := []*openfgav1.TupleKey{}
@@ -341,3 +375,21 @@ func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*
return tuples
}
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.registry.Types() {
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
})
if err != nil {
return nil, err
}
tuples = append(tuples, typeTuples...)
}
return tuples, nil
}

View File

@@ -105,6 +105,10 @@ 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 coretypes.Type) ([]*coretypes.Object, error) {
return server.pkgAuthzService.ListObjects(ctx, subject, relation, objectType)
}
func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
return server.pkgAuthzService.Write(ctx, additions, deletions)
}

View File

@@ -519,7 +519,7 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
if err != nil {
return "", err
}
_, err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
if err != nil {
return "", err
}

View File

@@ -29,7 +29,6 @@ type module struct {
settings factory.ScopedProviderSettings
querier querier.Querier
licensing licensing.Licensing
tagModule tag.Module
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
@@ -42,7 +41,6 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
settings: scopedProviderSettings,
querier: querier,
licensing: licensing,
tagModule: tagModule,
}
}
@@ -134,55 +132,6 @@ func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.U
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
}
func (module *module) GetDashboardByPublicIDV2(ctx context.Context, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
return nil, err
}
tags, err := module.tagModule.ListForResource(ctx, storableDashboard.OrgID, coretypes.KindDashboard, storableDashboard.ID)
if err != nil {
return nil, err
}
return storableDashboard.ToDashboardV2(tags)
}
func (module *module) GetPublicWidgetQueryRangeV2(ctx context.Context, id valuer.UUID, panelKey, startTimeRaw, endTimeRaw string) (*querybuildertypesv5.QueryRangeResponse, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.CodeNamespace: "dashboard",
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRangeV2",
})
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
return nil, err
}
// tags are not needed for query range.
dashboard, err := storableDashboard.ToDashboardV2(nil)
if err != nil {
return nil, err
}
publicDashboard, err := module.GetPublic(ctx, dashboard.OrgID, dashboard.ID)
if err != nil {
return nil, err
}
startTime, endTime, err := publicDashboard.ResolveTimeRange(startTimeRaw, endTimeRaw)
if err != nil {
return nil, err
}
query, err := dashboard.GetPanelQuery(startTime, endTime, panelKey)
if err != nil {
return nil, err
}
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
}
func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publicDashboard *dashboardtypes.PublicDashboard) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
@@ -272,10 +221,6 @@ func (module *module) CloneV2(ctx context.Context, orgID valuer.UUID, createdBy
return module.pkgDashboardModule.CloneV2(ctx, orgID, createdBy, creator, id)
}
func (module *module) ConvertAllV1ToV2(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.V1ToV2MigrationResult, error) {
return module.pkgDashboardModule.ConvertAllV1ToV2(ctx, orgID)
}
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}

View File

@@ -141,7 +141,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(ctx context.Cont
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}
_, err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}

View File

@@ -116,15 +116,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
infraMonitoringV2 := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseInfraMonitoringV2, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureUseInfraMonitoringV2.String()),
Active: infraMonitoringV2,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -240,17 +240,17 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
mock := mockStore.Mock()
// Mock the fingerprint query (for Prometheus label matching)
// args: $1=metric_name (the __name__ matcher maps onto the column)
mock.ExpectQuery("SELECT fingerprint, any").
WithArgs("test_metric").
WithArgs("test_metric", "__name__", "test_metric").
WillReturnRows(fingerprintRows)
// Mock the samples query (for Prometheus metric data)
// args: metric_name IN (discovered names), subquery metric_name, start, end
mock.ExpectQuery("SELECT metric_name, fingerprint, unix_milli").
WithArgs(
"test_metric",
"test_metric",
"__name__",
"test_metric",
queryStart,
queryEnd,
).

View File

@@ -3,7 +3,6 @@ package postgressqlschema
import (
"context"
"database/sql"
"log/slog"
"github.com/uptrace/bun"
@@ -215,20 +214,6 @@ WHERE
}
func (provider *provider) GetIndices(ctx context.Context, name sqlschema.TableName) ([]sqlschema.Index, error) {
// A key is one entry an index is built on: a column (plain) or an expression
// like lower(x) (functional). Returns one row per key of each non-constraint-
// backed index on the table, ordered by position. Keys are enumerated with
// generate_series over indnkeyatts (count of key columns, excluding non-key
// INCLUDE columns), so expression keys are covered too. Reconstruction uses:
//
// index_name | index the key belongs to
// is_expression | true if the key is an expression, not a plain column
// key_def | rendered key text, e.g. org_id or lower(key)
// column_name | column name for plain keys; NULL for expression keys
// predicate | partial-index WHERE; NULL if not partial (same every row)
//
// table_name/unique/primary/column_position are carried over from the prior
// query (unique gates the loop, column_position orders the keys).
rows, err := provider.
sqlstore.
BunDB().
@@ -239,21 +224,19 @@ SELECT
i.indisunique AS unique,
i.indisprimary AS primary,
a.attname AS column_name,
n AS column_position,
(i.indkey[n - 1] = 0) AS is_expression,
pg_get_indexdef(i.indexrelid, n, true) AS key_def,
array_position(i.indkey, a.attnum) AS column_position,
pg_get_expr(i.indpred, i.indrelid) AS predicate
FROM
pg_index i
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
LEFT JOIN pg_class ci ON ci.oid = i.indexrelid
LEFT JOIN pg_attribute a ON a.attrelid = ct.oid
LEFT JOIN pg_constraint con ON con.conindid = i.indexrelid
CROSS JOIN generate_series(1, i.indnkeyatts) AS n
LEFT JOIN pg_attribute a ON a.attrelid = ct.oid AND a.attnum = i.indkey[n - 1]
WHERE
ct.relname = ?
AND ct.relkind = 'r'
a.attnum = ANY(i.indkey)
AND con.oid IS NULL
AND ct.relkind = 'r'
AND ct.relname = ?
ORDER BY index_name, column_position`, string(name))
if err != nil {
return nil, provider.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "no indices for table (%s) found", name)
@@ -268,10 +251,6 @@ ORDER BY index_name, column_position`, string(name))
type indexEntry struct {
columns []sqlschema.ColumnName
predicate *string
// keyDefs holds every key rendered by pg_get_indexdef, in key order; used
// for functional indexes where a key may be an expression like lower(x).
keyDefs []string
hasExpression bool
}
uniqueIndicesMap := make(map[string]*indexEntry)
@@ -281,45 +260,30 @@ ORDER BY index_name, column_position`, string(name))
indexName string
unique bool
primary bool
columnName *string
// starts from 1 and is unused in this function, this is to ensure that the column names are in the correct order
columnName string
// starts from 0 and is unused in this function, this is to ensure that the column names are in the correct order
columnPosition int
isExpression bool
keyDef string
predicate *string
)
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName, &columnPosition, &isExpression, &keyDef, &predicate); err != nil {
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName, &columnPosition, &predicate); err != nil {
return nil, err
}
if !unique {
continue
}
entry, ok := uniqueIndicesMap[indexName]
if !ok {
entry = &indexEntry{predicate: predicate}
uniqueIndicesMap[indexName] = entry
}
entry.keyDefs = append(entry.keyDefs, keyDef)
if isExpression {
entry.hasExpression = true
} else if columnName != nil {
entry.columns = append(entry.columns, sqlschema.ColumnName(*columnName))
if unique {
if _, ok := uniqueIndicesMap[indexName]; !ok {
uniqueIndicesMap[indexName] = &indexEntry{
columns: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
predicate: predicate,
}
} else {
uniqueIndicesMap[indexName].columns = append(uniqueIndicesMap[indexName].columns, sqlschema.ColumnName(columnName))
}
}
}
indices := make([]sqlschema.Index, 0)
for indexName, entry := range uniqueIndicesMap {
// functional partial indexes aren't representable (PartialUniqueIndex has no
// expressions); skip rather than misrepresent.
if entry.hasExpression && entry.predicate != nil {
provider.settings.Logger().WarnContext(ctx, "skipping functional partial unique index; not representable by sqlschema", slog.String("index", indexName), slog.String("table", string(name)))
continue
}
if entry.predicate != nil {
index := &sqlschema.PartialUniqueIndex{
TableName: name,
@@ -327,17 +291,6 @@ ORDER BY index_name, column_position`, string(name))
Where: *entry.predicate,
}
if index.Name() == indexName {
indices = append(indices, index)
} else {
indices = append(indices, index.Named(indexName))
}
} else if entry.hasExpression {
index := &sqlschema.UniqueIndexWithExpressions{
TableName: name,
Expressions: entry.keyDefs,
}
if index.Name() == indexName {
indices = append(indices, index)
} else {

View File

@@ -1,116 +0,0 @@
package postgressqlschema
import (
"context"
"database/sql/driver"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestGetIndices feeds GetIndices the exact rows the catalog query returns for a
// real table (captured from devenv postgres) and checks each unique-index shape
// is reconstructed: expression keys, partial (predicate), and plain columns.
func TestGetIndices(t *testing.T) {
// column order must match the SELECT in GetIndices.
columns := []string{"table_name", "index_name", "unique", "primary", "column_name", "column_position", "is_expression", "key_def", "predicate"}
testCases := []struct {
name string
table sqlschema.TableName
rows [][]driver.Value
want []sqlschema.Index
}{
{
name: "UniqueIndexWithExpressions",
table: "tag",
rows: [][]driver.Value{
{"tag", "uq_tag_a36c51df", true, false, "org_id", 1, false, "org_id", nil},
{"tag", "uq_tag_a36c51df", true, false, "kind", 2, false, "kind", nil},
{"tag", "uq_tag_a36c51df", true, false, nil, 3, true, "lower(key)", nil},
{"tag", "uq_tag_a36c51df", true, false, nil, 4, true, "lower(value)", nil},
},
want: []sqlschema.Index{
&sqlschema.UniqueIndexWithExpressions{
TableName: "tag",
Expressions: []string{"org_id", "kind", "lower(key)", "lower(value)"},
},
},
},
{
name: "PartialUniqueIndexes",
table: "service_account",
rows: [][]driver.Value{
{"service_account", "puq_service_account_email_org_id_471d6134", true, false, "email", 1, false, "email", "(status <> 'deleted'::text)"},
{"service_account", "puq_service_account_email_org_id_471d6134", true, false, "org_id", 2, false, "org_id", "(status <> 'deleted'::text)"},
{"service_account", "puq_service_account_name_org_id_471d6134", true, false, "name", 1, false, "name", "(status <> 'deleted'::text)"},
{"service_account", "puq_service_account_name_org_id_471d6134", true, false, "org_id", 2, false, "org_id", "(status <> 'deleted'::text)"},
},
want: []sqlschema.Index{
(&sqlschema.PartialUniqueIndex{
TableName: "service_account",
ColumnNames: []sqlschema.ColumnName{"email", "org_id"},
Where: "(status <> 'deleted'::text)",
}).Named("puq_service_account_email_org_id_471d6134"),
(&sqlschema.PartialUniqueIndex{
TableName: "service_account",
ColumnNames: []sqlschema.ColumnName{"name", "org_id"},
Where: "(status <> 'deleted'::text)",
}).Named("puq_service_account_name_org_id_471d6134"),
},
},
{
name: "PlainUniqueIndex",
table: "tag_relation",
rows: [][]driver.Value{
{"tag_relation", "uq_tag_relation_kind_resource_id_tag_id", true, false, "kind", 1, false, "kind", nil},
{"tag_relation", "uq_tag_relation_kind_resource_id_tag_id", true, false, "resource_id", 2, false, "resource_id", nil},
{"tag_relation", "uq_tag_relation_kind_resource_id_tag_id", true, false, "tag_id", 3, false, "tag_id", nil},
},
want: []sqlschema.Index{
&sqlschema.UniqueIndex{
TableName: "tag_relation",
ColumnNames: []sqlschema.ColumnName{"kind", "resource_id", "tag_id"},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
store := sqlstoretest.New(sqlstore.Config{Provider: "postgres"}, sqlmock.QueryMatcherRegexp)
rows := sqlmock.NewRows(columns)
for _, row := range testCase.rows {
rows.AddRow(row...)
}
store.Mock().ExpectQuery("pg_index").WillReturnRows(rows)
schema, err := New(context.Background(), instrumentationtest.New().ToProviderSettings(), sqlschema.Config{}, store)
require.NoError(t, err)
indices, err := schema.GetIndices(context.Background(), testCase.table)
require.NoError(t, err)
assert.Len(t, indices, len(testCase.want))
// GetIndices iterates a map, so match by name rather than slice order.
got := make(map[string]sqlschema.Index, len(indices))
for _, index := range indices {
got[index.Name()] = index
}
for _, want := range testCase.want {
actual, ok := got[want.Name()]
if !assert.True(t, ok, "expected index %s not returned", want.Name()) {
// so that other items in the for loop can also be checked.
continue
}
assert.True(t, want.Equals(actual), "index %s reconstructed differently", want.Name())
}
})
}
}

View File

@@ -293,8 +293,6 @@
// Forces subpath imports (@signozhq/ui/<component>) instead of the eagerly-loaded barrel
"signoz/no-css-module-bracket-access": "warn",
// Prevents bracket access on CSS modules (styles['kebab-case']) which fails with camelCaseOnly config
"signoz/no-dashboard-fetch-outside-root": "error",
// Forces useDashboardFetchRequired() outside the root V2 pages (allowlisted in overrides below)
"no-restricted-globals": [
"error",
{
@@ -560,18 +558,6 @@
"rules": {
"signoz/no-zustand-getstate-in-hooks": "off"
}
},
{
// Root V2 pages own the dashboard fetch lifecycle; useDashboardFetchRequired wraps it.
// Everywhere else must use useDashboardFetchRequired().
"files": [
"src/pages/DashboardPageV2/DashboardPageV2.tsx",
"src/pages/DashboardPageV2/PanelEditorPage/PanelEditorPage.tsx",
"src/pages/DashboardPageV2/DashboardContainer/hooks/useDashboardFetchRequired.ts"
],
"rules": {
"signoz/no-dashboard-fetch-outside-root": "off"
}
}
]
}

View File

@@ -25,7 +25,6 @@ You are operating within a constrained context window and strict system prompts.
- Never create barrel files.
- When writing new css, prefer CSS Modules
- Use ./docs/css-modules-guide.md as reference on how to write good CSS Modules.
- When writing code that could need authorization checks, read ./src/lib/authz/README.md
3. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:
- Run `pnpm tsgo --noEmit`

View File

@@ -1,3 +0,0 @@
export const IS_DEV = false;
export const IS_PROD = true;
export const MODE = 'test';

View File

@@ -1,88 +0,0 @@
// oxlint-disable-next-line no-restricted-imports
import * as React from 'react';
// In jsdom, AnimatePresence from motion/react keeps children in DOM during exit
// animations (awaiting rAF-driven completion that never fully runs in jsdom).
// This mock makes AnimatePresence render children immediately and makes motion.*
// elements render as their plain HTML equivalents without animation side-effects.
//
// IMPORTANT: motion component references are cached so React sees a stable
// component identity across re-renders and does not enter an infinite remount loop.
const MOTION_PROPS_TO_STRIP = new Set([
'initial',
'animate',
'exit',
'variants',
'transition',
'whileHover',
'whileTap',
'whileFocus',
'whileInView',
'layout',
'layoutId',
'onAnimationStart',
'onAnimationComplete',
]);
const cache = new Map<string, React.ComponentType>();
function getMotionComponent(tag: string): React.ComponentType {
if (!cache.has(tag)) {
const Component = React.forwardRef<HTMLElement, Record<string, unknown>>(
(props, ref) => {
const domProps: Record<string, unknown> = {};
for (const [k, v] of Object.entries(props)) {
if (!MOTION_PROPS_TO_STRIP.has(k)) {
domProps[k] = v;
}
}
return React.createElement(tag, { ...domProps, ref });
},
);
Component.displayName = `motion.${tag}`;
cache.set(tag, Component as unknown as React.ComponentType);
}
return cache.get(tag) as React.ComponentType;
}
const motionHandler: ProxyHandler<Record<string, React.ComponentType>> = {
get(_target, prop: string) {
return getMotionComponent(prop);
},
};
export const AnimatePresence: React.FC<{
children?: React.ReactNode;
mode?: string;
}> = ({ children }) => React.createElement(React.Fragment, null, children);
export const motion = new Proxy(
{} as Record<string, React.ComponentType>,
motionHandler,
);
export const useAnimation = (): Record<string, unknown> => ({
start: (): unknown => Promise.resolve(),
stop: (): unknown => undefined,
set: (): unknown => undefined,
});
export const useMotionValue = (
initial: unknown,
): { get: () => unknown; set: () => void } => ({
get: (): unknown => initial,
set: (): unknown => undefined,
});
export const useTransform = (): { get: () => number } => ({
get: (): number => 0,
});
export const useSpring = (v: unknown): unknown => v;
export const useScroll = (): { scrollY: { get: () => number } } => ({
scrollY: { get: (): number => 0 },
});
export default { motion, AnimatePresence };

View File

@@ -0,0 +1,29 @@
import { PropsWithChildren } from 'react';
type CommonProps = PropsWithChildren<{
className?: string;
minSize?: number;
maxSize?: number;
defaultSize?: number;
direction?: 'horizontal' | 'vertical';
autoSaveId?: string;
withHandle?: boolean;
}>;
export function ResizablePanelGroup({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizablePanel({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizableHandle({ className }: CommonProps): JSX.Element {
return <div className={className} />;
}

View File

@@ -20,7 +20,7 @@ const config: Config.InitialOptions = {
'\\.module\\.mjs$': '<rootDir>/__mocks__/cssMock.ts',
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
'^motion/react$': '<rootDir>/__mocks__/motionMock.tsx',
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
@@ -29,7 +29,6 @@ const config: Config.InitialOptions = {
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
'^@signozhq/icons$': '<rootDir>/__mocks__/signozhqIconsMock.tsx',
'^lib/env$': '<rootDir>/__mocks__/lib/env.ts',
'^test-mocks/(.*)$': '<rootDir>/__mocks__/$1',
'^react-syntax-highlighter/dist/esm/(.*)$':
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',

View File

@@ -49,6 +49,7 @@
"@sentry/vite-plugin": "5.3.0",
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.23",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
@@ -64,7 +65,7 @@
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.18.0",
"axios": "1.16.0",
"babel-jest": "^29.6.4",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
@@ -74,11 +75,10 @@
"crypto-js": "4.2.0",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.4.12",
"dompurify": "3.4.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"history": "4.10.1",
"html-to-image": "1.11.13",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
@@ -89,7 +89,7 @@
"lodash-es": "^4.17.21",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.16.0",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
@@ -127,7 +127,7 @@
"timestamp-nano": "^1.0.0",
"typescript": "5.9.3",
"uplot": "1.6.31",
"uuid": "14.0.1",
"uuid": "^8.3.2",
"vite": "npm:rolldown-vite@7.3.1",
"vite-plugin-html": "3.2.2",
"zod": "4.3.6",
@@ -199,9 +199,9 @@
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.97.3",
"sharp": "0.35.0",
"sharp": "0.34.5",
"stylelint": "17.7.0",
"svgo": "4.0.2",
"svgo": "4.0.1",
"ts-jest": "29.4.9",
"typescript-plugin-css-modules": "5.2.0",
"use-sync-external-store": "1.6.0",
@@ -222,17 +222,25 @@
"stylelint"
]
},
"overrides": {
"@babel/core@<=7.29.0": ">=7.29.6 <8",
"@istanbuljs/load-nyc-config>js-yaml": ">=4.2.0 <5",
"cookie@<0.7.0": ">=0.7.1 <1",
"dompurify@<=3.4.10": ">=3.4.11 <4",
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1 <0.29.0",
"js-cookie@<=3.0.5": ">=3.0.7 <4",
"js-yaml@>=4.0.0 <=4.1.1": ">=4.2.0 <5",
"prismjs@<1.30.0": ">=1.30.0 <2",
"react-router@>=6.7.0 <6.30.4": ">=6.30.4 <7",
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "4.1.1",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.6",
"brace-expansion": "^2.0.2",
"on-headers": "^1.1.0",
"js-cookie": "^3.0.7",
"tmp": "0.2.7",
"vite": "npm:rolldown-vite@7.3.1"
}
}
}

View File

@@ -1,48 +0,0 @@
/**
* Rule: no-dashboard-fetch-outside-root
*
* `useDashboardFetch` owns the V2 dashboard fetch and exposes its loading/error/refetch
* lifecycle. That lifecycle is a root-page concern: DashboardPageV2 and PanelEditorPage
* gate their subtree on a resolved dashboard before mounting anything below them.
*
* Every other component therefore renders inside an already-loaded subtree and must use
* `useDashboardFetchRequired`, which reuses the same react-query cache entry but guarantees
* a non-undefined dashboard (throwing otherwise) — so consumers don't re-handle states the
* root page already owns.
*
* This rule flags `useDashboardFetch` imports. It is disabled via an override in
* .oxlintrc.json for the two root pages and for useDashboardFetchRequired.ts (which wraps it).
*/
const FETCH_HOOK = 'useDashboardFetch';
export default {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow useDashboardFetch outside the root dashboard pages; use useDashboardFetchRequired instead',
category: 'Dashboard V2',
},
schema: [],
messages: {
useRequired:
'Use useDashboardFetchRequired() instead of useDashboardFetch — the latter is reserved for the root pages (DashboardPageV2, PanelEditorPage) that own the fetch lifecycle. Components below them render inside a loaded subtree and should assume a guaranteed dashboard.',
},
},
create(context) {
return {
ImportDeclaration(node) {
node.specifiers.forEach((specifier) => {
if (
specifier.type === 'ImportSpecifier' &&
specifier.imported.name === FETCH_HOOK
) {
context.report({ node: specifier, messageId: 'useRequired' });
}
});
},
};
},
};

View File

@@ -12,7 +12,6 @@ import noRawAbsolutePath from './rules/no-raw-absolute-path.mjs';
import noAntdComponents from './rules/no-antd-components.mjs';
import noSignozhqUiBarrel from './rules/no-signozhq-ui-barrel.mjs';
import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root.mjs';
export default {
meta: {
@@ -26,6 +25,5 @@ export default {
'no-antd-components': noAntdComponents,
'no-signozhq-ui-barrel': noSignozhqUiBarrel,
'no-css-module-bracket-access': noCssModuleBracketAccess,
'no-dashboard-fetch-outside-root': noDashboardFetchOutsideRoot,
},
};

2411
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +1,6 @@
blockExoticSubdeps: true
trustPolicy: no-downgrade
minimumReleaseAge: 2880 # 2d
minimumReleaseAgeStrict: true
minimumReleaseAgeExclude:
- '@signozhq/*'
minimumReleaseAgeStrict: true
# Security floors for vulnerable transitive deps. Where possible, targets are
# capped to avoid crossing breaking versions (major; and minor for 0.x).
# Some entries may still force a breaking bump when no safe release exists
# within the consumers constraint—see per-entry notes.
overrides:
# via: direct devDep @babel/core ^7.22.11 (+ babel plugin peers)
# remove: bump @babel/core in package.json to ^7.29.6
'@babel/core@<=7.29.0': '>=7.29.6 <8'
# via: jest > babel-plugin-istanbul > @istanbuljs/load-nyc-config@1.1.0 (js-yaml ^3.13.1)
# remove: blocked — 1.1.0 is latest and still depends on js-yaml 3.x
'@istanbuljs/load-nyc-config>js-yaml': '>=4.2.0 <5'
# via: msw@1.3.2 (devDep) > cookie ^0.4.2
# remove: upgrade msw to >=2 (ships cookie ^1). Do NOT open the cap: cookie >=1 is
# ESM-only and breaks msw under jest's CJS sandbox (kills every test suite)
cookie@<0.7.0: '>=0.7.1 <1'
# via: direct dep dompurify 3.4.0; @grafana/data@11.6.15 (3.4.0/3.2.4 exact);
# @monaco-editor/react > monaco-editor@0.55.1 (3.2.7 exact)
# remove: bump direct dep to 3.4.11; @grafana/data (latest 13.1.0) and
# monaco-editor (latest 0.55.1) still pin vulnerable versions — blocked
dompurify@<=3.4.10: '>=3.4.11 <4'
# via: rolldown-vite@7.3.1 (esbuild ^0.27.0); orval@8.9.1 (^0.27.4); ts-jest@29.4.9 (~0.27.4)
# remove: blocked on rolldown-vite (7.3.1 is latest, still ^0.27.0);
# orval >=8.20.0 and ts-jest >=29.4.11 already fixed on their side
esbuild@>=0.27.3 <0.28.1: '>=0.28.1 <0.29.0'
# via: react-use@17.5.1 (direct, js-cookie ^2.2.1); @grafana/data > react-use@17.6.0
# remove: bump react-use to >=17.6.1 (js-cookie ^3); @grafana/data side blocked
js-cookie@<=3.0.5: '>=3.0.7 <4'
# via: @orval/core@8.9.1 (devDep, js-yaml 4.1.1 EXACT pin — not deletable);
# json-schema-to-typescript@15 > @apidevtools/json-schema-ref-parser (^4.1.0)
# remove: upgrade orval to >=8.20.0 (drops js-yaml dependency entirely)
js-yaml@>=4.0.0 <=4.1.1: '>=4.2.0 <5'
# via: react-syntax-highlighter@15.5.0 (prismjs ^1.27.0 + refractor@3 ~1.27.0 tilde-pinned)
# remove: bump react-syntax-highlighter to >=16.1.1 (prismjs ^1.30.0, refractor@5)
prismjs@<1.30.0: '>=1.30.0 <2'
# via: direct dep react-router-dom-v5-compat@6.30.3 (react-router 6.30.3 exact)
# remove: bump react-router-dom-v5-compat to 6.30.4. Do NOT open the cap:
# react-router >=7 requires React 19 and breaks the app-wide CompatRouter
react-router@>=6.7.0 <6.30.4: '>=6.30.4 <7'
# via: msw@1.3.2 (devDep) > inquirer@8 > external-editor@3.1.0 (tmp ^0.0.33)
# remove: upgrade msw to >=2 (drops the inquirer/external-editor chain)
tmp@<0.2.6: '>=0.2.6 <0.3.0'
# via: jest > babel-plugin-macros > cosmiconfig@7 (yaml ^1.10.0);
# typescript-plugin-css-modules > postcss-load-config@3 (^1.10.2)
# remove: blocked — babel-plugin-macros 3.1.0 (latest) still uses cosmiconfig@7
yaml@>=1.0.0 <1.10.3: '>=1.10.3 <2'
trustPolicy: no-downgrade
trustPolicyExclude:
- 'semver@6.3.1 || 5.7.2'
blockExoticSubdeps: true

View File

@@ -62,7 +62,6 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
}
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
}

View File

@@ -87,7 +87,6 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
}
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
}

View File

@@ -134,8 +134,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
}
if (
(pathname.startsWith(`${ROUTES.AI_OBSERVABILITY_BASE}/`) ||
pathname === ROUTES.AI_OBSERVABILITY_BASE) &&
(pathname.startsWith(`${ROUTES.LLM_OBSERVABILITY_BASE}/`) ||
pathname === ROUTES.LLM_OBSERVABILITY_BASE) &&
!isAIObservabilityEnabled
) {
return <Redirect to={ROUTES.HOME} />;

View File

@@ -329,3 +329,10 @@ export const LLMObservabilityPage = Loadable(
/* webpackChunkName: "LLM Observability Page" */ 'pages/LLMObservability'
),
);
export const LLMObservabilityModelPricingPage = Loadable(
() =>
import(
/* webpackChunkName: "LLM Observability Model Pricing Page" */ 'pages/LLMObservabilityModelPricing'
),
);

View File

@@ -24,6 +24,7 @@ import {
LicensePage,
ListAllALertsPage,
LLMObservabilityPage,
LLMObservabilityModelPricingPage,
LiveLogs,
Login,
Logs,
@@ -514,24 +515,17 @@ const routes: AppRoutes[] = [
isPrivate: true,
},
{
path: ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
path: ROUTES.LLM_OBSERVABILITY_BASE,
exact: true,
component: LLMObservabilityPage,
key: 'AI_OBSERVABILITY_ATTRIBUTE_MAPPING',
key: 'LLM_OBSERVABILITY_BASE',
isPrivate: true,
},
{
path: ROUTES.AI_OBSERVABILITY_OVERVIEW,
path: ROUTES.LLM_OBSERVABILITY_MODEL_PRICING,
exact: true,
component: LLMObservabilityPage,
key: 'AI_OBSERVABILITY_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
exact: true,
component: LLMObservabilityPage,
key: 'AI_OBSERVABILITY_CONFIGURATION',
component: LLMObservabilityModelPricingPage,
key: 'LLM_OBSERVABILITY_MODEL_PRICING',
isPrivate: true,
},
];

View File

@@ -1,20 +0,0 @@
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
export function mockFieldsAPIsWithEmptyResponse(): void {
server.use(
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
),
),
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
),
),
);
}

View File

@@ -269,170 +269,3 @@ export function mockQueryRangeV5WithEventsResponse({
),
);
}
export type MockTracesOptions = {
offset?: number;
pageSize?: number;
hasMore?: boolean;
delay?: number;
customTraces?: Array<{
serviceName?: string;
name?: string;
durationNano?: number;
httpMethod?: string;
responseStatusCode?: string;
}>;
onReceiveRequest?: (
req: RestRequest,
) =>
| undefined
| void
| Omit<MockTracesOptions, 'onReceiveRequest'>
| Promise<Omit<MockTracesOptions, 'onReceiveRequest'>>
| Promise<void>;
};
const createTracesResponse = ({
offset = 0,
pageSize = 10,
hasMore = true,
customTraces,
}: MockTracesOptions): MetricRangePayloadV5 => {
const serviceNames = ['frontend', 'backend', 'database', 'api-gateway'];
const spanNames = [
'GET /api/users',
'POST /api/orders',
'SELECT * FROM users',
];
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE'];
const statusCodes = ['200', '201', '400', '404', '500'];
const rows = customTraces
? customTraces.map((trace, index) => {
const baseTimestamp = new Date('2026-04-21T17:54:33Z').getTime();
const currentTimestamp = new Date(baseTimestamp - index * 60000);
return {
timestamp: currentTimestamp.toISOString(),
data: {
serviceName:
trace.serviceName ?? serviceNames[index % serviceNames.length],
name: trace.name ?? spanNames[index % spanNames.length],
durationNano: trace.durationNano ?? 1000000 + index * 100000,
httpMethod: trace.httpMethod ?? httpMethods[index % httpMethods.length],
responseStatusCode:
trace.responseStatusCode ?? statusCodes[index % statusCodes.length],
traceID: `trace-id-${index}`,
spanID: `span-id-${index}`,
},
};
})
: Array.from(
{ length: hasMore ? pageSize : Math.ceil(pageSize / 2) },
(_, index) => {
const cumulativeIndex = offset + index;
const baseTimestamp = new Date('2026-04-21T17:54:33Z').getTime();
const currentTimestamp = new Date(baseTimestamp - cumulativeIndex * 60000);
return {
timestamp: currentTimestamp.toISOString(),
data: {
serviceName: serviceNames[cumulativeIndex % serviceNames.length],
name: spanNames[cumulativeIndex % spanNames.length],
durationNano: 1000000 + cumulativeIndex * 100000,
httpMethod: httpMethods[cumulativeIndex % httpMethods.length],
responseStatusCode: statusCodes[cumulativeIndex % statusCodes.length],
traceID: `trace-id-${cumulativeIndex}`,
spanID: `span-id-${cumulativeIndex}`,
},
};
},
);
return {
data: {
type: 'raw',
data: {
results: [
{
queryName: 'A',
nextCursor: hasMore ? 'next-cursor-token' : '',
rows,
},
],
},
meta: {
bytesScanned: 9682976,
durationMs: 295,
rowsScanned: 34198,
stepIntervals: { A: 170 },
},
},
};
};
export function mockQueryRangeV5WithTracesResponse({
hasMore = true,
offset = 0,
pageSize = 10,
delay = 0,
customTraces,
onReceiveRequest,
}: MockTracesOptions = {}): void {
server.use(
rest.post(QUERY_RANGE_URL, async (req, res, ctx) =>
res(
...(delay ? [ctx.delay(delay)] : []),
ctx.status(200),
ctx.json(
createTracesResponse(
(await onReceiveRequest?.(req)) ?? {
hasMore,
pageSize,
offset,
customTraces,
},
),
),
),
),
);
}
export function mockQueryRangeV5WithEmptyTraces(): void {
server.use(
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: {
type: 'raw',
data: {
results: [{ queryName: 'A', nextCursor: '', rows: [] }],
},
meta: {
bytesScanned: 0,
durationMs: 10,
rowsScanned: 0,
stepIntervals: {},
},
},
}),
),
),
);
}
export function mockQueryRangeV5WithKeyNotFoundError(): void {
server.use(
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
res(
ctx.status(400),
ctx.json({
error: {
code: 'invalid_input',
errors: [{ message: 'key not found' }],
},
}),
),
),
);
}

View File

@@ -38,11 +38,6 @@ import type {
GetPublicDashboard200,
GetPublicDashboardData200,
GetPublicDashboardDataPathParameters,
GetPublicDashboardDataV2200,
GetPublicDashboardDataV2PathParameters,
GetPublicDashboardPanelQueryRangeV2200,
GetPublicDashboardPanelQueryRangeV2Params,
GetPublicDashboardPanelQueryRangeV2PathParameters,
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
@@ -1804,232 +1799,6 @@ export const useLockDashboardV2 = <
> => {
return useMutation(getLockDashboardV2MutationOptions(options));
};
/**
* This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.
* @summary Get public dashboard data (v2)
*/
export const getPublicDashboardDataV2 = (
{ id }: GetPublicDashboardDataV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardDataV2200>({
url: `/api/v2/public/dashboards/${id}`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardDataV2QueryKey = ({
id,
}: GetPublicDashboardDataV2PathParameters) => {
return [`/api/v2/public/dashboards/${id}`] as const;
};
export const getGetPublicDashboardDataV2QueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetPublicDashboardDataV2QueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>
> = ({ signal }) => getPublicDashboardDataV2({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardDataV2QueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>
>;
export type GetPublicDashboardDataV2QueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get public dashboard data (v2)
*/
export function useGetPublicDashboardDataV2<
TData = Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardDataV2QueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get public dashboard data (v2)
*/
export const invalidateGetPublicDashboardDataV2 = async (
queryClient: QueryClient,
{ id }: GetPublicDashboardDataV2PathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardDataV2QueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint returns query range results for a panel of a v2-shape public dashboard. The panel is addressed by its key in spec.panels.
* @summary Get query range result (v2)
*/
export const getPublicDashboardPanelQueryRangeV2 = (
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardPanelQueryRangeV2200>({
url: `/api/v2/public/dashboards/${id}/panels/${key}/query_range`,
method: 'GET',
params,
signal,
});
};
export const getGetPublicDashboardPanelQueryRangeV2QueryKey = (
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
) => {
return [
`/api/v2/public/dashboards/${id}/panels/${key}/query_range`,
...(params ? [params] : []),
] as const;
};
export const getGetPublicDashboardPanelQueryRangeV2QueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>
> = ({ signal }) =>
getPublicDashboardPanelQueryRangeV2({ id, key }, params, signal);
return {
queryKey,
queryFn,
enabled: !!(id && key),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardPanelQueryRangeV2QueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>
>;
export type GetPublicDashboardPanelQueryRangeV2QueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get query range result (v2)
*/
export function useGetPublicDashboardPanelQueryRangeV2<
TData = Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardPanelQueryRangeV2QueryOptions(
{ id, key },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get query range result (v2)
*/
export const invalidateGetPublicDashboardPanelQueryRangeV2 = async (
queryClient: QueryClient,
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{
queryKey: getGetPublicDashboardPanelQueryRangeV2QueryKey(
{ id, key },
params,
),
},
options,
);
return queryClient;
};
/**
* Same as ListDashboardsV2 but personalized for the calling user: each dashboard carries the caller's `pinned` state, and pinned dashboards float to the top of the requested ordering. Supports the same filter DSL, sort, order, and pagination.
* @summary List dashboards for the current user (v2)

View File

@@ -21,7 +21,6 @@ import type {
GetChecks200,
GetChecksParams,
InframonitoringtypesPostableClustersDTO,
InframonitoringtypesPostableContainersDTO,
InframonitoringtypesPostableDaemonSetsDTO,
InframonitoringtypesPostableDeploymentsDTO,
InframonitoringtypesPostableHostsDTO,
@@ -32,7 +31,6 @@ import type {
InframonitoringtypesPostableStatefulSetsDTO,
InframonitoringtypesPostableVolumesDTO,
ListClusters200,
ListContainers200,
ListDaemonSets200,
ListDeployments200,
ListHosts200,
@@ -136,7 +134,7 @@ export const invalidateGetChecks = async (
};
/**
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Clusters for Infra Monitoring
*/
export const listClusters = (
@@ -219,7 +217,7 @@ export const useListClusters = <
return useMutation(getListClustersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes, the number of nodes running at least one ready daemon pod) and misscheduledNodes (k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon pod but not supposed to) — note these are node counts, not pod counts. It also reports per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
* @summary List DaemonSets for Infra Monitoring
*/
export const listDaemonSets = (
@@ -302,7 +300,7 @@ export const useListDaemonSets = <
return useMutation(getListDaemonSetsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* @summary List Deployments for Infra Monitoring
*/
export const listDeployments = (
@@ -468,7 +466,7 @@ export const useListHosts = <
return useMutation(getListHostsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status); note podCountsByStatus.failed (current pod status) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* @summary List Jobs for Infra Monitoring
*/
export const listJobs = (
@@ -551,90 +549,7 @@ export const useListJobs = <
return useMutation(getListJobsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes containers with key kubeletstats metrics: CPU usage (cores), CPU request/limit utilization, memory working set, and memory request/limit utilization. Each container also reports health signals from the k8s_cluster receiver: status (kubectl-style display status derived from k8s.container.status.state + k8s.container.status.reason), restarts (absolute count from k8s.container.restarts), and ready (ready/not_ready from k8s.container.ready). The row identity is (k8s.pod.uid, k8s.container.name), stable across container restarts. Each container includes metadata attributes (k8s.container.name, k8s.pod.name, container.image.name, container.image.tag, k8s.namespace.name, k8s.node.name, k8s.cluster.name, and workload owner such as deployment/statefulset/daemonset/job). The response type is 'list' for the default (k8s.pod.uid, k8s.container.name) grouping (each row is one container with its current status and ready state) or 'grouped_list' for custom groupBy keys (each row aggregates containers in the group with per-status counts under containerCountsByStatus, per-readiness counts under containerCountsByReady, and restarts as the group sum). Status requires the optional k8s.container.status.state and k8s.container.status.reason metrics; when either is missing, status is omitted and a warning is returned while restarts and ready are still computed. Supports filtering via a filter expression, custom groupBy, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, cpuRequestUtilization, cpuLimitUtilization, memory, memoryRequestUtilization, memoryLimitUtilization) and restarts return -1 as a sentinel when no data is available for that field.
* @summary List Kubernetes Containers for Infra Monitoring
*/
export const listContainers = (
inframonitoringtypesPostableContainersDTO?: BodyType<InframonitoringtypesPostableContainersDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListContainers200>({
url: `/api/v2/infra_monitoring/kube_containers`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: inframonitoringtypesPostableContainersDTO,
signal,
});
};
export const getListContainersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
> => {
const mutationKey = ['listContainers'];
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 listContainers>>,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> }
> = (props) => {
const { data } = props ?? {};
return listContainers(data);
};
return { mutationFn, ...mutationOptions };
};
export type ListContainersMutationResult = NonNullable<
Awaited<ReturnType<typeof listContainers>>
>;
export type ListContainersMutationBody =
| BodyType<InframonitoringtypesPostableContainersDTO>
| undefined;
export type ListContainersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List Kubernetes Containers for Infra Monitoring
*/
export const useListContainers = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
> => {
return useMutation(getListContainersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* @summary List Namespaces for Infra Monitoring
*/
export const listNamespaces = (
@@ -717,7 +632,7 @@ export const useListNamespaces = <
return useMutation(getListNamespacesMutationOptions(options));
};
/**
* 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 podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } for pods scheduled on the listed nodes, reflecting each pod's latest kubectl-style display status). 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 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.
* 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 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 = (
@@ -800,7 +715,7 @@ export const useListNodes = <
return useMutation(getListNodesMutationOptions(options));
};
/**
* 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 status (kubectl-style display status such as Running/Pending/CrashLoopBackOff, or 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 status) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-status counts under podCountsByStatus: { pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } derived from each pod's latest kubectl-style display status in the window). Also reports 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 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 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 = (
@@ -966,7 +881,7 @@ export const useListVolumes = <
return useMutation(getListVolumesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* @summary List StatefulSets for Infra Monitoring
*/
export const listStatefulSets = (

View File

@@ -23,7 +23,6 @@ import type {
GetLLMPricingRulePathParameters,
ListLLMPricingRules200,
ListLLMPricingRulesParams,
ListUnmappedLLMModels200,
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -394,87 +393,3 @@ export const invalidateGetLLMPricingRule = async (
return queryClient;
};
/**
* Returns models seen in the last hour of trace data (gen_ai.request.model) that no pricing rule pattern matches, so the user can add them to an existing rule or create a new one.
* @summary List unmapped models
*/
export const listUnmappedLLMModels = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListUnmappedLLMModels200>({
url: `/api/v1/llm_pricing_rules/unmapped_models`,
method: 'GET',
signal,
});
};
export const getListUnmappedLLMModelsQueryKey = () => {
return [`/api/v1/llm_pricing_rules/unmapped_models`] as const;
};
export const getListUnmappedLLMModelsQueryOptions = <
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListUnmappedLLMModelsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listUnmappedLLMModels>>
> = ({ signal }) => listUnmappedLLMModels(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListUnmappedLLMModelsQueryResult = NonNullable<
Awaited<ReturnType<typeof listUnmappedLLMModels>>
>;
export type ListUnmappedLLMModelsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List unmapped models
*/
export function useListUnmappedLLMModels<
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListUnmappedLLMModelsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List unmapped models
*/
export const invalidateListUnmappedLLMModels = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListUnmappedLLMModelsQueryKey() },
options,
);
return queryClient;
};

View File

@@ -22,16 +22,12 @@ import type {
CreateServiceAccountKey201,
CreateServiceAccountKeyPathParameters,
CreateServiceAccountRole201,
CreateServiceAccountRoleDeprecated201,
CreateServiceAccountRoleDeprecatedPathParameters,
CreateServiceAccountRolePathParameters,
DeleteServiceAccountPathParameters,
DeleteServiceAccountRoleDeprecatedPathParameters,
DeleteServiceAccountRolePathParameters,
GetMyServiceAccount200,
GetServiceAccount200,
GetServiceAccountPathParameters,
GetServiceAccountRole200,
GetServiceAccountRolePathParameters,
GetServiceAccountRoles200,
GetServiceAccountRolesPathParameters,
ListServiceAccountKeys200,
@@ -39,7 +35,6 @@ import type {
ListServiceAccounts200,
RenderErrorResponseDTO,
RevokeServiceAccountKeyPathParameters,
ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
ServiceaccounttypesPostableFactorAPIKeyDTO,
ServiceaccounttypesPostableServiceAccountDTO,
ServiceaccounttypesPostableServiceAccountRoleDTO,
@@ -51,272 +46,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint assigns a role to a service account
* @summary Create service account role
*/
export const createServiceAccountRole = (
serviceaccounttypesPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateServiceAccountRole201>({
url: `/api/v1/service_account_roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesPostableServiceAccountRoleDTO,
signal,
});
};
export const getCreateServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
> => {
const mutationKey = ['createServiceAccountRole'];
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 createServiceAccountRole>>,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> }
> = (props) => {
const { data } = props ?? {};
return createServiceAccountRole(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRole>>
>;
export type CreateServiceAccountRoleMutationBody =
| BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>
| undefined;
export type CreateServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create service account role
*/
export const useCreateServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
> => {
return useMutation(getCreateServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint revokes a role from a service account
* @summary Delete service account role
*/
export const deleteServiceAccountRole = (
{ id }: DeleteServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/service_account_roles/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
const mutationKey = ['deleteServiceAccountRole'];
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 deleteServiceAccountRole>>,
{ pathParams: DeleteServiceAccountRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteServiceAccountRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRole>>
>;
export type DeleteServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete service account role
*/
export const useDeleteServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
return useMutation(getDeleteServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint gets an existing service account role
* @summary Get service account role
*/
export const getServiceAccountRole = (
{ id }: GetServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetServiceAccountRole200>({
url: `/api/v1/service_account_roles/${id}`,
method: 'GET',
signal,
});
};
export const getGetServiceAccountRoleQueryKey = ({
id,
}: GetServiceAccountRolePathParameters) => {
return [`/api/v1/service_account_roles/${id}`] as const;
};
export const getGetServiceAccountRoleQueryOptions = <
TData = Awaited<ReturnType<typeof getServiceAccountRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetServiceAccountRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetServiceAccountRoleQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getServiceAccountRole>>
> = ({ signal }) => getServiceAccountRole({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetServiceAccountRoleQueryResult = NonNullable<
Awaited<ReturnType<typeof getServiceAccountRole>>
>;
export type GetServiceAccountRoleQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get service account role
*/
export function useGetServiceAccountRole<
TData = Awaited<ReturnType<typeof getServiceAccountRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetServiceAccountRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceAccountRoleQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get service account role
*/
export const invalidateGetServiceAccountRole = async (
queryClient: QueryClient,
{ id }: GetServiceAccountRolePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceAccountRoleQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint lists the service accounts for an organisation
* @summary List service accounts
@@ -1255,46 +984,45 @@ export const invalidateGetServiceAccountRoles = async (
/**
* This endpoint assigns a role to a service account
* @deprecated
* @summary Create service account role
*/
export const createServiceAccountRoleDeprecated = (
{ id }: CreateServiceAccountRoleDeprecatedPathParameters,
serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>,
export const createServiceAccountRole = (
{ id }: CreateServiceAccountRolePathParameters,
serviceaccounttypesPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateServiceAccountRoleDeprecated201>({
return GeneratedAPIInstance<CreateServiceAccountRole201>({
url: `/api/v1/service_accounts/${id}/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
data: serviceaccounttypesPostableServiceAccountRoleDTO,
signal,
});
};
export const getCreateServiceAccountRoleDeprecatedMutationOptions = <
export const getCreateServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
> => {
const mutationKey = ['createServiceAccountRoleDeprecated'];
const mutationKey = ['createServiceAccountRole'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -1304,66 +1032,62 @@ export const getCreateServiceAccountRoleDeprecatedMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return createServiceAccountRoleDeprecated(pathParams, data);
return createServiceAccountRole(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateServiceAccountRoleDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>
export type CreateServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRole>>
>;
export type CreateServiceAccountRoleDeprecatedMutationBody =
| BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>
export type CreateServiceAccountRoleMutationBody =
| BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>
| undefined;
export type CreateServiceAccountRoleDeprecatedMutationError =
export type CreateServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create service account role
*/
export const useCreateServiceAccountRoleDeprecated = <
export const useCreateServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
> => {
return useMutation(
getCreateServiceAccountRoleDeprecatedMutationOptions(options),
);
return useMutation(getCreateServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint revokes a role from service account
* @deprecated
* @summary Delete service account role
*/
export const deleteServiceAccountRoleDeprecated = (
{ id, rid }: DeleteServiceAccountRoleDeprecatedPathParameters,
export const deleteServiceAccountRole = (
{ id, rid }: DeleteServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
@@ -1373,23 +1097,23 @@ export const deleteServiceAccountRoleDeprecated = (
});
};
export const getDeleteServiceAccountRoleDeprecatedMutationOptions = <
export const getDeleteServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
const mutationKey = ['deleteServiceAccountRoleDeprecated'];
const mutationKey = ['deleteServiceAccountRole'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -1399,47 +1123,44 @@ export const getDeleteServiceAccountRoleDeprecatedMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters }
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
{ pathParams: DeleteServiceAccountRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteServiceAccountRoleDeprecated(pathParams);
return deleteServiceAccountRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteServiceAccountRoleDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>
export type DeleteServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRole>>
>;
export type DeleteServiceAccountRoleDeprecatedMutationError =
export type DeleteServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Delete service account role
*/
export const useDeleteServiceAccountRoleDeprecated = <
export const useDeleteServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
return useMutation(
getDeleteServiceAccountRoleDeprecatedMutationOptions(options),
);
return useMutation(getDeleteServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint gets my service account

File diff suppressed because it is too large Load Diff

View File

@@ -30,10 +30,8 @@ import type {
RenderErrorResponseDTO,
SpantypesPostableSpanMapperDTO,
SpantypesPostableSpanMapperGroupDTO,
SpantypesPostableSpanMapperTestDTO,
SpantypesUpdatableSpanMapperDTO,
SpantypesUpdatableSpanMapperGroupDTO,
TestSpanMappers200,
UpdateSpanMapperGroupPathParameters,
UpdateSpanMapperPathParameters,
} from '../sigNoz.schemas';
@@ -782,86 +780,3 @@ export const useUpdateSpanMapper = <
> => {
return useMutation(getUpdateSpanMapperMutationOptions(options));
};
/**
* Tests how span mappers would transform sample spans
* @summary Test span mappers against sample spans
*/
export const testSpanMappers = (
spantypesPostableSpanMapperTestDTO?: BodyType<SpantypesPostableSpanMapperTestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<TestSpanMappers200>({
url: `/api/v1/span_mapper_groups/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableSpanMapperTestDTO,
signal,
});
};
export const getTestSpanMappersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
> => {
const mutationKey = ['testSpanMappers'];
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 testSpanMappers>>,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> }
> = (props) => {
const { data } = props ?? {};
return testSpanMappers(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestSpanMappersMutationResult = NonNullable<
Awaited<ReturnType<typeof testSpanMappers>>
>;
export type TestSpanMappersMutationBody =
| BodyType<SpantypesPostableSpanMapperTestDTO>
| undefined;
export type TestSpanMappersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Test span mappers against sample spans
*/
export const useTestSpanMappers = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
> => {
return useMutation(getTestSpanMappersMutationOptions(options));
};

View File

@@ -19,15 +19,12 @@ import type {
import type {
AuthtypesPostableUserDTO,
AuthtypesPostableUserRoleDTO,
CreateInvite201,
CreateResetPasswordToken201,
CreateResetPasswordTokenPathParameters,
CreateUser201,
CreateUserRole201,
DeleteUserDeprecatedPathParameters,
DeleteUserPathParameters,
DeleteUserRolePathParameters,
GetMyUser200,
GetMyUserDeprecated200,
GetResetPasswordToken200,
@@ -40,8 +37,6 @@ import type {
GetUserDeprecated200,
GetUserDeprecatedPathParameters,
GetUserPathParameters,
GetUserRole200,
GetUserRolePathParameters,
GetUsersByRoleID200,
GetUsersByRoleIDPathParameters,
ListUsers200,
@@ -1159,267 +1154,6 @@ export const invalidateGetUsersByRoleID = async (
return queryClient;
};
/**
* This endpoint assigns a role to a user
* @summary Create user role
*/
export const createUserRole = (
authtypesPostableUserRoleDTO?: BodyType<AuthtypesPostableUserRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateUserRole201>({
url: `/api/v2/user_roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: authtypesPostableUserRoleDTO,
signal,
});
};
export const getCreateUserRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
> => {
const mutationKey = ['createUserRole'];
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 createUserRole>>,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> }
> = (props) => {
const { data } = props ?? {};
return createUserRole(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateUserRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createUserRole>>
>;
export type CreateUserRoleMutationBody =
| BodyType<AuthtypesPostableUserRoleDTO>
| undefined;
export type CreateUserRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create user role
*/
export const useCreateUserRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
> => {
return useMutation(getCreateUserRoleMutationOptions(options));
};
/**
* This endpoint revokes a role from a user
* @summary Delete user role
*/
export const deleteUserRole = (
{ id }: DeleteUserRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/user_roles/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteUserRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
> => {
const mutationKey = ['deleteUserRole'];
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 deleteUserRole>>,
{ pathParams: DeleteUserRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteUserRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteUserRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUserRole>>
>;
export type DeleteUserRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete user role
*/
export const useDeleteUserRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
> => {
return useMutation(getDeleteUserRoleMutationOptions(options));
};
/**
* This endpoint gets an existing user role
* @summary Get user role
*/
export const getUserRole = (
{ id }: GetUserRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetUserRole200>({
url: `/api/v2/user_roles/${id}`,
method: 'GET',
signal,
});
};
export const getGetUserRoleQueryKey = ({ id }: GetUserRolePathParameters) => {
return [`/api/v2/user_roles/${id}`] as const;
};
export const getGetUserRoleQueryOptions = <
TData = Awaited<ReturnType<typeof getUserRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetUserRoleQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getUserRole>>> = ({
signal,
}) => getUserRole({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetUserRoleQueryResult = NonNullable<
Awaited<ReturnType<typeof getUserRole>>
>;
export type GetUserRoleQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get user role
*/
export function useGetUserRole<
TData = Awaited<ReturnType<typeof getUserRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetUserRoleQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get user role
*/
export const invalidateGetUserRole = async (
queryClient: QueryClient,
{ id }: GetUserRolePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetUserRoleQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint lists all users for the organization
* @summary List users v2
@@ -2131,7 +1865,6 @@ export const invalidateGetRolesByUserID = async (
/**
* This endpoint assigns the role to the user roles by user id
* @deprecated
* @summary Set user roles
*/
export const setRoleByUserID = (
@@ -2203,7 +1936,6 @@ export type SetRoleByUserIDMutationBody =
export type SetRoleByUserIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Set user roles
*/
export const useSetRoleByUserID = <
@@ -2232,7 +1964,6 @@ export const useSetRoleByUserID = <
};
/**
* This endpoint removes a role from the user by user id and role id
* @deprecated
* @summary Remove a role from user
*/
export const removeUserRoleByUserIDAndRoleID = (
@@ -2291,7 +2022,6 @@ export type RemoveUserRoleByUserIDAndRoleIDMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Remove a role from user
*/
export const useRemoveUserRoleByUserIDAndRoleID = <

View File

@@ -22,7 +22,6 @@ export const getKeySuggestions = (
fieldContext = '',
fieldDataType = '',
signalSource = '',
metricNamespace = '',
} = props;
const encodedSignal = encodeURIComponent(signal);
@@ -31,9 +30,8 @@ export const getKeySuggestions = (
const encodedFieldContext = encodeURIComponent(fieldContext);
const encodedFieldDataType = encodeURIComponent(fieldDataType);
const encodedSource = encodeURIComponent(signalSource);
const encodedMetricNamespace = encodeURIComponent(metricNamespace);
return axios.get(
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}&metricNamespace=${encodedMetricNamespace}`,
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
);
};

View File

@@ -1,50 +1,26 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError, AxiosResponse } from 'axios';
import { AxiosError } from 'axios';
import { ErrorV2Resp } from 'types/api';
import { ExportRawDataProps } from 'types/api/exportRawData/getExportRawData';
export interface FetchExportDataProps extends ExportRawDataProps {
signal?: AbortSignal;
}
async function postExportRawData({
format,
body,
signal,
}: FetchExportDataProps): Promise<AxiosResponse<Blob>> {
return axios.post<Blob>(
`export_raw_data?format=${encodeURIComponent(format)}`,
body,
{
responseType: 'blob',
decompress: true,
headers: {
Accept: 'application/octet-stream',
'Content-Type': 'application/json',
},
timeout: 0,
signal,
},
);
}
/**
* Fetches a single export_raw_data page and returns it as a Blob.
* Callers own retry/cancel/error UX.
*/
export async function fetchExportData(
props: FetchExportDataProps,
): Promise<Blob> {
const response = await postExportRawData(props);
return response.data;
}
export const downloadExportData = async (
props: ExportRawDataProps,
): Promise<void> => {
try {
const response = await postExportRawData(props);
const response = await axios.post<Blob>(
`export_raw_data?format=${encodeURIComponent(props.format)}`,
props.body,
{
responseType: 'blob',
decompress: true,
headers: {
Accept: 'application/octet-stream',
'Content-Type': 'application/json',
},
timeout: 0,
},
);
if (response.status !== 200) {
throw new Error(

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1287b1" viewBox="0 0 24 24"><title>Apache Cassandra</title><path d="M10.374 10.53a3 3 0 0 1-.428-.222l.555.143c0 .02-.01.036-.01.055l-.117.025zm-.283 1.506-.315.253.852-1.079-1.078.391c.002.017.009.033.009.05a.57.57 0 0 1-.184.42q.154.327.375.616a3.2 3.2 0 0 1 .34-.651zm.717-2.347-.652-.82a.43.43 0 0 1-.506.162c-.054.073-.083.162-.13.24l1.258.463zm-1.666.444c-.07.314-.087.637-.05.956a.57.57 0 0 1 .451.475l.946-.606c-.067-.022-.126-.06-.191-.088l-1.119-.08.64-.14a3.2 3.2 0 0 1-.668-.554zM20.1 11.648c-.164.202.833 1.022.833 1.022s-1.654-1.022-2.234-.72c-.278.144.574.811 1.175 1.242-.428-.274-.982-.571-1.175-.408-.328.277 1.565 2.549 1.565 2.549s-2.145-2.322-2.36-2.209c-.214.114.593 1.224.593 1.224s-1.06-1.16-1.35-.959c-.29.202 1.514 3.218 1.514 3.218s-1.956-3.091-2.763-2.574c1.268 2.782.795 3.18.795 3.18s-.162-2.839-1.742-2.764c-.795.038.379 2.12.379 2.12s-1.08-1.902-1.8-1.864c1.326 2.51.854 3.53.854 3.53s.219-2.143-1.58-3.336c.682.606-.427 3.336-.427 3.336s.976-4.023-.719-3.256c-.268.121-.019 2.007-.019 2.007s-.34-2.158-.851-2.045c-.298.066-1.893 2.99-1.893 2.99s1.306-3.16.908-3.027c-.29.096-.833 1.4-.833 1.4s.265-1.287 0-1.363c-.264-.075-1.74 1.363-1.74 1.363s1.097-1.287.908-1.552c-.287-.402-.623-.42-1.022-.265-.581.226-1.363 1.287-1.363 1.287s.78-1.074.643-1.476c-.219-.647-2.46 1.249-2.46 1.249s1.325-1.25 1.022-1.514c-.303-.265-1.947-.183-2.46-.185-1.515-.004-2.039-.36-2.498-.724 1.987.997 3.803-.151 6.094.494l.21.06c-1.3-.558-2.144-1.378-2.226-2.354-.036-.416.074-.827.297-1.222.619-.4 1.29-.773 2.06-1.095a4 4 0 0 0-.064.698c0 2.44 2.203 4.417 4.92 4.417s4.92-1.977 4.92-4.417c0-.45-.083-.881-.223-1.29 1.431.404 2.45.968 3.132 1.335.022.092.045.184.053.279.024.274-.018.547-.11.814.095-.147.198-.288.28-.445.367-.997 1.855.227 1.855.227s-1.085-.454-1.06-.24c.026.215 1.628.96 1.628.96s-1.45-.455-1.362-.114c.088.34 1.817 1.703 1.817 1.703s-1.956-1.489-2.12-1.287zm-7.268 2.65.042-.008-.06.01zM9.256 9.753c.12.13.26.234.396.343l.927-.029-1.064-.788c-.093.154-.195.303-.26.474Zm10.62 3.44c.3.215.54.373.54.373s-.24-.181-.54-.374zM7.507 8.617c-.14.229-.214.492-.215.76a3.99 3.99 0 0 0 2.358 3.64q0-.008.003-.014a3.2 3.2 0 0 1-.58-.788c-.648.099-.926-.794-.336-1.08a3.2 3.2 0 0 1 .138-1.388 3.16 3.16 0 0 1-.52-1.36q-.444.105-.848.23m1.488.82q.163-.36.402-.661a.435.435 0 0 1 .568-.557c.077-.059.166-.099.248-.15a16 16 0 0 0-1.727.284c.114.388.272.76.509 1.084m2.285 3.928c1.4 0 2.633-.723 3.344-1.816a3.4 3.4 0 0 0-1.265-.539l-.297-.023.916.9-1.197-.467.704 1.078-1.074-.832-.012.006.347 1.278-.596-1.134-.098 1.33-.401-1.326-.472 1.261.114-1.359-.015-.008-.814 1.154.286-1.067c-.34.322-.605.713-.781 1.146q.143.153.303.29.484.126 1.008.128m10.145-4.434c.971-.567 1.716-1.955 1.716-1.955s-1.893 1.955-3.205 1.665c1.186-.934 1.766-2.549 1.766-2.549s-1.506 2.325-2.448 2.423c1.086-.959 1.54-2.322 1.54-2.322s-1.237 1.817-2.196 1.944c1.287-1.161 1.338-1.893 1.338-1.893s-1.781 2.302-2.499 1.943c.858-.934 1.439-2.12 1.439-2.12s-1.489 2.019-1.893 1.69c-.277-.05.454-.958.454-.958s-.908.807-1.16.606c.454-.278 1.236-1.64 1.236-1.64S16 7.505 15.621 7.304l.731-1.483s-.73 1.483-1.715 1.23c.454-.58.63-1.112.63-1.112s-.756 1.213-1.69.885c-.22-.077.273-.635.273-.635s-.626.61-1.055.534c-.43-.076.025-.858.025-.858s-.757 1.186-.908 1.136.075-.833.075-.833-.555.908-.858.858 0-.934 0-.934-.328.984-.58.909c-.252-.076-.303-.656-.303-.656s-.068.788-.429.858c-2.725.53-5.728 1.69-9.489 5.45C3.887 10.738 5.3 7.91 11.962 7.659c5.044-.191 7.399 2.137 8.177 2.17C22.51 9.93 24 7.633 24 7.633s-1.489 1.716-2.574 1.3zm-7.74.872-.608.464v.001l.054.003a3.4 3.4 0 0 0 .554-.468m1.583-.426c0-.536-.237-.929-.594-1.217a3.2 3.2 0 0 1-.165.825.393.393 0 0 1-.328.681c-.154.233-.34.445-.549.63l.661.034-.995.237c-.025.018-.045.041-.07.058a3.2 3.2 0 0 1 1.536.691c.32-.574.504-1.235.504-1.94zM10.99 7.996a3.5 3.5 0 0 0-.785.46.43.43 0 0 1-.013.357l.885.643.023-.016-.36-1.262.627 1.12c.018-.006.04-.006.058-.011l-.02-1.251.398 1.163.477-1.15.016 1.268.012.007.713-1.005-.363 1.218.009.01 1.04-.69-.759 1.05.002.005.95-.34.041-.045a.395.395 0 0 1 .394-.632 3.4 3.4 0 0 0 .27-.784 14 14 0 0 0-2.798-.168c-.286.011-.55.033-.817.053"/></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="48 -2.25 262.5 364"><defs><style>.cls-1{fill:#326ce5}.cls-3{fill:none}</style></defs><path d="M59.724 97.778a10.183 10.183 0 0 1 0-17.073l114.07-74.16a10.18 10.18 0 0 1 11.1 0l114.07 74.16a10.183 10.183 0 0 1 0 17.073l-114.07 74.16a10.18 10.18 0 0 1-11.1 0z" class="cls-1"/><path fill="#c1d2f7" d="M197.356 110.866h7.912a6.003 6.003 0 0 0 5.2-9.005l-25.924-44.902a6.004 6.004 0 0 0-10.399 0l-25.924 44.902a6.003 6.003 0 0 0 5.2 9.005h7.912a6.003 6.003 0 0 1 6.004 6.003v51.257l5.31 3.452a12.29 12.29 0 0 0 13.395 0l5.31-3.452v-51.257a6.003 6.003 0 0 1 6.004-6.003"/><path d="M173.793 353.271a10.1 10.1 0 0 0 3.457 1.402 119 119 0 0 0-6.623-3.46zm-6.456-161.195-11.316 7.356a111 111 0 0 0 11.316 6.6zm24.015 23.716c9.739 3.115 19.813 5.648 30.11 8.186 10.92 2.692 21.973 5.427 32.818 9.009l-35.416-23.025a216 216 0 0 1-27.512-9.41zm0 62.191v.586c0 2.57-2.688 4.652-6.003 4.652H173.34c-3.315 0-6.003-2.083-6.003-4.652v-6.605c-24.906-6.402-49.873-14.485-70.86-33.82l-14.529 9.445c22.468 22.05 50.28 28.933 79.715 36.189 27.178 6.698 55.18 13.651 78.685 33.442l14.68-9.544c-18.532-16.504-40.41-23.687-63.676-29.693M93.315 300.95l38.908 25.295c20.064 5.113 40.2 11.248 58.31 23.36l14.901-9.687c-19.752-13.672-42.55-19.313-66.419-25.197-15.287-3.767-30.835-7.616-45.7-13.771m74.022-82.254a124.3 124.3 0 0 1-21.534-12.62l-14.805 9.625a124.5 124.5 0 0 0 36.339 18.708zm51.41 16.294c-9.106-2.245-18.304-4.524-27.395-7.299v13.682q5.051 1.286 10.177 2.538c29.934 7.379 60.888 15.01 85.708 39.829.61.61 1.162 1.24 1.752 1.857l9.974-6.484a10.1 10.1 0 0 0 3.475-3.8 101 101 0 0 0-3.289-3.486c-22.585-22.585-50.669-29.508-80.402-36.837m-19.933 19.933q-3.722-.918-7.462-1.855v13.21c25.817 6.561 51.803 14.688 73.455 35.04l14.521-9.44-.11-.118c-22.586-22.585-50.67-29.509-80.404-36.837m-31.477-8.587c-16.28-5.278-32.118-12.397-46.344-24.131l-14.682 9.545c17.87 15.557 38.784 22.673 61.026 28.5z" class="cls-3"/><path d="M73.24 254.96c-.348-.348-.658-.71-1-1.06l-12.514 8.136a10 10 0 0 0-1.644 1.397c1.053 1.157 2.115 2.31 3.245 3.44 22.585 22.585 50.67 29.509 80.403 36.837 25.343 6.247 51.401 12.717 73.865 29.603l14.806-9.626c-20.865-16.42-45.53-22.509-71.453-28.898-29.934-7.379-60.888-15.01-85.708-39.83" class="cls-3"/><path d="m218.864 209.962-27.512-17.886v8.476a216 216 0 0 0 27.512 9.41m-27.512 5.83v11.9c9.091 2.774 18.289 5.053 27.395 7.298 29.733 7.329 57.817 14.252 80.402 36.837a101 101 0 0 1 3.29 3.486 10.193 10.193 0 0 0-3.476-13.277l-44.683-29.05c-10.845-3.58-21.898-6.316-32.818-9.008-10.297-2.538-20.371-5.071-30.11-8.186m-24.015-9.76a111 111 0 0 1-11.316-6.6l-10.218 6.644a124.3 124.3 0 0 0 21.534 12.62zm34.192 37.88q-5.125-1.264-10.177-2.539v11.695q3.737.938 7.462 1.855c29.733 7.328 57.818 14.252 80.403 36.837.039.039.073.079.111.118l9.66-6.28c-.589-.618-1.142-1.248-1.751-1.858-24.82-24.82-55.774-32.45-85.708-39.829m-34.192-9.503a124.5 124.5 0 0 1-36.34-18.708l-10.004 6.504c14.226 11.734 30.064 18.853 46.344 24.13zm0 25.842c-22.242-5.828-43.156-12.944-61.026-28.5l-9.834 6.392c20.987 19.336 45.954 27.42 70.86 33.821zm24.015 17.732c23.266 6.006 45.144 13.19 63.675 29.693l9.78-6.358c-21.652-20.352-47.638-28.479-73.455-35.04zM81.948 247.59l-9.707 6.311c.341.35.652.712 1 1.06 24.82 24.82 55.773 32.45 85.707 39.829 25.922 6.39 50.588 12.478 71.452 28.898l9.948-6.467c-23.505-19.791-51.507-26.744-78.685-33.442-29.436-7.256-57.247-14.138-79.715-36.189m-20.621 19.284c-1.13-1.13-2.192-2.283-3.245-3.44a10.154 10.154 0 0 0 1.644 15.68l33.589 21.837c14.865 6.155 30.413 10.004 45.7 13.771 23.869 5.884 46.667 11.525 66.419 25.197l10.16-6.605c-22.463-16.886-48.521-23.356-73.864-29.603-29.733-7.328-57.818-14.252-80.403-36.837m109.3 84.34a119 119 0 0 1 6.623 3.46 10.16 10.16 0 0 0 7.645-1.402l5.639-3.666c-18.11-12.112-38.247-18.247-58.311-23.36z" class="cls-1"/></svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><title>PlanetScale</title><path d="M0 12C0 5.373 5.373 0 12 0c4.873 0 9.067 2.904 10.947 7.077l-15.87 15.87a12 12 0 0 1-1.935-1.099L14.99 12H12l-8.485 8.485A11.96 11.96 0 0 1 0 12m12.004 12L24 12.004C23.998 18.628 18.628 23.998 12.004 24"/></svg>

Before

Width:  |  Height:  |  Size: 326 B

View File

@@ -12,4 +12,5 @@
import '@signozhq/design-tokens';
import '@signozhq/icons';
import '@signozhq/resizable';
import '@signozhq/ui';

View File

@@ -2,7 +2,7 @@ import { Controller, useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
@@ -92,7 +92,6 @@ function CreateServiceAccountModal(): JSX.Element {
width="narrow"
className="create-sa-modal"
disableOutsideClick={isErrorModalVisible}
testId="create-service-account-modal"
>
<div className="create-sa-modal__content">
<form
@@ -135,17 +134,18 @@ function CreateServiceAccountModal(): JSX.Element {
Cancel
</Button>
<AuthZButton
checks={[SACreatePermission]}
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</AuthZButton>
<AuthZTooltip checks={[SACreatePermission]}>
<Button
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</Button>
</AuthZTooltip>
</DialogFooter>
</DialogWrapper>
);

View File

@@ -1,7 +1,13 @@
import { toast } from '@signozhq/ui/sonner';
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';
@@ -83,7 +89,7 @@ describe('CreateServiceAccountModal', () => {
await waitFor(() => {
expect(
screen.queryByTestId('create-service-account-modal'),
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
});
});
@@ -123,7 +129,7 @@ describe('CreateServiceAccountModal', () => {
});
expect(
screen.getByTestId('create-service-account-modal'),
screen.getByRole('dialog', { name: /New Service Account/i }),
).toBeInTheDocument();
});
@@ -131,14 +137,15 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await screen.findByTestId('create-service-account-modal');
const dialog = await screen.findByRole('dialog', {
name: /New Service Account/i,
});
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitFor(() => {
expect(
screen.queryByTestId('create-service-account-modal'),
).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(dialog);
expect(
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
});
it('shows "Name is required" after clearing the name field', async () => {

View File

@@ -3,8 +3,7 @@ import { Button, Popover, Tooltip } from 'antd';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import { TelemetryFieldKey } from 'api/v5/v5';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useExportRawData } from 'hooks/useExportData/useServerExport';
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
import { Download, LoaderCircle } from '@signozhq/icons';
import { DataSource } from 'types/common/queryBuilder';
@@ -19,13 +18,11 @@ import './DownloadOptionsMenu.styles.scss';
interface DownloadOptionsMenuProps {
dataSource: DataSource;
selectedColumns?: TelemetryFieldKey[];
panelType?: PANEL_TYPES;
}
export default function DownloadOptionsMenu({
dataSource,
selectedColumns,
panelType,
}: DownloadOptionsMenuProps): JSX.Element {
const [exportFormat, setExportFormat] = useState<string>(DownloadFormats.CSV);
const [rowLimit, setRowLimit] = useState<number>(DownloadRowCounts.TEN_K);
@@ -36,7 +33,6 @@ export default function DownloadOptionsMenu({
const { isDownloading, handleExportRawData } = useExportRawData({
dataSource,
panelType,
});
const handleExport = useCallback(async (): Promise<void> => {

View File

@@ -1,7 +1,7 @@
import { QueryParams } from 'constants/query';
export const ExploreHeaderToolTip = {
url: 'https://signoz.io/docs/querying/overview/?utm_source=product&utm_medium=new-query-builder',
url: 'https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=new-query-builder',
text: 'More details on how to use query builder',
};

View File

@@ -1,33 +0,0 @@
.export-menu-popover {
width: 240px;
padding: 0 12px 12px 12px;
.title {
display: flex;
color: var(--l1-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-small);
font-style: normal;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
margin-bottom: 8px;
}
.export-format {
padding: 12px 4px;
display: flex;
flex-direction: column;
// radio option labels — radix popover content inherits the root font
// size; pin to the app's 13px base the antd popover used to impose
label {
font-size: var(--periscope-font-size-base);
}
}
.export-button {
width: 100%;
}
}

View File

@@ -1,92 +0,0 @@
import { Download } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import {
ClientExportData,
useClientExport,
} from 'hooks/useExportData/useClientExport';
import { ExportFormat } from 'lib/exportData/types';
import { useCallback, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import './ExportMenu.styles.scss';
interface ExportMenuProps {
dataSource: DataSource;
// The queryRange response object the view holds — the hook picks the
// serializer (timeseries / table) from what it carries.
data: ClientExportData;
query?: Query;
yAxisUnit?: string;
fileName?: string;
}
// Download menu for in-memory query results (client-side serialization).
// The raw/list backend export keeps its own menu in DownloadOptionsMenu.
export default function ExportMenu({
dataSource,
data,
query,
yAxisUnit,
fileName,
}: ExportMenuProps): JSX.Element {
const [exportFormat, setExportFormat] = useState<string>(ExportFormat.Csv);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const { isExporting, handleExport: handleClientExport } = useClientExport({
data,
query,
yAxisUnit,
fileName,
});
const handleExport = useCallback((): void => {
setIsPopoverOpen(false);
handleClientExport({ format: exportFormat as ExportFormat });
}, [exportFormat, handleClientExport]);
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<TooltipSimple title="Download">
<PopoverTrigger asChild>
<Button
variant="ghost"
color="secondary"
size="icon"
aria-label="Download"
data-testid={`export-menu-${dataSource}`}
disabled={isExporting}
loading={isExporting}
>
<Download size={14} />
</Button>
</PopoverTrigger>
</TooltipSimple>
<PopoverContent align="end" className="export-menu-popover">
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup value={exportFormat} onChange={setExportFormat}>
<RadioGroupItem value={ExportFormat.Csv}>csv</RadioGroupItem>
<RadioGroupItem value={ExportFormat.Jsonl}>jsonl</RadioGroupItem>
</RadioGroup>
</div>
<Button
variant="solid"
color="primary"
className="export-button"
onClick={handleExport}
disabled={isExporting}
loading={isExporting}
prefix={<Download size={16} />}
>
Export
</Button>
</PopoverContent>
</Popover>
);
}

View File

@@ -1,85 +0,0 @@
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import { fireEvent, render, screen } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import ExportMenu from '../ExportMenu';
const mockHandleExport = jest.fn();
let mockIsExporting = false;
jest.mock('hooks/useExportData/useClientExport', () => ({
useClientExport: (): unknown => ({
isExporting: mockIsExporting,
handleExport: mockHandleExport,
}),
}));
const data = {
statusCode: 200,
error: null,
message: '',
payload: { data: { result: [], resultType: 'time_series' } },
} as unknown as MetricQueryRangeSuccessResponse;
const TEST_ID = `export-menu-${DataSource.LOGS}`;
function renderMenu(): void {
render(
<ExportMenu
dataSource={DataSource.LOGS}
data={data}
fileName="logs-timeseries"
/>,
);
}
describe('ExportMenu', () => {
beforeEach(() => {
mockHandleExport.mockReset();
mockIsExporting = false;
});
it('renders the download trigger button', () => {
renderMenu();
expect(screen.getByTestId(TEST_ID)).toBeInTheDocument();
});
it('shows only format options — no shape, row-count, or column controls', () => {
renderMenu();
fireEvent.click(screen.getByTestId(TEST_ID));
expect(screen.getByText('FORMAT')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'csv' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'jsonl' })).toBeInTheDocument();
expect(screen.queryByText('Number of Rows')).not.toBeInTheDocument();
expect(screen.queryByText('Columns')).not.toBeInTheDocument();
expect(screen.queryByRole('radio', { name: 'long' })).not.toBeInTheDocument();
expect(screen.queryByRole('radio', { name: 'wide' })).not.toBeInTheDocument();
});
it('exports as csv by default', () => {
renderMenu();
fireEvent.click(screen.getByTestId(TEST_ID));
fireEvent.click(screen.getByText('Export'));
expect(mockHandleExport).toHaveBeenCalledTimes(1);
expect(mockHandleExport).toHaveBeenCalledWith({ format: 'csv' });
});
it('exports as jsonl when selected', () => {
renderMenu();
fireEvent.click(screen.getByTestId(TEST_ID));
fireEvent.click(screen.getByRole('radio', { name: 'jsonl' }));
fireEvent.click(screen.getByText('Export'));
expect(mockHandleExport).toHaveBeenCalledWith({ format: 'jsonl' });
});
it('disables the trigger while an export is in progress', () => {
mockIsExporting = true;
renderMenu();
expect(screen.getByTestId(TEST_ID)).toBeDisabled();
});
});

View File

@@ -20,7 +20,7 @@ import { Globe, Inbox, SquarePen } from '@signozhq/icons';
import AnnouncementsModal from './AnnouncementsModal';
import FeedbackModal from './FeedbackModal';
import ShareURLModal, { type ShareURLExtraOption } from './ShareURLModal';
import ShareURLModal from './ShareURLModal';
import './HeaderRightSection.styles.scss';
import { Typography } from '@signozhq/ui/typography';
@@ -29,15 +29,12 @@ interface HeaderRightSectionProps {
enableAnnouncements: boolean;
enableShare: boolean;
enableFeedback: boolean;
/** Optional page-specific toggle for the share dialog (e.g. "Include variables"). */
shareModalExtraOption?: ShareURLExtraOption;
}
function HeaderRightSection({
enableAnnouncements,
enableShare,
enableFeedback,
shareModalExtraOption,
}: HeaderRightSectionProps): JSX.Element | null {
const location = useLocation();
@@ -188,7 +185,7 @@ function HeaderRightSection({
rootClassName="header-section-popover-root"
className="shareable-link-popover"
placement="bottomRight"
content={<ShareURLModal extraOption={shareModalExtraOption} />}
content={<ShareURLModal />}
open={openShareURLModal}
destroyTooltipOnHide
arrow={false}

View File

@@ -24,22 +24,7 @@ const routesToBeSharedWithTime = [
ROUTES.METER_EXPLORER,
];
/**
* An optional, page-specific toggle in the share dialog (e.g. a dashboard's
* "Include variables"). When enabled, `apply` mutates the URL params that go into
* the shared link. Keeps this shared modal generic — the page owns what it adds.
*/
export interface ShareURLExtraOption {
label: string;
defaultEnabled?: boolean;
apply: (params: URLSearchParams) => void;
}
interface ShareURLModalProps {
extraOption?: ShareURLExtraOption;
}
function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
function ShareURLModal(): JSX.Element {
const urlQuery = useUrlQuery();
const location = useLocation();
const { selectedTime } = useSelector<AppState, GlobalReducer>(
@@ -49,9 +34,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(
selectedTime !== 'custom',
);
const [enableExtraOption, setEnableExtraOption] = useState(
extraOption?.defaultEnabled ?? false,
);
const startTime = urlQuery.get(QueryParams.startTime);
const endTime = urlQuery.get(QueryParams.endTime);
@@ -111,11 +93,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
}
}
if (extraOption && enableExtraOption) {
extraOption.apply(urlQuery);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
}
return currentUrl;
};
@@ -166,20 +143,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
</>
)}
{extraOption && (
<div className="absolute-relative-time-toggler-container">
<Typography.Text className="absolute-relative-time-toggler-label">
{extraOption.label}
</Typography.Text>
<div className="absolute-relative-time-toggler">
<Switch
value={enableExtraOption}
onChange={(): void => setEnableExtraOption((prev) => !prev)}
/>
</div>
</div>
)}
<div className="share-link">
<div className="url-share-container">
<div className="url-share-container-header">

View File

@@ -12,8 +12,6 @@ type BadgeColor =
interface HttpStatusBadgeProps {
statusCode: string | number;
testId?: string;
className?: string;
}
function getStatusCodeColor(statusCode: number): BadgeColor {
@@ -37,8 +35,6 @@ function getStatusCodeColor(statusCode: number): BadgeColor {
function HttpStatusBadge({
statusCode,
testId,
className,
}: HttpStatusBadgeProps): JSX.Element | null {
const numericStatusCode = Number(statusCode);
@@ -49,12 +45,7 @@ function HttpStatusBadge({
const color = getStatusCodeColor(numericStatusCode);
return (
<Badge
color={color}
variant="outline"
data-testid={testId}
className={className}
>
<Badge color={color} variant="outline">
{statusCode}
</Badge>
);

View File

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

View File

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

View File

@@ -80,8 +80,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -101,8 +101,7 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
border-left: 6px dotted var(--l1-border);
}
/* Horizontal line pointing from vertical to the item */
@@ -115,8 +114,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -242,8 +241,7 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
border-left: 6px dotted var(--l1-border);
}
/* Horizontal line pointing from vertical to the item */
@@ -256,8 +254,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -275,16 +273,6 @@
line-height: 16px; /* 128.571% */
resize: none;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
.formula-legend {
@@ -294,42 +282,15 @@
.ant-input-group-addon {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: var(
--query-builder-v2-background-color,
var(--l2-background)
);
color: var(--query-builder-v2-color, var(--l2-foreground));
background: var(--l2-background);
color: var(--l2-foreground);
font-size: 12px;
font-weight: 300;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
position: relative;
}
.ant-input {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
height: 36px;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
position: relative;
margin-left: -1px;
&:hover,
&:focus {
z-index: 1;
border-color: var(--internal-ant-border-color-hover);
}
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
}
}
@@ -362,8 +323,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -434,8 +395,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -451,7 +412,7 @@
min-width: 120px;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
@@ -496,16 +457,13 @@
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
height: 36px !important;
border: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
height: 34px !important;
box-sizing: border-box !important;
.ant-select-selection-item {
color: var(--query-builder-v2-color, var(--l1-foreground));
color: var(--l1-foreground);
}
}

View File

@@ -92,11 +92,6 @@
.ant-select {
width: 100%;
.ant-select-selector {
min-height: 36px;
}
.ant-select-selection-search-input {
min-width: max-content !important;
max-width: 100% !important;
@@ -105,12 +100,9 @@
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground));
border: 1.005px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -131,7 +123,6 @@
.input {
flex: initial;
width: 100px !important;
min-height: 36px;
}
}
}

View File

@@ -131,7 +131,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
@@ -254,7 +254,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}

View File

@@ -8,9 +8,9 @@
.ant-select-selection-search-input {
font-size: 12px !important;
line-height: 25px;
line-height: 27px;
&::placeholder {
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
color: var(--l2-foreground) !important;
font-size: 12px !important;
}
}
@@ -22,12 +22,9 @@
.ant-select-selector {
width: 100%;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground));
border: 1px solid var(--l1-border) !important;
background: var(--l1-background);
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -36,49 +33,36 @@
min-height: 36px;
.ant-select-selection-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
color: var(--l2-foreground) !important;
font-size: 12px !important;
}
}
}
.qb-select-popover.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-item {
color: var(--query-builder-v2-color, var(--l2-foreground));
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
.ant-select-item {
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
&:not(:last-of-type) {
margin-bottom: 4px;
}
&:hover,
&.ant-select-item-option-active {
background: var(--l3-background) !important;
}
&:hover,
&.ant-select-item-option-active {
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
}
&.ant-select-item-option-selected {
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
font-weight: 600;
&.ant-select-item-option-selected {
background: var(--l3-background) !important;
border: 1px solid var(--l1-border);
font-weight: 600;
}
}
}
}

View File

@@ -142,7 +142,6 @@ export const MetricsSelect = memo(function MetricsSelect({
{signalSourceChangeEnabled && (
<Select
className="source-selector"
popupClassName="qb-select-popover"
placeholder="Source"
options={SOURCE_OPTIONS}
value={source}

View File

@@ -1,23 +1,8 @@
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
.query-add-ons {
width: 100%;
--toggle-group-secondary-bg: var(
--query-builder-v2-toggle-group-background-color,
var(--l1-background-hover)
);
--toggle-group-secondary-border: var(
--query-builder-v2-toggle-group-border-color,
var(--l2-border)
);
--toggle-group-secondary-active-bg: var(
--query-builder-v2-toggle-group-active-background-color,
var(--l1-background)
);
--toggle-group-secondary-bg-hover: var(
--query-builder-v2-toggle-group-background-color-hover,
var(--l2-background)
);
.add-on-tab-title {
display: flex;
align-items: center;
@@ -44,33 +29,32 @@
font-style: normal;
font-weight: var(--font-weight-normal);
color: var(--query-builder-v2-color, var(--l2-foreground));
color: var(--l2-foreground);
}
> button {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
border-left: none;
min-width: 120px;
height: 36px;
line-height: 36px;
&:first-child {
border-left: 1px solid
var(--query-builder-v2-border-color, var(--l2-border));
border-left: 1px solid var(--l1-border);
}
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l1-border);
}
&[data-state='on'] {
color: var(--text-robin-500);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
display: none;
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l1-border);
}
}
}
@@ -81,7 +65,7 @@
height: 30px;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
@@ -94,13 +78,10 @@
align-items: center;
.having-filter-select-container {
position: relative;
width: 100%;
display: flex;
flex-direction: row;
align-items: flex-start;
background: var(--query-builder-v2-background-color, var(--l2-background));
padding-right: 38px;
align-items: center;
.having-filter-select-editor {
border-radius: 2px;
@@ -125,17 +106,15 @@
}
.cm-content {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left-width: 0px;
border-right-width: 0px;
border-radius: 2px;
border: 1px solid var(--l1-border);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l2-background) !important;
&:focus-within {
border-color: var(--query-builder-v2-border-color, var(--l2-border));
border-color: var(--l1-border);
}
}
@@ -239,27 +218,17 @@
}
.cm-line {
min-height: 34px;
line-height: 32px !important;
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
::selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
@@ -268,11 +237,8 @@
}
.chip-decorator {
background: var(
--query-builder-v2-chip-decorator-background-color,
var(--l3-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
@@ -280,38 +246,34 @@
}
.cm-selectionBackground {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
.cm-activeLine > span {
font-size: 12px !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
}
.close-btn {
position: absolute;
top: 0;
right: 0;
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
height: 100%;
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
&:focus:not(:focus-visible),
&.ant-btn:focus:not(:focus-visible) {
border-color: var(--l2-border);
border-left-color: transparent;
outline: none;
box-shadow: none;
}
}
}
}
@@ -338,8 +300,20 @@
font-size: 12px !important;
}
input {
min-height: 36px;
$add-on-row-height: 38px;
.periscope-input-with-label {
.input {
.ant-select {
height: $add-on-row-height;
}
}
}
.input-with-label {
.input {
height: $add-on-row-height;
}
}
}
}

View File

@@ -52,7 +52,7 @@ const ADD_ONS = [
key: ADD_ONS_KEYS.GROUP_BY,
description:
'Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments.',
docLink: 'https://signoz.io/docs/querying/aggregation-grouping/#grouping',
docLink: 'https://signoz.io/docs/userguide/query-builder-v5/#grouping',
},
{
icon: <ScrollText size={14} />,
@@ -61,7 +61,7 @@ const ADD_ONS = [
description:
'Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500',
docLink:
'https://signoz.io/docs/querying/result-manipulation/#conditional-filtering-with-having',
'https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having',
},
{
icon: <ScrollText size={14} />,
@@ -70,7 +70,7 @@ const ADD_ONS = [
description:
'Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers.',
docLink:
'https://signoz.io/docs/querying/result-manipulation/#sorting--limiting',
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: <ScrollText size={14} />,
@@ -79,7 +79,7 @@ const ADD_ONS = [
description:
'Show only the top/bottom N results. Perfect for focusing on outliers, reducing noise, and improving dashboard performance.',
docLink:
'https://signoz.io/docs/querying/result-manipulation/#how-limit-works-for-time-series',
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: <ScrollText size={14} />,
@@ -88,7 +88,7 @@ const ADD_ONS = [
description:
'Customize series labels using variables like {{service.name}}-{{endpoint}}. Makes charts readable at a glance during incident investigation.',
docLink:
'https://signoz.io/docs/querying/aggregation-grouping/#legend-formatting',
'https://signoz.io/docs/userguide/query-builder-v5/#legend-formatting',
},
];
@@ -99,7 +99,7 @@ const REDUCE_TO = {
description:
'Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#result-manipulation',
'https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations',
};
const hasValue = (value: unknown): boolean =>
@@ -349,7 +349,7 @@ function QueryAddOns({
<TooltipContent
label="Group By"
description="Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments."
docLink="https://signoz.io/docs/querying/aggregation-grouping/#grouping"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#grouping"
/>
}
placement="top"
@@ -385,7 +385,7 @@ function QueryAddOns({
<TooltipContent
label="Having"
description="Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500"
docLink="https://signoz.io/docs/querying/result-manipulation/#conditional-filtering-with-having"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having"
/>
}
placement="top"
@@ -434,7 +434,7 @@ function QueryAddOns({
<TooltipContent
label="Order By"
description="Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers."
docLink="https://signoz.io/docs/querying/result-manipulation/#sorting--limiting"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting"
/>
}
placement="top"
@@ -473,7 +473,7 @@ function QueryAddOns({
<TooltipContent
label="Reduce to"
description="Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value."
docLink="https://signoz.io/docs/userguide/query-builder-v5/#result-manipulation"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations"
/>
}
placement="top"

View File

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

View File

@@ -65,7 +65,7 @@ function QueryAggregationOptions({
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}

View File

@@ -676,7 +676,7 @@ function QueryAggregationSelect({
</span>
<br />
<a
href="https://signoz.io/docs/querying/aggregation-grouping/#core-aggregation-functions-logs--traces"
href="https://signoz.io/docs/userguide/query-builder-v5/#core-aggregation-functions"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}

View File

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

View File

@@ -44,7 +44,7 @@ function TraceOperatorSection({
<div style={{ textAlign: 'center' }}>
Add Trace Matching
<Typography.Link
href="https://signoz.io/docs/querying/multi-query-analysis/#trace-matching"
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
target="_blank"
style={{ textDecoration: 'underline' }}
>
@@ -106,7 +106,7 @@ export default function QueryFooter({
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/querying/multi-query-analysis/#advanced-comparisons"
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>

View File

@@ -40,14 +40,11 @@ $max-recents-shown: 5;
.query-status-container {
width: 32px;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l1-background) !important;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
border-radius: 2px;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
@@ -86,16 +83,16 @@ $max-recents-shown: 5;
.cm-content {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
padding: 0px !important;
&:focus-within {
border-color: var(--query-builder-v2-border-color, var(--l2-border));
border-color: var(--l1-border);
}
}
&.cm-focused {
outline: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
outline: 1px solid var(--l1-border);
}
.cm-tooltip-autocomplete {
@@ -186,17 +183,11 @@ $max-recents-shown: 5;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
background-color: var(--l1-background) !important;
color: var(--l2-foreground) !important;
&:hover {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
}
.cm-completionIcon {
@@ -214,10 +205,7 @@ $max-recents-shown: 5;
}
&[aria-selected='true'] {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
font-weight: 600 !important;
}
}
@@ -286,44 +274,25 @@ $max-recents-shown: 5;
}
.cm-line {
line-height: 36px !important;
line-height: 34px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
::selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
}
.cm-selectionBackground {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
.cursor-position {

View File

@@ -52,8 +52,6 @@ import type { SignalType } from 'types/api/v5/queryRange';
import { queryExamples, SUGGESTIONS_SECTION } from './constants';
import {
combineInitialAndUserExpression,
dedupeOptionsByLabel,
getFieldContextPrefix,
getRecentOptions,
renderRecentDeleteButton,
} from './utils';
@@ -91,7 +89,6 @@ interface QuerySearchProps {
onChange: (value: string) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
metricNamespace?: string;
signalSource?: string;
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
@@ -110,7 +107,6 @@ function QuerySearch({
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
initialExpression,
metricNamespace,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -224,12 +220,6 @@ function QuerySearch({
QueryKeyDataSuggestionsProps[] | null
>(null);
// dedupe keySuggestions by label/name
const dedupedKeySuggestions = useMemo(
() => dedupeOptionsByLabel(keySuggestions || []),
[keySuggestions],
);
const [showExamples] = useState(false);
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
@@ -254,11 +244,9 @@ function QuerySearch({
[key: string]: QueryKeyDataSuggestionsProps[];
}): any[] =>
Object.values(keys).flatMap((items: QueryKeyDataSuggestionsProps[]) =>
items.map(({ name, fieldDataType, fieldContext }) => ({
items.map(({ name, fieldDataType }) => ({
label: name,
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
fieldContext,
fieldDataType,
info: '',
details: '',
})),
@@ -311,23 +299,18 @@ function QuerySearch({
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
metricNamespace,
});
if (response.data.data) {
const { keys } = response.data.data;
const options = generateOptions(keys);
// Deduplicate by full variant identity (name + context + data type), NOT by
// label. deduping by label removes varient which is not expected. If we need
// to dedupe by label use dedupedKeySuggestions not dedupe the source itself
const variantId = (opt: QueryKeyDataSuggestionsProps): string =>
`${opt.label}|${opt.fieldContext ?? ''}|${opt.fieldDataType ?? ''}`;
// Use a Map to deduplicate by label and preserve order: new options take precedence
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
options.forEach((opt) => merged.set(variantId(opt), opt));
options.forEach((opt) => merged.set(opt.label, opt));
if (searchText && lastKeyRef.current !== searchText) {
(keySuggestions || []).forEach((opt) => {
if (!merged.has(variantId(opt))) {
merged.set(variantId(opt), opt);
if (!merged.has(opt.label)) {
merged.set(opt.label, opt);
}
});
}
@@ -348,7 +331,6 @@ function QuerySearch({
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
metricNamespace,
],
);
@@ -933,55 +915,8 @@ function QuerySearch({
if (queryContext.isInKey) {
const searchText = word?.text.toLowerCase().trim() ?? '';
const fieldContextMatch = getFieldContextPrefix(searchText);
if (fieldContextMatch) {
const { context: fieldContext, remainder } = fieldContextMatch;
// Fetch the context's page when the prefix is typed exactly eg.("attribute.")
if (remainder === '' && lastFetchedKeyRef.current !== searchText) {
debouncedFetchKeySuggestions(searchText);
}
//suggestions that actually do start with <fieldContext>.
const nameMatches = (keySuggestions || [])
.filter((option) => option.label.toLowerCase().includes(searchText))
.map((option) => ({ ...option, boost: 100 }));
//suggestions which do not start with the prefix but qualifies for suggestion
const contextQualified = (keySuggestions || [])
.filter(
(option) =>
option.fieldContext === fieldContext &&
option.label.toLowerCase().includes(remainder),
)
.map((option) => ({
...option,
label: `${fieldContext}.${option.label}`,
boost: 0,
}));
const contextOptions = dedupeOptionsByLabel([
...nameMatches,
...contextQualified,
]);
// If contextOptions is empty fetch again.
if (
contextOptions.length === 0 &&
lastFetchedKeyRef.current !== searchText
) {
debouncedFetchKeySuggestions(searchText);
}
return {
from: word?.from ?? 0,
to: word?.to ?? cursorPos.ch,
options: addSpaceToOptions(contextOptions),
};
}
options = dedupedKeySuggestions.filter((option) =>
options = (keySuggestions || []).filter((option) =>
option.label.toLowerCase().includes(searchText),
);
@@ -1030,26 +965,9 @@ function QuerySearch({
// If we have a key context, add that info to the operator suggestions
if (keyName) {
const keyContextMatch = getFieldContextPrefix(keyName);
// key-suggestion can contain multiple variants of a single key
// In variants we capture ones that match the label to typed keyName exactly or,
// if it has a prefix fieldContext remove it and then match.
const variants = (keySuggestions || []).filter(
(k) =>
k.label === keyName ||
(keyContextMatch !== null &&
k.fieldContext === keyContextMatch.context &&
k.label === keyContextMatch.remainder),
);
const variantTypes = new Set(
variants
.map((k) =>
k.type === 'keyword' ? QUERY_BUILDER_KEY_TYPES.STRING : k.type,
)
.filter(Boolean),
);
//if there are multi-variant, show all suggestions else just the one
const keyType = variantTypes.size === 1 ? [...variantTypes][0] : '';
// Find the key details from suggestions
const keyDetails = (keySuggestions || []).find((k) => k.label === keyName);
const keyType = keyDetails?.type || '';
// Filter operators based on key type
if (keyType) {
@@ -1292,7 +1210,7 @@ function QuerySearch({
if (curChar === '(') {
// In expression context, suggest keys, functions, or nested parentheses
options = [
...dedupedKeySuggestions,
...(keySuggestions || []),
{ label: '(', type: 'parenthesis', info: 'Open nested group' },
{ label: 'NOT', type: 'operator', info: 'Negate expression' },
...options.filter((opt) => opt.type === 'function'),

View File

@@ -1,120 +0,0 @@
import {
combineInitialAndUserExpression,
dedupeOptionsByLabel,
getFieldContextPrefix,
getUserExpressionFromCombined,
} from '../utils';
describe('entityLogsExpression', () => {
describe('combineInitialAndUserExpression', () => {
it('returns user when initial is empty', () => {
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
'body contains error',
);
});
it('returns initial when user is empty', () => {
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
'k8s.pod.name = "x"',
);
});
it('wraps user in parentheses with AND', () => {
expect(
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
).toBe('k8s.pod.name = "x" AND (body = "a")');
});
});
describe('getUserExpressionFromCombined', () => {
it('returns empty when combined equals initial', () => {
expect(
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
).toBe('');
});
it('extracts user from wrapped form', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND (body = "a")',
),
).toBe('body = "a"');
});
it('extracts user from legacy AND without parens', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND body = "a"',
),
).toBe('body = "a"');
});
it('returns full combined when initial is empty', () => {
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
'service.name = "a"',
);
});
});
});
describe('getFieldContextPrefix', () => {
it('matches a complete context prefix with a remainder', () => {
expect(getFieldContextPrefix('attribute.status')).toStrictEqual({
context: 'attribute',
remainder: 'status',
});
});
it('matches a bare context prefix with empty remainder', () => {
expect(getFieldContextPrefix('resource.')).toStrictEqual({
context: 'resource',
remainder: '',
});
});
it('matches every backend field context', () => {
['attribute', 'resource', 'span', 'body', 'log', 'metric'].forEach((ctx) => {
expect(getFieldContextPrefix(`${ctx}.x`)).toStrictEqual({
context: ctx,
remainder: 'x',
});
});
});
it('does not match a partial context name', () => {
expect(getFieldContextPrefix('attr')).toBeNull();
expect(getFieldContextPrefix('attribute')).toBeNull();
});
it('does not match a non-context key with dots', () => {
expect(getFieldContextPrefix('status.code')).toBeNull();
});
it('matches context case-insensitively but keeps remainder casing', () => {
expect(getFieldContextPrefix('Attribute.Status')).toStrictEqual({
context: 'attribute',
remainder: 'Status',
});
});
});
describe('dedupeOptionsByLabel', () => {
it('keeps the first occurrence per label, preserving order', () => {
expect(
dedupeOptionsByLabel([
{ label: 'status.code', type: 'keyword' },
{ label: 'status.code', type: 'number' },
{ label: 'duration', type: 'number' },
]),
).toStrictEqual([
{ label: 'status.code', type: 'keyword' },
{ label: 'duration', type: 'number' },
]);
});
it('returns an empty array for empty input', () => {
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
});
});

View File

@@ -0,0 +1,58 @@
import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from '../utils';
describe('entityLogsExpression', () => {
describe('combineInitialAndUserExpression', () => {
it('returns user when initial is empty', () => {
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
'body contains error',
);
});
it('returns initial when user is empty', () => {
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
'k8s.pod.name = "x"',
);
});
it('wraps user in parentheses with AND', () => {
expect(
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
).toBe('k8s.pod.name = "x" AND (body = "a")');
});
});
describe('getUserExpressionFromCombined', () => {
it('returns empty when combined equals initial', () => {
expect(
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
).toBe('');
});
it('extracts user from wrapped form', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND (body = "a")',
),
).toBe('body = "a"');
});
it('extracts user from legacy AND without parens', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND body = "a"',
),
).toBe('body = "a"');
});
it('returns full combined when initial is empty', () => {
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
'service.name = "a"',
);
});
});
});

View File

@@ -1,16 +1,6 @@
export const RECENTS_SECTION = { name: 'Recent searches', rank: 1 } as const;
export const SUGGESTIONS_SECTION = { name: 'Suggestions', rank: 2 } as const;
// TODO: move to using TelemetrytypesFieldContextDTO when we migrate getKeySuggestions API (Part of https://github.com/SigNoz/engineering-pod/issues/5289)
export const FIELD_CONTEXTS = [
'attribute',
'resource',
'span',
'body',
'log',
'metric',
] as const;
// Custom CodeMirror Completion.type for recent-query entries. Used to discriminate
// recents from regular autocomplete completions in renderers and event handlers.
export const RECENT_COMPLETION_TYPE = 'recent';

View File

@@ -9,45 +9,11 @@ import type { SignalType } from 'types/api/v5/queryRange';
import 'utils/timeUtils';
import {
FIELD_CONTEXTS,
RECENT_COMPLETION_TYPE,
RECENTS_DISPLAY_CAP,
RECENTS_SECTION,
} from './constants';
export interface FieldContextPrefixMatch {
context: string;
remainder: string;
}
// This function checks if the text(query key) starts with a fieldContext
// This util strictly checks that and returns context and remainder back.
// This helps differentiate if the typed key was prefixed with a context or
// was it an actual queryKey like (attribute.abc)
export function getFieldContextPrefix(
text: string,
): FieldContextPrefixMatch | null {
const lower = text.toLowerCase();
const context = FIELD_CONTEXTS.find((ctx) => lower.startsWith(`${ctx}.`));
return context ? { context, remainder: text.slice(context.length + 1) } : null;
}
// Keeps the first occurrence per label, preserving order. Key suggestions hold
// one entry per (name, fieldContext, fieldDataType) variant; This means query builder
// could show multiple labels and this avoids that.
export function dedupeOptionsByLabel<T extends { label: string }>(
options: T[],
): T[] {
const seen = new Set<string>();
return options.filter((option) => {
if (seen.has(option.label)) {
return false;
}
seen.add(option.label);
return true;
});
}
export function combineInitialAndUserExpression(
initial: string,
user: string,

View File

@@ -65,14 +65,6 @@
display: flex;
flex-direction: column;
gap: 8px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
}
&-aggregation-container {

View File

@@ -1,217 +0,0 @@
import { initialQueriesMap } from 'constants/queryBuilder';
import { rest, server } from 'mocks-server/server';
import { render, userEvent, waitFor } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import QuerySearch from '../QuerySearch/QuerySearch';
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
const CM_OPTION_SELECTOR = '.cm-tooltip-autocomplete .cm-completionLabel';
beforeAll(() => {
mockCodeMirrorDomApis();
});
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
const handleRunQuery = jest.fn();
return {
__esModule: true,
useQueryBuilder: (): { handleRunQuery: () => void } => ({ handleRunQuery }),
handleRunQuery,
};
});
// Keys fixture: status.code exists as 3 variants (attribute string/number +
// resource string); attribute.a.b.c is a key literally named with the prefix.
const KEYS_FIXTURE = {
'status.code': [
{
name: 'status.code',
fieldContext: 'attribute',
fieldDataType: 'string',
signal: 'logs',
},
{
name: 'status.code',
fieldContext: 'attribute',
fieldDataType: 'number',
signal: 'logs',
},
{
name: 'status.code',
fieldContext: 'resource',
fieldDataType: 'string',
signal: 'logs',
},
],
'attribute.a.b.c': [
{
name: 'attribute.a.b.c',
fieldContext: 'attribute',
fieldDataType: 'string',
signal: 'logs',
},
],
'duration.nano': [
{
name: 'duration.nano',
fieldContext: 'span',
fieldDataType: 'number',
signal: 'logs',
},
],
};
const fetchedSearchTexts: string[] = [];
beforeEach(() => {
fetchedSearchTexts.length = 0;
server.use(
rest.get('http://localhost/api/v1/fields/keys', (req, res, ctx) => {
fetchedSearchTexts.push(req.url.searchParams.get('searchText') ?? '');
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: { complete: true, keys: KEYS_FIXTURE },
}),
);
}),
rest.get('http://localhost/api/v1/fields/values', (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: { values: { stringValues: [], numberValues: [] } },
}),
),
),
);
});
async function renderAndType(text: string): Promise<HTMLElement> {
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
await waitFor(() => {
expect(document.querySelector(CM_EDITOR_SELECTOR)).toBeInTheDocument();
});
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
await userEvent.click(editor);
await userEvent.type(editor, text);
return editor;
}
// Types a key, waits for its suggestion to render (proving the debounced key
// fetch resolved into state), then types a space to enter operator context.
async function typeKeyThenEnterOperatorContext(
key: string,
expectedKeyLabel: string,
): Promise<void> {
const editor = await renderAndType(key);
await waitFor(() => {
expect(visibleOptionLabels()).toContain(expectedKeyLabel);
});
// skipClick: a click would reset the caret to position 0 under the mocked
// DOM rects; we need the space appended at the end of the typed key.
await userEvent.type(editor, ' ', { skipClick: true });
await waitFor(() => {
expect(visibleOptionLabels()).toContain('=');
});
}
function visibleOptionLabels(): string[] {
return Array.from(document.querySelectorAll(CM_OPTION_SELECTOR)).map(
(el) => el.textContent ?? '',
);
}
describe('QuerySearch context-prefixed key suggestions', () => {
it('shows one deduped suggestion per key name in normal mode', async () => {
await renderAndType('statu');
await waitFor(() => {
expect(visibleOptionLabels()).toContain('status.code');
});
const statusOptions = visibleOptionLabels().filter(
(label) => label === 'status.code',
);
expect(statusOptions).toHaveLength(1);
});
it('shows context-scoped suggestions for a complete context prefix', async () => {
await renderAndType('attribute.');
await waitFor(() => {
expect(visibleOptionLabels()).toContain('attribute.status.code');
});
const labels = visibleOptionLabels();
// Literal key name match ranks first
expect(labels[0]).toBe('attribute.a.b.c');
// Context-qualified form of the literal key is also present
expect(labels).toContain('attribute.attribute.a.b.c');
// span-only key is not suggested under attribute.
expect(labels).not.toContain('attribute.duration.nano');
});
it('fetches the context page when the prefix is typed exactly', async () => {
await renderAndType('attribute.');
await waitFor(() => {
expect(fetchedSearchTexts).toContain('attribute.');
});
});
});
describe('QuerySearch operator suggestions by key type', () => {
it('shows all operators for a key with ambiguous types', async () => {
// status.code is string + number across variants → no type filtering
await typeKeyThenEnterOperatorContext('status.code', 'status.code');
const labels = visibleOptionLabels();
expect(labels).toContain('>');
expect(labels).toContain('LIKE');
});
it('shows numeric operators for a single-type number key', async () => {
await typeKeyThenEnterOperatorContext('duration.nano', 'duration.nano');
const labels = visibleOptionLabels();
expect(labels).toContain('>');
expect(labels).not.toContain('LIKE');
});
it('narrows operators when a context prefix disambiguates the type', async () => {
// status.code is ambiguous globally, but resource.status.code is string-only
await typeKeyThenEnterOperatorContext(
'resource.status.code',
'resource.status.code',
);
const labels = visibleOptionLabels();
expect(labels).toContain('LIKE');
expect(labels).not.toContain('>');
});
});

View File

@@ -8,13 +8,78 @@ import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/ty
import { DataSource } from 'types/common/queryBuilder';
import QuerySearch from '../QuerySearch/QuerySearch';
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
// Mock DOM APIs that CodeMirror needs
beforeAll(() => {
mockCodeMirrorDomApis();
// Mock getClientRects and getBoundingClientRect for Range objects
const mockRect: DOMRect = {
width: 100,
height: 20,
top: 0,
left: 0,
right: 100,
bottom: 20,
x: 0,
y: 0,
toJSON: (): DOMRect => mockRect,
} as DOMRect;
// Create a minimal Range mock with only what CodeMirror actually uses
const createMockRange = (): Range => {
let startContainer: Node = document.createTextNode('');
let endContainer: Node = document.createTextNode('');
let startOffset = 0;
let endOffset = 0;
const mockRange = {
// CodeMirror uses these for text measurement
getClientRects: (): DOMRectList =>
({
length: 1,
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
0: mockRect,
*[Symbol.iterator](): Generator<DOMRect> {
yield mockRect;
},
}) as unknown as DOMRectList,
getBoundingClientRect: (): DOMRect => mockRect,
// CodeMirror calls these to set up text ranges
setStart: (node: Node, offset: number): void => {
startContainer = node;
startOffset = offset;
},
setEnd: (node: Node, offset: number): void => {
endContainer = node;
endOffset = offset;
},
// Minimal Range properties (TypeScript requires these)
get startContainer(): Node {
return startContainer;
},
get endContainer(): Node {
return endContainer;
},
get startOffset(): number {
return startOffset;
},
get endOffset(): number {
return endOffset;
},
get collapsed(): boolean {
return startContainer === endContainer && startOffset === endOffset;
},
commonAncestorContainer: document.body,
};
return mockRange as unknown as Range;
};
// Mock document.createRange to return a new Range instance each time
document.createRange = (): Range => createMockRange();
// Mock getBoundingClientRect for elements
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
});
jest.mock('hooks/useDarkMode', () => ({

View File

@@ -1,71 +0,0 @@
// Mocks the DOM measurement APIs CodeMirror needs to render in jsdom
// (Range client rects + element bounding rects). Call from a beforeAll in
// specs that render the real CodeMirror editor.
export function mockCodeMirrorDomApis(): void {
const mockRect: DOMRect = {
width: 100,
height: 20,
top: 0,
left: 0,
right: 100,
bottom: 20,
x: 0,
y: 0,
toJSON: (): DOMRect => mockRect,
} as DOMRect;
// Create a minimal Range mock with only what CodeMirror actually uses
const createMockRange = (): Range => {
let startContainer: Node = document.createTextNode('');
let endContainer: Node = document.createTextNode('');
let startOffset = 0;
let endOffset = 0;
const mockRange = {
// CodeMirror uses these for text measurement
getClientRects: (): DOMRectList =>
({
length: 1,
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
0: mockRect,
*[Symbol.iterator](): Generator<DOMRect> {
yield mockRect;
},
}) as unknown as DOMRectList,
getBoundingClientRect: (): DOMRect => mockRect,
// CodeMirror calls these to set up text ranges
setStart: (node: Node, offset: number): void => {
startContainer = node;
startOffset = offset;
},
setEnd: (node: Node, offset: number): void => {
endContainer = node;
endOffset = offset;
},
// Minimal Range properties (TypeScript requires these)
get startContainer(): Node {
return startContainer;
},
get endContainer(): Node {
return endContainer;
},
get startOffset(): number {
return startOffset;
},
get endOffset(): number {
return endOffset;
},
get collapsed(): boolean {
return startContainer === endContainer && startOffset === endOffset;
},
commonAncestorContainer: document.body,
};
return mockRange as unknown as Range;
};
// Mock document.createRange to return a new Range instance each time
document.createRange = (): Range => createMockRange();
// Mock getBoundingClientRect for elements
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
}

View File

@@ -9,7 +9,6 @@ import { extractQueryPairs } from 'utils/queryContextUtils';
import {
convertAggregationToExpression,
convertExpressionToFilters,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
formatValueForExpression,
@@ -1599,36 +1598,4 @@ describe('formatValueForExpression', () => {
expect(formatValueForExpression([123] as any)).toBe('[123]');
});
});
// Regression: an escaped single quote must not gain a backslash on each
// expression <-> filters round-trip (broke Create Alert from a panel).
describe('escaped quote round-trip', () => {
const EXPR = "name = 'it\\'s a ribbon'"; // name = 'it\'s a ribbon' (single backslash)
it('stores the unescaped value in the filter item', () => {
const items = convertExpressionToFilters(EXPR);
expect(items).toHaveLength(1);
expect(items[0].value).toBe("it's a ribbon");
});
it('is lossless: expression -> filters -> expression', () => {
const items = convertExpressionToFilters(EXPR);
expect(convertFiltersToExpression({ items, op: 'AND' }).expression).toBe(
EXPR,
);
});
it('is idempotent across repeated existing-query conversions', () => {
const pass1 = convertFiltersToExpressionWithExistingQuery(
{ items: [], op: 'AND' },
EXPR,
);
expect(pass1.filter.expression).toBe(EXPR);
const pass2 = convertFiltersToExpressionWithExistingQuery(
pass1.filters,
pass1.filter.expression,
);
expect(pass2.filter.expression).toBe(EXPR);
});
});
});

View File

@@ -176,9 +176,7 @@ function formatSingleValueForFilter(
}
if (isQuoted(value)) {
// Unescape `\'` → `'` (inverse of formatSingleValue) so the round-trip doesn't
// double the backslash each pass.
return unquote(value).replace(/\\'/g, "'");
return unquote(value);
}
}

View File

@@ -2,7 +2,6 @@ import { Button } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { TooltipSimple } from '@signozhq/ui/tooltip';
interface CheckboxValueRowProps {
value: string;
@@ -47,11 +46,9 @@ function CheckboxValueRow({
{customRendererForValue ? (
customRendererForValue(value)
) : (
<TooltipSimple title={String(value)} side="top" align="start">
<Typography.Text className="value-string" truncate={1}>
{String(value)}
</Typography.Text>
</TooltipSimple>
<Typography.Text className="value-string" truncate={1}>
{String(value)}
</Typography.Text>
)}
<Button type="text" className="only-btn">
{onlyButtonLabel}

View File

@@ -11,8 +11,6 @@ import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { isKeyMatch } from './utils';
import { CheckedState } from '../../types';
import { SectionType } from './v2/itemRules';
export const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
export const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
@@ -150,8 +148,6 @@ export function applyCheckboxToggle({
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
}: {
currentQuery: Query;
activeQueryIndex: number;
@@ -161,8 +157,6 @@ export function applyCheckboxToggle({
value: string;
checked: boolean;
isOnlyOrAllClicked: boolean;
previousState?: CheckedState;
sectionType?: SectionType;
}): Query {
const activeItems =
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
@@ -222,7 +216,6 @@ export function applyCheckboxToggle({
);
if (currentFilter) {
const runningOperator = currentFilter?.op;
switch (runningOperator) {
case 'in':
if (checked) {
@@ -253,29 +246,9 @@ export function applyCheckboxToggle({
});
}
} else if (!checked) {
// Related section: clicking to exclude creates NOT_IN for just this value
if (sectionType === SectionType.RELATED) {
const newFilter: TagFilterItem = {
id: uuid(),
op: getNotInOperator(source),
key: filter.attributeKey,
value,
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
query.filter.expression,
[filter.attributeKey.key],
);
}
} else if (isArray(currentFilter.value)) {
// if we are removing some value when the running operator is IN we filter.
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
// if we are removing some value when the running operator is IN we filter.
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: currentFilter.value.filter((val) => val !== value),
@@ -302,38 +275,11 @@ export function applyCheckboxToggle({
}
break;
case 'nin':
case 'not in': {
// NOT IN means "exclude these values"
// Check if value is currently in the exclusion list
const isValueInFilter = isArray(currentFilter.value)
? currentFilter.value.includes(value)
: currentFilter.value === value;
// When clicking unchecked "Other" item, user wants to SELECT it
// Replace NOT IN filter with IN [value]
if (previousState === 'unchecked' && checked) {
const newFilter: TagFilterItem = {
id: uuid(),
op: getOperatorValue(OPERATORS.IN),
key: filter.attributeKey,
value,
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
query.filter.expression,
[filter.attributeKey.key],
);
}
} else if (!checked || !isValueInFilter) {
// Add to NOT IN when:
// - checked=false (user explicitly unchecked to exclude)
// - checked=true but value not in filter (clicking "other" value to exclude)
case 'not in':
// if the current running operator is NIN then when unchecking the value it gets
// added to the clause like key NIN [value1 , currentUnselectedValue]
if (!checked) {
// in case of array add the currentUnselectedValue to the list.
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
@@ -346,6 +292,7 @@ export function applyCheckboxToggle({
return item;
});
} else {
// in case of not an array make it one!
const newFilter = {
...currentFilter,
value: [currentFilter.value as string, value],
@@ -357,9 +304,8 @@ export function applyCheckboxToggle({
return item;
});
}
} else {
// Remove from NOT IN when value IS in filter and checked=true
// (user wants to include this value back)
} else if (checked) {
// opposite of above!
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
@@ -400,7 +346,6 @@ export function applyCheckboxToggle({
}
}
break;
}
case '=':
if (checked) {
const newFilter = {
@@ -444,11 +389,10 @@ export function applyCheckboxToggle({
}
}
} else {
// No filter for this key - all are visually selected.
// checked=true → user wants to select (IN), checked=false → exclude (NOT IN)
// case - when there is no filter for the current key that means all are selected right now.
const newFilterItem: TagFilterItem = {
id: uuid(),
op: checked ? getOperatorValue(OPERATORS.IN) : getNotInOperator(source),
op: getNotInOperator(source),
key: filter.attributeKey,
value,
};

View File

@@ -10,8 +10,6 @@ import {
applyCheckboxToggle,
clearFilterFromQuery,
} from './checkboxFilterQuery';
import { CheckedState } from '../../types';
import { SectionType } from './v2/itemRules';
interface UseCheckboxFilterActionsProps {
filter: IQuickFiltersConfig;
@@ -26,8 +24,6 @@ interface UseCheckboxFilterActionsReturn {
value: string,
checked: boolean,
isOnlyOrAllClicked: boolean,
previousState?: CheckedState,
sectionType?: SectionType,
) => void;
onClear: () => void;
}
@@ -57,8 +53,6 @@ function useCheckboxFilterActions({
value: string,
checked: boolean,
isOnlyOrAllClicked: boolean,
previousState?: CheckedState,
sectionType?: SectionType,
): void => {
dispatch(
applyCheckboxToggle({
@@ -70,8 +64,6 @@ function useCheckboxFilterActions({
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
}),
);
};

View File

@@ -1,103 +0,0 @@
.checkboxFilter {
display: flex;
flex-direction: column;
padding: var(--spacing-6);
gap: var(--spacing-6);
border-bottom: 1px solid var(--l1-border);
}
.search {
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-focus-border-color: var(--l2-border);
}
.searchSpinner {
color: var(--l2-foreground);
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.values {
display: flex;
flex-direction: column;
}
.sectionValues {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.loadingValues {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-6) 0;
}
.loadingMore {
align-self: center;
}
.noData {
align-self: center;
}
.showMore {
display: flex;
align-items: center;
justify-content: center;
}
.showMoreText {
color: var(--accent-primary);
cursor: pointer;
}
.goToDocs {
display: flex;
flex-direction: column;
gap: 44px;
}
.goToDocsContainer {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
margin-top: var(--spacing-2);
}
.goToDocsMessage {
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
.goToDocsButton {
display: flex;
align-items: center;
gap: var(--spacing-2);
cursor: pointer;
margin: 0 0 var(--spacing-2);
padding: 0;
}
.goToDocsButtonText {
color: var(--bg-robin-400);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}

View File

@@ -1,126 +0,0 @@
import { render, RenderResult } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
FiltersType,
IQuickFiltersConfig,
QuickFilterCheckboxUseFieldApis,
QuickFiltersSource,
} from '../../../types';
import CheckboxFilterV2 from './CheckboxFilterV2';
export const DEFAULT_FILTER: IQuickFiltersConfig = {
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'tag',
},
dataSource: DataSource.TRACES,
defaultOpen: true,
};
export const DEFAULT_USE_FIELD_APIS: QuickFilterCheckboxUseFieldApis = {
startUnixMilli: 1700000000000,
endUnixMilli: 1700003600000,
existingQuery: null,
};
export function mockFieldsValuesAPI(response: {
relatedValues?: (string | null)[];
stringValues?: (string | null)[];
numberValues?: (number | null)[];
}): void {
server.use(
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: response.relatedValues ?? [],
stringValues: response.stringValues ?? [],
numberValues: response.numberValues ?? [],
},
},
}),
),
),
);
}
export function mockFieldsValuesAPILoading(): void {
server.use(
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
res(ctx.delay(10000)),
),
);
}
export function setupServer(): void {
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
}
export interface FilterItemConfig {
op: string;
value: string | string[];
}
export function renderWithFilter(
onFilterChange: jest.Mock,
filterItem?: FilterItemConfig,
): RenderResult {
const items: TagFilterItem[] = filterItem
? [
{
key: { key: 'deployment.environment' },
op: filterItem.op,
value: filterItem.value,
} as TagFilterItem,
]
: [];
return render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
onFilterChange={onFilterChange}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items, op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
}
export function getFilterFromCall(
onFilterChange: jest.Mock,
callIndex = 0,
): TagFilterItem | undefined {
const query = onFilterChange.mock.calls[callIndex]?.[0] as Query | undefined;
return query?.builder.queryData[0]?.filters?.items?.find(
(item) => item.key?.key === 'deployment.environment',
);
}

View File

@@ -1,241 +0,0 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { LoaderCircle } from '@signozhq/icons';
import {
IQuickFiltersConfig,
QuickFilterCheckboxUseFieldApis,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { NON_SELECTED_OPERATORS } from '../checkboxFilterQuery';
import useActiveQueryIndex from '../useActiveQueryIndex';
import useCheckboxDisclosure from '../useCheckboxDisclosure';
import useCheckboxFilterActions from '../useCheckboxFilterActions';
import useCheckboxFilterState from '../useCheckboxFilterState';
import { useFieldValues } from './useFieldValues';
import { useExistingQuery } from './useExistingQuery';
import { isKeyMatch } from '../utils';
import { CheckboxFilterV2Header } from './CheckboxFilterV2Header';
import { CheckboxFilterV2Section } from './CheckboxFilterV2Section';
import { buildSelectedSet, useSectionedValues } from './useSectionedValues';
import { useStaleRelatedExclusions } from './useStaleRelatedExclusions';
import styles from './CheckboxFilterV2.module.scss';
interface CheckboxFilterV2Props {
filter: IQuickFiltersConfig;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
useFieldApis: QuickFilterCheckboxUseFieldApis;
}
export default function CheckboxFilterV2(
props: CheckboxFilterV2Props,
): JSX.Element {
const { source, filter, onFilterChange, useFieldApis } = props;
const [searchText, setSearchText] = useState<string>('');
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
const { currentQuery } = useQueryBuilder();
const activeQueryIndex = useActiveQueryIndex(source);
const {
isOpen,
isSomeFilterPresentForCurrentAttribute,
visibleItemsCount,
onToggleOpen,
onShowMore,
} = useCheckboxDisclosure({ filter, activeQueryIndex });
// Auto-preserve open state when filter is present
useEffect(() => {
if (isSomeFilterPresentForCurrentAttribute && userToggleState === null) {
setUserToggleState(true);
}
}, [isSomeFilterPresentForCurrentAttribute, userToggleState]);
const { existingQuery, hasExistingQuery } = useExistingQuery({
useFieldApis,
activeQueryIndex,
});
const { relatedValues, allValues, isLoading, isFetching } = useFieldValues({
filter,
searchText,
existingQuery,
metricNamespace: useFieldApis.metricNamespace,
startUnixMilli: useFieldApis.startUnixMilli,
endUnixMilli: useFieldApis.endUnixMilli,
enabled: isOpen,
});
// Track if initial load completed (don't show skeleton after first load)
// Must track if loading ever started, otherwise hasLoadedOnce gets set
// immediately on first render when query is disabled (isLoading=false)
const hasLoadedOnce = useRef(false);
const wasLoading = useRef(false);
if (isLoading) {
wasLoading.current = true;
}
if (!isLoading && wasLoading.current && !hasLoadedOnce.current) {
hasLoadedOnce.current = true;
}
// Combine for state derivation
const attributeValues = useMemo(() => {
const combined = [...relatedValues, ...allValues];
return [...new Set(combined)];
}, [relatedValues, allValues]);
const { currentFilterState, isFilterDisabled, isMultipleValuesTrueForTheKey } =
useCheckboxFilterState({ filter, attributeValues, activeQueryIndex });
const { onChange, onClear } = useCheckboxFilterActions({
filter,
source,
attributeValues,
activeQueryIndex,
onFilterChange,
});
const setSearchTextDebounced = useDebouncedFn((...args) => {
setSearchText(args[0] as string);
}, DEBOUNCE_DELAY);
const currentFilterOp = useMemo(() => {
const filterSync = currentQuery?.builder.queryData?.[
activeQueryIndex
]?.filters?.items.find((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
);
return filterSync?.op;
}, [
currentQuery?.builder.queryData,
activeQueryIndex,
filter.attributeKey.key,
]);
const isNotInOperator = NON_SELECTED_OPERATORS.includes(currentFilterOp || '');
const selectedValues = useMemo(
() => [
...buildSelectedSet(
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
),
],
[currentFilterState, isSomeFilterPresentForCurrentAttribute, isNotInOperator],
);
const isRefreshing = isFetching && !isLoading && searchText === '';
const relatedExclusions = useStaleRelatedExclusions({
selectedValues,
isFetching,
isRefreshing,
});
const { sections, totalCount } = useSectionedValues({
relatedValues,
allValues,
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
hasExistingQuery,
visibleItemsCount,
relatedExclusions,
});
return (
<div className={styles.checkboxFilter} data-testid="checkbox-filter-v2">
<CheckboxFilterV2Header
title={filter.title}
isOpen={isOpen}
showClearAll={!!attributeValues.length}
onToggleOpen={onToggleOpen}
onClear={onClear}
isSomeFilterPresentForCurrentAttribute={
isSomeFilterPresentForCurrentAttribute
}
/>
{isOpen && isLoading && !hasLoadedOnce.current && (
<section>
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && (!isLoading || hasLoadedOnce.current) && (
<>
<section className={styles.search}>
<Input
placeholder="Filter values"
onChange={(e): void => setSearchTextDebounced(e.target.value)}
disabled={isFilterDisabled}
data-testid="checkbox-filter-search"
suffix={
isFetching ? (
<LoaderCircle
size={14}
className={styles.searchSpinner}
data-testid="checkbox-filter-search-loading"
/>
) : null
}
/>
</section>
{totalCount > 0 && (
<section className={styles.values}>
{sections.map((section, index) => (
<CheckboxFilterV2Section
key={section.type}
section={section}
index={index}
isFilterDisabled={isFilterDisabled}
filter={filter}
isSomeFilterPresentForCurrentAttribute={
isSomeFilterPresentForCurrentAttribute
}
isMultipleValuesTrueForTheKey={isMultipleValuesTrueForTheKey}
onChange={onChange}
/>
))}
</section>
)}
{totalCount === 0 && hasLoadedOnce.current && !isFetching && (
<section
className={styles.noData}
data-testid={
searchText
? 'checkbox-filter-no-search-results'
: 'checkbox-filter-empty'
}
>
<Typography.Text>No values found</Typography.Text>
</section>
)}
{visibleItemsCount < totalCount &&
!(searchText && (isLoading || isFetching)) && (
<section className={styles.showMore}>
<Typography.Text
className={styles.showMoreText}
onClick={onShowMore}
data-testid="checkbox-filter-show-more"
>
Show More...
</Typography.Text>
</section>
)}
</>
)}
</div>
);
}

View File

@@ -1,33 +0,0 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.leftAction {
display: flex;
align-items: center;
gap: var(--spacing-3);
}
.title {
color: var(--l2-foreground);
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
text-transform: capitalize;
}
.rightAction {
display: flex;
align-items: center;
min-width: 48px;
}
.clearAll {
font-size: 12px;
color: var(--accent-primary);
cursor: pointer;
}

View File

@@ -1,62 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import styles from './CheckboxFilterV2Header.module.scss';
interface CheckboxFilterHeaderProps {
title: string;
isOpen: boolean;
showClearAll: boolean;
onToggleOpen: () => void;
onClear: () => void;
isSomeFilterPresentForCurrentAttribute: boolean;
}
export function CheckboxFilterV2Header({
title,
isOpen,
showClearAll,
onToggleOpen,
onClear,
isSomeFilterPresentForCurrentAttribute,
}: CheckboxFilterHeaderProps): JSX.Element {
return (
<section
role="button"
tabIndex={0}
className={styles.header}
onClick={onToggleOpen}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
onToggleOpen();
}
}}
data-testid="checkbox-filter-header"
data-state={isOpen ? 'open' : 'closed'}
>
<section className={styles.leftAction}>
{isOpen ? (
<ChevronDown size={13} cursor="pointer" />
) : (
<ChevronRight size={13} cursor="pointer" />
)}
<Typography.Text className={styles.title}>{title}</Typography.Text>
</section>
<section className={styles.rightAction}>
{isOpen && showClearAll && isSomeFilterPresentForCurrentAttribute && (
<Typography.Text
className={styles.clearAll}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
onClear();
}}
data-testid="checkbox-filter-clear-all"
>
Clear
</Typography.Text>
)}
</section>
</section>
);
}

View File

@@ -1,106 +0,0 @@
import {
IQuickFiltersConfig,
CheckedState,
} from 'components/QuickFilters/types';
import { CheckboxFilterV2ValueRow } from './CheckboxFilterV2ValueRow';
import { SectionDivider } from './SectionDivider';
import { Section } from './useSectionedValues';
import { SectionType } from './itemRules';
import styles from './CheckboxFilterV2.module.scss';
interface SectionConfig {
label: string;
tooltip?: string;
}
function getSectionConfig(type: SectionType): SectionConfig | null {
switch (type) {
case SectionType.SELECTED:
return { label: 'Selected' };
case SectionType.RELATED:
return {
label: 'Related',
tooltip: 'Values that are filtered by your current selection.',
};
case SectionType.ALL_VALUES:
return { label: 'All values' };
default:
return null;
}
}
interface CheckboxFilterV2SectionProps {
section: Section;
index: number;
isFilterDisabled: boolean;
filter: IQuickFiltersConfig;
isSomeFilterPresentForCurrentAttribute: boolean;
isMultipleValuesTrueForTheKey: boolean;
onChange: (
value: string,
checked: boolean,
isOnly: boolean,
previousState?: CheckedState,
sectionType?: SectionType,
) => void;
}
export function CheckboxFilterV2Section(
props: CheckboxFilterV2SectionProps,
): JSX.Element | null {
const {
section,
index,
isFilterDisabled,
filter,
isSomeFilterPresentForCurrentAttribute,
isMultipleValuesTrueForTheKey,
onChange,
} = props;
if (section.items.length === 0) {
return null;
}
const config = getSectionConfig(section.type);
// Show divider for all sections except first SELECTED section
const showDivider =
config !== null && (index > 0 || section.type !== SectionType.SELECTED);
return (
<div data-testid={`section-${section.type}`} className={styles.sectionValues}>
{showDivider && config && (
<SectionDivider label={config.label} tooltip={config.tooltip} />
)}
{section.items.map(({ value, badge, checkedState }) => {
const isChecked = checkedState === 'checked';
return (
<CheckboxFilterV2ValueRow
key={value}
value={value}
checkedState={checkedState}
disabled={isFilterDisabled}
title={filter.title}
badge={badge}
onlyButtonLabel={
isSomeFilterPresentForCurrentAttribute
? isChecked && !isMultipleValuesTrueForTheKey
? 'All'
: 'Only'
: 'Only'
}
customRendererForValue={filter.customRendererForValue}
onCheckboxChange={(checked, previousState): void =>
onChange(value, checked, false, previousState, section.type)
}
onOnlyOrAllClick={(): void => onChange(value, isChecked, true)}
/>
);
})}
</div>
);
}

View File

@@ -1,164 +0,0 @@
.valueRow {
display: flex;
align-items: center;
gap: var(--spacing-4);
min-height: 24px;
}
.checkbox {
display: inline-flex;
align-items: center;
}
.valueButton {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: var(--spacing-2);
width: calc(100% - 24px);
cursor: pointer;
}
.content {
display: flex;
align-items: center;
gap: var(--spacing-2);
min-width: 0;
}
.valueLabel {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.actions {
display: grid;
align-items: center;
justify-items: end;
// Stack badge / only / toggle in a single cell so the crossfade overlaps
// instead of laying them side-by-side mid-transition.
> * {
grid-area: 1 / 1;
}
}
.badge {
opacity: 1;
transition:
opacity 0.16s ease,
display 0.16s allow-discrete;
}
.onlyButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
}
}
.toggleButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
}
}
.isDisabled {
cursor: not-allowed;
.valueLabel {
color: var(--l3-foreground);
}
.onlyButton {
cursor: not-allowed;
color: var(--l3-foreground);
}
.toggleButton {
cursor: not-allowed;
color: var(--l3-foreground);
}
}
.valueButton:hover {
.onlyButton {
display: flex;
opacity: 1;
transform: translateX(0);
@starting-style {
opacity: 0;
transform: translateX(4px);
}
}
.badge {
display: none;
opacity: 0;
}
}
.checkbox:hover ~ .valueButton {
.toggleButton {
display: flex;
opacity: 1;
transform: translateX(0);
@starting-style {
opacity: 0;
transform: translateX(4px);
}
}
.badge {
display: none;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.badge,
.onlyButton,
.toggleButton {
transition: none;
}
}
.indicatorFalse {
width: 2px;
height: 11px;
border-radius: 2px;
background: var(--danger-background);
}
.indicatorTrue {
width: 2px;
height: 11px;
border-radius: 2px;
background: var(--bg-forest-500);
}

View File

@@ -1,118 +0,0 @@
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { BadgeConfig } from './itemRules';
import { CheckedState } from '../../../types';
import styles from './CheckboxFilterV2ValueRow.module.scss';
interface ValueRowProps {
value: string;
checkedState: CheckedState;
disabled: boolean;
title: string;
onlyButtonLabel: string;
customRendererForValue?: (value: string) => JSX.Element;
onCheckboxChange: (checked: boolean, previousState: CheckedState) => void;
onOnlyOrAllClick: () => void;
badge: BadgeConfig | null;
}
function toCheckboxValue(state: CheckedState): boolean | 'indeterminate' {
if (state === 'indeterminate') {
return 'indeterminate';
}
return state === 'checked';
}
const INDICATOR_CLASS_MAP = {
false: styles.indicatorFalse,
true: styles.indicatorTrue,
} as Record<string, string>;
export function CheckboxFilterV2ValueRow({
value,
checkedState,
disabled,
title,
onlyButtonLabel,
customRendererForValue,
onCheckboxChange,
onOnlyOrAllClick,
badge,
}: ValueRowProps): JSX.Element {
const indicatorClass = INDICATOR_CLASS_MAP[value];
return (
<div
className={styles.valueRow}
data-testid={`checkbox-value-row-${value}`}
data-state={checkedState}
data-disabled={disabled}
>
<div className={styles.checkbox}>
<Checkbox
onChange={(isChecked): void =>
onCheckboxChange(isChecked === true, checkedState)
}
value={toCheckboxValue(checkedState)}
disabled={disabled}
color="primary"
testId={`checkbox-${title}-${value}`}
/>
</div>
<div
role="button"
tabIndex={disabled ? -1 : 0}
className={cx(styles.valueButton, disabled && styles.isDisabled)}
onClick={(): void => {
if (disabled) {
return;
}
onOnlyOrAllClick();
}}
onKeyDown={(e): void => {
if (disabled) {
return;
}
if (e.key === 'Enter' || e.key === ' ') {
onOnlyOrAllClick();
}
}}
>
<div className={styles.content}>
{indicatorClass && <div className={indicatorClass} />}
{customRendererForValue ? (
customRendererForValue(value)
) : (
<Typography.Text title={value} className={styles.valueLabel}>
{value}
</Typography.Text>
)}
</div>
<div className={styles.actions}>
{badge && (
<Badge
variant="outline"
color={badge.color}
className={styles.badge}
testId={`badge-${badge.key}`}
>
{badge.label}
</Badge>
)}
<Button variant="ghost" color="secondary" className={styles.onlyButton}>
{onlyButtonLabel}
</Button>
<Button variant="ghost" color="secondary" className={styles.toggleButton}>
Toggle
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,29 +0,0 @@
.divider {
display: flex;
align-items: center;
gap: var(--spacing-4);
padding: var(--spacing-2) 0;
margin-bottom: calc(-1 * var(--spacing-4));
}
.line {
flex: 1;
height: 1px;
background: var(--l1-border);
}
.label {
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--l3-foreground);
white-space: nowrap;
}
.infoIcon {
color: var(--l3-foreground);
cursor: help;
flex-shrink: 0;
}

View File

@@ -1,33 +0,0 @@
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Info } from '@signozhq/icons';
import styles from './SectionDivider.module.scss';
interface SectionDividerProps {
label: string;
tooltip?: string;
}
export function SectionDivider({
label,
tooltip,
}: SectionDividerProps): JSX.Element {
const testId = label.toLowerCase().replace(/\s+/g, '-');
return (
<div className={styles.divider} data-testid={`section-divider-${testId}`}>
<div className={styles.line} />
<Typography.Text className={styles.label}>{label}</Typography.Text>
{tooltip && (
<Tooltip title={tooltip}>
<Info
size={12}
className={styles.infoIcon}
data-testid="section-divider-info"
/>
</Tooltip>
)}
<div className={styles.line} />
</div>
);
}

View File

@@ -1,302 +0,0 @@
import { screen } from '@testing-library/react';
import { server, rest } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
setupServer,
} from '../CheckboxFilterV2.testUtils';
const USE_FIELD_APIS_AUTO_DERIVE = {
...DEFAULT_USE_FIELD_APIS,
existingQuery: undefined,
};
setupServer();
describe('CheckboxFilterV2 - existingQuery calculation', () => {
const captureExistingQuery = (): Promise<string | null> =>
new Promise((resolve) => {
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const existingQuery = req.url.searchParams.get('existingQuery');
resolve(existingQuery);
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: ['test'],
numberValues: [],
},
},
}),
);
}),
);
});
describe('useFieldApis.existingQuery takes precedence', () => {
it('uses useFieldApis.existingQuery when provided', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'custom.query = "value"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'should.be.ignored = "yes"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe('custom.query = "value"');
});
it('returns undefined when useFieldApis.existingQuery is null', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: null,
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'should.be.ignored = "yes"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBeNull();
});
});
describe('V5 filter.expression preferred over V3 filters.items', () => {
it('uses V5 filter.expression when both exist', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'service.name', dataType: 'string', type: 'tag' },
op: '=',
value: 'from-v3-items',
},
],
op: 'AND',
},
filter: { expression: 'v5.expression = "preferred"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe('v5.expression = "preferred"');
});
it('uses V5 filter.expression when no V3 items exist', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'only.v5 = "expression"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe('only.v5 = "expression"');
});
});
describe('V3 filters.items fallback', () => {
it('converts V3 filters.items to expression when no V5 expression exists', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'service.name', dataType: 'string', type: 'tag' },
op: '=',
value: 'api-service',
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe("service.name = 'api-service'");
});
it('converts multiple V3 filters.items with AND operator', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'service.name', dataType: 'string', type: 'tag' },
op: '=',
value: 'api',
},
{
key: { key: 'env', dataType: 'string', type: 'tag' },
op: '=',
value: 'prod',
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe("service.name = 'api' AND env = 'prod'");
});
it('returns undefined when no filters exist', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBeNull();
});
});
});

View File

@@ -1,768 +0,0 @@
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { server, rest } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
getFilterFromCall,
mockFieldsValuesAPI,
renderWithFilter,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - interactions', () => {
describe('search functionality', () => {
it('filters values based on search text', async () => {
const user = userEvent.setup();
let searchTextReceived = '';
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
searchTextReceived = req.url.searchParams.get('searchText') || '';
const values =
searchTextReceived === ''
? ['production', 'staging', 'development']
: ['production'];
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: values,
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
expect(screen.getByTestId('checkbox-value-row-staging')).toBeInTheDocument();
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(searchTextReceived).toBe('prod');
});
await waitFor(() => {
expect(
screen.queryByTestId('checkbox-value-row-staging'),
).not.toBeInTheDocument();
});
});
it('filters values via search while preserving existingQuery context', async () => {
const user = userEvent.setup();
let requestCount = 0;
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
requestCount += 1;
const searchText = req.url.searchParams.get('searchText') || '';
if (requestCount === 1) {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: ['production'],
stringValues: ['staging', 'development'],
numberValues: [],
},
},
}),
);
}
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: searchText === 'prod' ? ['production'] : [],
stringValues: searchText === 'prod' ? ['production'] : ['staging'],
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-production');
// Related values now appear in "Related" section (no badge, uses divider instead)
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(
screen.queryByTestId('checkbox-value-row-staging'),
).not.toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-production'),
).toBeInTheDocument();
});
});
it('shows filtered results in all_values section when searching with existingQuery', async () => {
const user = userEvent.setup();
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const searchText = req.url.searchParams.get('searchText') || '';
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: searchText === '' ? ['prod', 'staging'] : ['prod-match'],
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
);
await screen.findByTestId('checkbox-value-row-prod');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(screen.getByTestId('section-all_values')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-prod-match'),
).toBeInTheDocument();
});
});
it('shows empty search results message when no matches found', async () => {
const user = userEvent.setup();
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const searchText = req.url.searchParams.get('searchText') || '';
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: searchText === '' ? ['prod', 'staging'] : [],
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-prod');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'xyz-no-match');
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-no-search-results'),
).toBeInTheDocument();
});
});
it('shows both RELATED and ALL_VALUES sections with non-overlapping values', async () => {
// This tests the bug fix where different pod names in relatedValues vs stringValues
// caused all items to go to ALL_VALUES instead of showing RELATED section
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
// Non-overlapping: different pod instances
relatedValues: ['pod-a-instance-1', 'pod-b-instance-1'],
stringValues: ['pod-a-instance-2', 'pod-b-instance-2'],
numberValues: [],
},
},
}),
),
),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
);
// Wait for values to load
await screen.findByTestId('checkbox-value-row-pod-a-instance-1');
// RELATED section should exist with relatedValues
expect(screen.getByTestId('section-related')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-instance-1'),
).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-b-instance-1'),
).toBeInTheDocument();
// ALL_VALUES section should exist with stringValues
expect(screen.getByTestId('section-all_values')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-instance-2'),
).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-b-instance-2'),
).toBeInTheDocument();
});
it('shows both sections during search with filtered non-overlapping values', async () => {
const user = userEvent.setup();
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const searchText = req.url.searchParams.get('searchText') || '';
// Simulate API filtering - both arrays filtered but remain non-overlapping
const relatedValues =
searchText === '' ? ['pod-a-v1', 'pod-b-v1', 'pod-c-v1'] : ['pod-a-v1']; // filtered to match 'pod-a'
const stringValues =
searchText === '' ? ['pod-a-v2', 'pod-b-v2', 'pod-c-v2'] : ['pod-a-v2']; // filtered to match 'pod-a'
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues,
stringValues,
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
);
await screen.findByTestId('checkbox-value-row-pod-a-v1');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'pod-a');
// After search, both sections should still appear with filtered results
await waitFor(() => {
// RELATED section with filtered relatedValues
expect(screen.getByTestId('section-related')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-v1'),
).toBeInTheDocument();
// ALL_VALUES section with filtered stringValues
expect(screen.getByTestId('section-all_values')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-v2'),
).toBeInTheDocument();
// Other values should be filtered out
expect(
screen.queryByTestId('checkbox-value-row-pod-b-v1'),
).not.toBeInTheDocument();
});
});
});
describe('header interactions', () => {
it('collapses when header clicked on open filter', async () => {
const user = userEvent.setup();
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'open');
await user.click(header);
expect(header).toHaveAttribute('data-state', 'closed');
expect(
screen.queryByTestId('checkbox-value-row-production'),
).not.toBeInTheDocument();
});
it('expands when header clicked on closed filter', async () => {
const user = userEvent.setup();
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'closed');
await user.click(header);
expect(header).toHaveAttribute('data-state', 'open');
await screen.findByTestId('checkbox-value-row-production');
});
});
describe('show more functionality', () => {
it('shows "Show More..." when more than 10 values', async () => {
const values = Array.from(
{ length: 15 },
(_, i) => `value-${String(i).padStart(2, '0')}`,
);
mockFieldsValuesAPI({ stringValues: values });
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-value-00');
expect(screen.getByTestId('checkbox-filter-show-more')).toBeInTheDocument();
expect(
screen.queryByTestId('checkbox-value-row-value-10'),
).not.toBeInTheDocument();
});
it('loads more values when "Show More..." clicked', async () => {
const user = userEvent.setup();
const values = Array.from(
{ length: 15 },
(_, i) => `value-${String(i).padStart(2, '0')}`,
);
mockFieldsValuesAPI({ stringValues: values });
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-value-00');
await user.click(screen.getByTestId('checkbox-filter-show-more'));
await screen.findByTestId('checkbox-value-row-value-10');
expect(
screen.getByTestId('checkbox-value-row-value-14'),
).toBeInTheDocument();
});
});
describe('clear functionality', () => {
it('shows clear button when filter is open and has filter applied', async () => {
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-production');
expect(screen.getByTestId('checkbox-filter-clear-all')).toBeInTheDocument();
});
it('hides clear button when no filter applied for attribute', async () => {
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
expect(
screen.queryByTestId('checkbox-filter-clear-all'),
).not.toBeInTheDocument();
});
it('calls onFilterChange when clear clicked', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
onFilterChange={onFilterChange}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-production');
await user.click(screen.getByTestId('checkbox-filter-clear-all'));
expect(onFilterChange).toHaveBeenCalled();
});
});
describe('value row interactions', () => {
it('calls onFilterChange when checkbox value clicked', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
onFilterChange={onFilterChange}
/>,
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
await user.click(within(productionRow).getByText('production'));
expect(onFilterChange).toHaveBeenCalled();
});
it('creates NOT IN filter when unchecking related item with no existing filter', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Start with no filter, related items show as checked
// Clicking unchecks them → creates NOT IN filter to exclude
renderWithFilter(onFilterChange);
const rowA = await screen.findByTestId('checkbox-value-row-valueA');
expect(rowA).toHaveAttribute('data-state', 'checked');
await user.click(within(rowA).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('not in');
expect(filter?.value).toBe('valueA');
});
it('converts NOT IN to IN when toggling unchecked (other) item', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Clicking unchecked "Other" item with NOT IN filter should convert to IN [B]
renderWithFilter(onFilterChange, { op: 'not in', value: ['valueA'] });
const rowB = await screen.findByTestId('checkbox-value-row-valueB');
expect(rowB).toHaveAttribute('data-state', 'unchecked');
await user.click(within(rowB).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('in');
expect(filter?.value).toBe('valueB');
});
it('accumulates both values in IN when toggling checked (related) then unchecked (other)', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Start with IN filter for valueA
renderWithFilter(onFilterChange, { op: 'in', value: ['valueA'] });
const rowA = await screen.findByTestId('checkbox-value-row-valueA');
expect(rowA).toHaveAttribute('data-state', 'checked');
const rowB = screen.getByTestId('checkbox-value-row-valueB');
expect(rowB).toHaveAttribute('data-state', 'unchecked');
// Toggle B (unchecked -> should add to IN)
await user.click(within(rowB).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('in');
expect(filter?.value).toStrictEqual(['valueA', 'valueB']);
});
it('adds to NOT IN when toggling checked (related) with existing NOT IN filter', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Start with NOT IN filter for valueB
renderWithFilter(onFilterChange, { op: 'not in', value: ['valueB'] });
const rowA = await screen.findByTestId('checkbox-value-row-valueA');
// Related values show as checked
expect(rowA).toHaveAttribute('data-state', 'checked');
// Toggle A (checked -> should add to NOT IN)
await user.click(within(rowA).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('not in');
expect(filter?.value).toStrictEqual(['valueB', 'valueA']);
});
it('creates NOT IN for single value when toggling related item with existing IN filter', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['relatedValue'],
stringValues: ['otherValue'],
});
// Start with IN filter for otherValue (Selected section)
// relatedValue is in Related section (checked visually)
renderWithFilter(onFilterChange, { op: 'in', value: ['otherValue'] });
const selectedRow = await screen.findByTestId(
'checkbox-value-row-otherValue',
);
expect(selectedRow).toHaveAttribute('data-state', 'checked');
const relatedRow = screen.getByTestId('checkbox-value-row-relatedValue');
expect(relatedRow).toHaveAttribute('data-state', 'checked');
// Toggle related item (checked -> should create NOT IN with just this value)
await user.click(within(relatedRow).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('not in');
expect(filter?.value).toBe('relatedValue');
});
});
describe('custom renderer', () => {
it('uses customRendererForValue when provided', async () => {
mockFieldsValuesAPI({
stringValues: ['production'],
});
const customRenderer = (value: string): JSX.Element => (
<span data-testid="custom-rendered">{`ENV: ${value}`}</span>
);
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, customRendererForValue: customRenderer }}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('custom-rendered');
expect(screen.getByText('ENV: production')).toBeInTheDocument();
});
});
});

View File

@@ -1,499 +0,0 @@
import { screen, within } from '@testing-library/react';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
mockFieldsValuesAPI,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - item rules', () => {
describe('no existing query', () => {
it('all values show as checked with no badge when no query exists', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(within(productionRow).getByText('production')).toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(productionRow).toHaveAttribute('data-state', 'checked');
expect(stagingRow).toHaveAttribute('data-state', 'checked');
// No section dividers when no existing query (all in "selected" section)
expect(
screen.queryByTestId('section-divider-related'),
).not.toBeInTheDocument();
expect(
screen.queryByTestId('section-divider-all-values'),
).not.toBeInTheDocument();
});
});
describe('with existing query (related values)', () => {
it('shows related values in "Related" section with checked state', async () => {
mockFieldsValuesAPI({
relatedValues: ['production'],
stringValues: ['staging', 'development'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(within(productionRow).getByText('production')).toBeInTheDocument();
// Related values show as checked in "Related" section
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
expect(productionRow).toHaveAttribute('data-state', 'checked');
// Other values show as unchecked in "All values" section
expect(screen.getByTestId('section-divider-all-values')).toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
});
it('shows no badge for values not in relatedValues (unchecked state in All values section)', async () => {
mockFieldsValuesAPI({
relatedValues: ['production'],
stringValues: ['staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const stagingRow = await screen.findByTestId('checkbox-value-row-staging');
expect(within(stagingRow).getByText('staging')).toBeInTheDocument();
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
expect(within(stagingRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
it('shows related values as checked when hasFilterForThisKey=true and isInRelatedValues=true', async () => {
mockFieldsValuesAPI({
relatedValues: ['production', 'staging'],
stringValues: ['development'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
// production is selected - in selected section
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(productionRow).toHaveAttribute('data-state', 'checked');
// staging is related - in related section with checked state
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'checked');
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
});
});
describe('selected values with IN operator', () => {
it('shows checked state with no badge for IN-selected values', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(productionRow).toHaveAttribute('data-state', 'checked');
expect(
within(productionRow).queryByTestId(/^badge-/),
).not.toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
});
});
describe('selected values with NOT IN operator', () => {
it('shows unchecked state with no badge for NOT_IN-selected values', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'not in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(productionRow).toHaveAttribute('data-state', 'unchecked');
expect(
within(productionRow).queryByTestId(/^badge-/),
).not.toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
expect(within(stagingRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
});
describe('section ordering', () => {
it('orders selected section before related section before other section', async () => {
mockFieldsValuesAPI({
relatedValues: ['related-value'],
stringValues: ['other-value', 'selected-value'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['selected-value'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-selected-value');
const allRows = screen.getAllByTestId(/^checkbox-value-row-/);
const values = allRows.map((row) =>
row.getAttribute('data-testid')?.replace('checkbox-value-row-', ''),
);
expect(values[0]).toBe('selected-value');
expect(values[1]).toBe('related-value');
expect(values[2]).toBe('other-value');
});
it('sorts alphabetically within same section', async () => {
mockFieldsValuesAPI({
relatedValues: ['zebra', 'alpha', 'mike'],
stringValues: [],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-alpha');
const allRows = screen.getAllByTestId(/^checkbox-value-row-/);
const values = allRows.map((row) =>
row.getAttribute('data-testid')?.replace('checkbox-value-row-', ''),
);
expect(values).toStrictEqual(['alpha', 'mike', 'zebra']);
});
});
describe('mixed state scenarios', () => {
it('handles mixed state: IN-selected + related + other in same list', async () => {
mockFieldsValuesAPI({
relatedValues: ['related-env'],
stringValues: ['other-env', 'selected-env'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['selected-env'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const selectedRow = await screen.findByTestId(
'checkbox-value-row-selected-env',
);
expect(selectedRow).toHaveAttribute('data-state', 'checked');
expect(within(selectedRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
// Related values now show as checked
const relatedRow = screen.getByTestId('checkbox-value-row-related-env');
expect(relatedRow).toHaveAttribute('data-state', 'checked');
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
const otherRow = screen.getByTestId('checkbox-value-row-other-env');
expect(otherRow).toHaveAttribute('data-state', 'unchecked');
expect(within(otherRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
it('handles NOT_IN-selected alongside related values', async () => {
mockFieldsValuesAPI({
relatedValues: ['related-env'],
stringValues: ['other-env', 'excluded-env'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'not in',
value: ['excluded-env'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const excludedRow = await screen.findByTestId(
'checkbox-value-row-excluded-env',
);
expect(excludedRow).toHaveAttribute('data-state', 'unchecked');
expect(within(excludedRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
// Related values show as checked
const relatedRow = screen.getByTestId('checkbox-value-row-related-env');
expect(relatedRow).toHaveAttribute('data-state', 'checked');
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
});
});
});

View File

@@ -1,207 +0,0 @@
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { server, rest } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
mockFieldsValuesAPI,
mockFieldsValuesAPILoading,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - states', () => {
describe('loading states', () => {
it('shows skeleton while loading initial data', async () => {
mockFieldsValuesAPILoading();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
expect(screen.getByTestId('checkbox-filter-v2')).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-v2').querySelector('.ant-skeleton'),
).toBeInTheDocument();
});
});
it('shows skeleton when initially closed filter is opened for the first time', async () => {
const user = userEvent.setup();
mockFieldsValuesAPILoading();
const closedFilter = { ...DEFAULT_FILTER, defaultOpen: false };
render(
<CheckboxFilterV2
filter={closedFilter}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
// Filter starts closed - no skeleton, no content
expect(
screen.getByTestId('checkbox-filter-v2').querySelector('.ant-skeleton'),
).not.toBeInTheDocument();
expect(
screen.queryByTestId('checkbox-filter-empty'),
).not.toBeInTheDocument();
// Click header to open
const header = screen.getByTestId('checkbox-filter-header');
await user.click(header);
// Should show skeleton while loading, NOT "No values found"
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-v2').querySelector('.ant-skeleton'),
).toBeInTheDocument();
});
expect(
screen.queryByTestId('checkbox-filter-empty'),
).not.toBeInTheDocument();
});
it('shows search spinner when fetching after initial load', async () => {
const user = userEvent.setup();
let requestCount = 0;
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
requestCount += 1;
if (requestCount === 1) {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: ['production', 'staging'],
numberValues: [],
},
},
}),
);
}
return res(ctx.delay(10000));
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-search-loading'),
).toBeInTheDocument();
});
});
});
describe('empty states', () => {
it('shows "No values found" when API returns empty arrays', async () => {
mockFieldsValuesAPI({
relatedValues: [],
stringValues: [],
numberValues: [],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const emptySection = await screen.findByTestId('checkbox-filter-empty');
expect(emptySection).toBeInTheDocument();
});
});
describe('value rendering', () => {
it('renders values from API response', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging', 'development'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
expect(screen.getByTestId('checkbox-value-row-staging')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-development'),
).toBeInTheDocument();
});
it('renders number values converted to strings', async () => {
mockFieldsValuesAPI({
numberValues: [200, 404, 500],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const row200 = await screen.findByTestId('checkbox-value-row-200');
expect(within(row200).getByText('200')).toBeInTheDocument();
expect(
within(screen.getByTestId('checkbox-value-row-404')).getByText('404'),
).toBeInTheDocument();
expect(
within(screen.getByTestId('checkbox-value-row-500')).getByText('500'),
).toBeInTheDocument();
});
it('filters null/undefined values from response', async () => {
mockFieldsValuesAPI({
stringValues: ['valid', null, '', undefined as unknown as string],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const validRow = await screen.findByTestId('checkbox-value-row-valid');
expect(within(validRow).getByText('valid')).toBeInTheDocument();
expect(screen.queryAllByTestId(/^checkbox-value-row-/)).toHaveLength(1);
});
});
});

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