mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-29 04:20:41 +00:00
Compare commits
27 Commits
issue_nerv
...
fix/sentry
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72eb26419a | ||
|
|
5b5b9794f9 | ||
|
|
1957e5a112 | ||
|
|
18eb8ca849 | ||
|
|
3b34a0d951 | ||
|
|
0036cda4f1 | ||
|
|
665e70b17a | ||
|
|
450ceed5fd | ||
|
|
63d0470894 | ||
|
|
c69b77e030 | ||
|
|
4daf425a19 | ||
|
|
6f2ee41dcb | ||
|
|
8ab8c63bea | ||
|
|
0513ab7f1f | ||
|
|
83b4754eaa | ||
|
|
120da85b49 | ||
|
|
b57e5e8e09 | ||
|
|
ba155067e7 | ||
|
|
070343a6c0 | ||
|
|
19af7782d9 | ||
|
|
119ab97240 | ||
|
|
e163ec3b1b | ||
|
|
1e89b49ecf | ||
|
|
8a3702a7f9 | ||
|
|
0376eec24f | ||
|
|
368efd2afe | ||
|
|
fe9165447f |
142
.claude/skills/authz-configurator/SKILL.md
Normal file
142
.claude/skills/authz-configurator/SKILL.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
name: authz-configurator
|
||||
description: Add authorization (authz) to a SigNoz backend feature end-to-end — register kinds/resources, grant managed-role permissions, wire routes with ResourceDefs, backfill tuples for existing orgs, and write the authz integration tests. Use this whenever the user mentions authz, authorization, permissions, access control, managed roles (admin/editor/viewer), OpenFGA, ResourceDef, CheckResources, 403/forbidden behavior on an endpoint, wiring or protecting a new route, registering a new resource/kind, or writing integration tests that assert role-based allow/deny. Also use it when a new API endpoint is being added and its access control hasn't been decided yet — every route needs an authz decision even if the user didn't say the word.
|
||||
---
|
||||
|
||||
# SigNoz Authz Workflow
|
||||
|
||||
This skill automates the procedure documented in `docs/contributing/go/authz.md`. Read that doc first — it is the canonical explanation of the concepts (types, verbs, kinds, resources, selectors, tuples, the community/enterprise split). This skill tells you what to do; the doc tells you why it works.
|
||||
|
||||
The core model in one paragraph: permissions are never attached to users. They are attached to **roles** as OpenFGA tuples, and users/service accounts are made **assignees** of roles. You declare everything in code registries under `pkg/types/coretypes/`; tuples for new organizations are derived from those registries at org bootstrap. Routes declare what resource+verb they touch via `ResourceDef`s, and middleware turns that into a check that works in both community (role gate) and enterprise (per-resource FGA) editions.
|
||||
|
||||
## Before you start
|
||||
|
||||
Figure out what shape the work is. Ask the user if unclear:
|
||||
|
||||
- **New resource** (new kind + routes) → all steps below.
|
||||
- **New route on an existing resource** → step 4 only (maybe step 3 if roles need new verbs).
|
||||
- **Changing what a role can do** → steps 3, 5, 6, 7.
|
||||
- **Just tests** → steps 6, 7.
|
||||
|
||||
Almost every feature resource is a `metaresource` (dashboards, rules, pipelines) or a `telemetryresource` (logs, traces, metrics). If the feature genuinely needs a brand-new FGA **type**, stop and flag it to the user — that requires editing both `pkg/authz/openfgaschema/base.fga` and `ee/authz/openfgaschema/base.fga` together and is almost never the right call. A new **kind** never touches the schema.
|
||||
|
||||
## Step 1: Register the kind
|
||||
|
||||
Edit `pkg/types/coretypes/registry_kind.go`. Add a var and append it to the `Kinds` slice (both are required — the slice is what gets validated and iterated):
|
||||
|
||||
```go
|
||||
KindThing = MustNewKind("thing")
|
||||
```
|
||||
|
||||
Kind names are lowercase, hyphen-separated (`quick-filter`, `trace-funnel`, `notification-channel`).
|
||||
|
||||
Also add the kind to `Kind.Enum()` in `pkg/types/coretypes/kind.go` — a second hand-maintained list mirroring `Kinds` that feeds the OpenAPI enum. It is easy to miss because nothing fails at build time without it; the drift only shows up in the generated spec.
|
||||
|
||||
## Step 2: Register the resource
|
||||
|
||||
Edit `pkg/types/coretypes/registry_resource.go`. Add a var and append it to the `Resources` slice:
|
||||
|
||||
```go
|
||||
ResourceMetaResourceThing = NewResourceMetaResource(KindThing)
|
||||
```
|
||||
|
||||
Use `NewResourceTelemetryResource(KindThing)` for telemetry data resources. Pass an explicit verb list only when the resource supports fewer verbs than the type default — see `ResourceMetaResourceFactorAPIKey` in that file for the shape.
|
||||
|
||||
## Step 3: Grant permissions to managed roles
|
||||
|
||||
Edit `pkg/types/coretypes/registry_managed_role.go`. Add `Transaction` entries to `ManagedRoleToTransactions` for each role that may act on the resource:
|
||||
|
||||
```go
|
||||
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindThing}, WildCardSelectorString)},
|
||||
```
|
||||
|
||||
The convention: **admin** gets everything; **editor** gets full CRUD only on day-to-day observability resources (dashboards, rules, saved views — not access control or org settings); **viewer** gets `read`/`list`; **anonymous** gets nothing (public dashboards are the only exception). Match the file's existing style, including its one-line grouping comment per resource block (e.g. `// thing — editors manage, viewers read`) — that file uses grouping comments as structure; do not add comments anywhere else.
|
||||
|
||||
This map is the single source of truth: changing it affects only organizations created afterwards (see step 5 for existing orgs), and the integration suite asserts its contents via a golden file (see step 6).
|
||||
|
||||
## Step 4: Wire the route
|
||||
|
||||
Register routes in the existing domain file under `pkg/apiserver/signozapiserver/` — do not create new per-sub-resource route files. Each route needs three coordinated pieces:
|
||||
|
||||
1. **`provider.authzMiddleware.CheckResources(handlerFn, roles...)`** — the role list is the community-edition fallback gate (which managed roles may call this route when per-resource checks are unavailable).
|
||||
2. **`handler.WithResourceDefs(...)`** — declares resource, verb, audit category, ID extractor, and selector.
|
||||
3. **`SecuritySchemes: newScopedSecuritySchemes([]string{resource.Scope(verb)})`** — advertises the scope in the OpenAPI spec.
|
||||
|
||||
Selector rules (getting these wrong silently over- or under-authorizes):
|
||||
|
||||
| Operation | Selector | ID extractor |
|
||||
|---|---|---|
|
||||
| create | `WildcardSelector` | `ResponseJSONPath("data.id")` (ID exists only after the handler runs; recorded for audit) |
|
||||
| list | `WildcardSelector` | none |
|
||||
| read/update/delete of one instance | `IDSelector` | `PathParam("id")` or `BodyJSONPath(...)` |
|
||||
| request ID ≠ FGA selector (e.g. route has a role UUID, FGA objects use role names) | custom `SelectorFunc` | as appropriate |
|
||||
|
||||
Routes that link two resources check **both** sides. Read `references/route-wiring.md` before wiring any create route, linking route (attach/detach, parent-child), or custom selector — it has complete real examples from `serviceaccount.go`.
|
||||
|
||||
Prefer `CheckResources` + `ResourceDef` for anything resource-shaped. The legacy gates `ViewAccess`/`EditAccess`/`AdminAccess` are coarse role-only checks — do not use them for new routes. `OpenAccess` means no authorization (authentication still applies); `CheckWithoutClaims` serves anonymous routes.
|
||||
|
||||
## Step 5: Backfill existing organizations (only if needed)
|
||||
|
||||
New organizations derive their tuples from the registries at creation — no migration needed for them. If **existing** organizations must get the new permissions, add a migration in `pkg/sqlmigration/` that does two things per org:
|
||||
|
||||
1. **Insert the FGA tuples** — follow `083_add_role_crud_tuples.go`: one `tuple` row (+ `changelog` row) per role/verb/object, with `ON CONFLICT ... DO NOTHING` so it stays idempotent, handling both the Postgres and SQLite tuple column layouts as that file does.
|
||||
2. **Refresh the managed roles' JSON record** — since `099_add_role_transaction_groups.go`, each role row carries a `transaction_groups` JSON column that mirrors its permissions. Tuples and JSON must move together or the stored record drifts from the registry. Follow 099's managed-role loop: marshal `authtypes.NewTransactionGroupsFromTransactions(coretypes.ManagedRoleToTransactions[roleName])` and `UPDATE role SET transaction_groups = ? WHERE org_id = ? AND type = 'managed' AND name = ?`. Serializing from the registry (which already contains your new grants) makes this step self-updating. Custom roles need no changes — only managed roles mirror the registry.
|
||||
|
||||
Register the new migration factory in `NewSQLMigrationProviderFactories` in `pkg/signoz/provider.go`, appended after the existing entries. The factory name passed to `factory.MustNewName` must match `^[a-z][a-z0-9_-]{0,30}$` — at most 31 characters, or it panics at startup; prefer a short name like `notification_template_tuples` over `add_notification_template_tuples`.
|
||||
|
||||
Ask the user whether backfill is needed rather than assuming — shipping a feature to existing orgs almost always means yes.
|
||||
|
||||
## Step 6: Update the golden testdata and regenerate CI-checked artifacts
|
||||
|
||||
If you changed `ManagedRoleToTransactions`, update `tests/integration/testdata/role/managed_role_transactions.json` to match — the `role/01_register.py` integration test compares each managed role's verb/type/kind matrix against this file and will fail otherwise.
|
||||
|
||||
If you touched the registries or `Kind.Enum()`, regenerate the derived artifacts. The `goci` GitHub workflow reruns each of these and fails the PR on any uncommitted diff:
|
||||
|
||||
```bash
|
||||
go run cmd/enterprise/*.go generate openapi # docs/api/openapi.yml
|
||||
go run cmd/enterprise/*.go generate authz # frontend/src/lib/authz/hooks/useAuthZ/permissions.type.ts
|
||||
go run cmd/enterprise/*.go generate config transaction-groups # frontend/src/schemas/generated/transactionGroups.schema.json
|
||||
```
|
||||
|
||||
Commit whatever they change; never hand-edit these generated files.
|
||||
|
||||
`generate authz` is allowlist-based: it only emits resources listed in the `allowedResources` map inside `runGenerateAuthz` in `cmd/authz.go`. For the new resource to surface in the UI's permission gating (AuthZButton and friends), add it there —
|
||||
|
||||
```go
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceThing).String(): true,
|
||||
```
|
||||
|
||||
— then rerun `generate authz` and commit the regenerated `permissions.type.ts`. If the resource has no UI surface yet, skip the allowlist entry and the command produces no diff; that is expected.
|
||||
|
||||
## Step 7: Write integration tests
|
||||
|
||||
Read `references/integration-tests.md` for the suite layout, fixtures, and canonical allow/deny test shapes. The minimum bar for a new authz'd resource: each managed role's expected 200-vs-403 matrix on the new routes, and (if enterprise/FGA behavior matters) per-object scoping with a custom role — granted instance succeeds, non-granted instance gets 403, revoke flips it back to 403.
|
||||
|
||||
Python test conventions that are hard rules in this repo: inline `requests.*` calls directly (never wrap single API calls in helpers), always pass `timeout=`, and run `make py-fmt && make py-lint` before finishing.
|
||||
|
||||
## Verification
|
||||
|
||||
Run these before declaring the work done:
|
||||
|
||||
```bash
|
||||
make go-build-community
|
||||
```
|
||||
|
||||
Consistency greps — every one of these must hold:
|
||||
|
||||
- the new `Kind` var appears in BOTH the `Kinds` slice (`registry_kind.go`) and `Kind.Enum()` (`kind.go`)
|
||||
- the three `generate` commands from step 6 have been run and leave `git diff` clean when re-run
|
||||
- if the resource is UI-gated, it appears in `allowedResources` in `cmd/authz.go` and `permissions.type.ts` was regenerated
|
||||
- the new `Resource` var appears in the `Resources` slice (`registry_resource.go`)
|
||||
- every `ResourceDef` in the route file references the registered resource and one of its allowed verbs
|
||||
- every route's `SecuritySchemes` scope matches its `ResourceDef` verb
|
||||
- if `ManagedRoleToTransactions` changed, `managed_role_transactions.json` changed with it
|
||||
|
||||
Then run the relevant integration suites (`make py-test-setup` from the repo root boots the environment; pytest runs from `tests/`):
|
||||
|
||||
```bash
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse integration/tests/role/
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse integration/tests/<your-suite>/
|
||||
```
|
||||
|
||||
General repo conventions apply throughout: no code comments (the grouping comments in `registry_managed_role.go` are the sole exception), complete descriptive names, and never commit without an explicit ask.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Authz Integration Tests Reference
|
||||
|
||||
Existing authz suites to imitate: `integration/tests/role/` (managed roles, custom-role CRUD, FGA scoping), `integration/tests/serviceaccount/` (API-key auth by role, role assignment), `integration/tests/passwordauthn/04_role.py` (user role changes, editor denied on admin endpoints).
|
||||
|
||||
## Where a new test goes
|
||||
|
||||
- Suite package: `tests/integration/tests/<suite>/` — pick the existing suite matching the feature's domain, or create a new package when the feature is its own domain.
|
||||
- File name: two-digit ordered prefix, `NN_<topic>.py` (`03_fga.py`, `04_roles.py`). Files in a suite run in order; setup tests early in the file establish state that later tests use.
|
||||
- Golden/testdata JSON: `tests/integration/testdata/<suite>/`, loaded via `fixtures.fs.get_testdata_file_path`.
|
||||
- If the change touched `ManagedRoleToTransactions`, update `tests/integration/testdata/role/managed_role_transactions.json` — `role/01_register.py` asserts every managed role's verb/type/kind matrix against it.
|
||||
|
||||
## Environment and running
|
||||
|
||||
`make` targets run from the repo root; direct `uv run pytest` commands run from `tests/`:
|
||||
|
||||
```bash
|
||||
make py-test-setup # repo root — boot the stack once (~4 min), keep it with --reuse
|
||||
cd tests
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse integration/tests/<suite>/
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse integration/tests/role/03_fga.py::test_read_scoped_to_granted_role
|
||||
make py-test-teardown # repo root — destroy when done; never mix --reuse and --teardown
|
||||
make py-fmt && make py-lint # repo root — mandatory before finishing
|
||||
```
|
||||
|
||||
## Fixtures you'll need
|
||||
|
||||
From `tests/fixtures/auth.py` (registered via `pytest_plugins` in `tests/conftest.py`):
|
||||
|
||||
- `signoz: types.SigNoz` — the running stack; build URLs with `signoz.self.host_configs["8080"].get("/api/v1/...")`.
|
||||
- `create_user_admin` — package-scoped; registers the first admin (`USER_ADMIN_EMAIL` / `USER_ADMIN_PASSWORD`). Take it as an unused argument in every test that needs the org to exist.
|
||||
- `get_token(email, password) -> str` — bearer token for a user.
|
||||
- `add_license(signoz, make_http_mocks, get_token)` — custom roles are enterprise-only; call it in a `test_apply_license` at the top of any suite that creates custom roles.
|
||||
- `create_active_user(signoz, admin_token, email=..., role="signoz-viewer", password=..., name=...) -> user_id` — invite + activate. `role` is a managed role name looked up verbatim (e.g. `signoz-viewer`).
|
||||
- `change_user_role(signoz, admin_token, user_id, old_role, new_role)` — e.g. `"signoz-viewer"` → your custom role name.
|
||||
|
||||
From `tests/fixtures/role.py`:
|
||||
|
||||
- `create_role(token, name, transaction_groups=None, description="") -> role_id` (fixture).
|
||||
- `find_role_by_name(signoz, token, name) -> role_id` — plain function, import `from fixtures.role import find_role_by_name`.
|
||||
- `transaction_group(relation, type_name, kind_name, selectors)` — builds one permission entry, e.g. `transaction_group("read", "metaresource", "thing", ["*"])` or with a specific instance name/ID as selector.
|
||||
|
||||
From `tests/fixtures/serviceaccount.py`:
|
||||
|
||||
- `create_service_account_with_key(signoz, token, name, role="signoz-admin") -> (sa_id, api_key)`.
|
||||
- Service accounts authenticate with the header `SIGNOZ-API-KEY: <key>`; users with `Authorization: Bearer <token>`.
|
||||
|
||||
Hard rule: inline `requests.get/post/put/delete` calls directly in tests — do not wrap single API calls in helper functions or fixtures. Always pass `timeout=5`. Include the response text in assertion messages so failures are diagnosable.
|
||||
|
||||
## Shape 1: managed-role matrix (community-safe)
|
||||
|
||||
For each role, assert the expected status on each new route. Pattern from `serviceaccount/03_auth.py`:
|
||||
|
||||
```python
|
||||
def test_editor_forbidden_on_admin_endpoint(signoz, create_user_admin, get_token):
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
_, api_key = create_service_account_with_key(signoz, token, "sa-role-editor", role="signoz-editor")
|
||||
|
||||
resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/service_accounts"),
|
||||
headers={"SIGNOZ-API-KEY": api_key},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.FORBIDDEN, f"expected 403, got {resp.status_code}: {resp.text}"
|
||||
```
|
||||
|
||||
The user-token variant creates an editor/viewer via `create_active_user` and asserts 403 on the route, 200 for admin.
|
||||
|
||||
## Shape 2: per-object FGA scoping (enterprise, custom role)
|
||||
|
||||
Grant instance verbs on one object only; assert the other stays forbidden. Structure from `role/03_fga.py`:
|
||||
|
||||
```python
|
||||
def test_apply_license(signoz, create_user_admin, make_http_mocks, get_token):
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
|
||||
def test_setup_actor(signoz, create_user_admin, get_token, create_role):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
create_role(
|
||||
admin_token,
|
||||
"thing-fga-actor",
|
||||
[
|
||||
transaction_group("read", "metaresource", "thing", [_TARGET_A_ID]),
|
||||
transaction_group("list", "metaresource", "thing", ["*"]),
|
||||
],
|
||||
)
|
||||
user_id = create_active_user(signoz, admin_token, email=_ACTOR_EMAIL, role="signoz-viewer", password=_ACTOR_PASSWORD, name="thing fga actor")
|
||||
change_user_role(signoz, admin_token, user_id, "signoz-viewer", "thing-fga-actor")
|
||||
|
||||
|
||||
def test_read_scoped_to_grant(signoz, create_user_admin, get_token):
|
||||
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
|
||||
|
||||
resp = requests.get(signoz.self.host_configs["8080"].get(f"/api/v1/things/{_TARGET_A_ID}"), headers={"Authorization": f"Bearer {token}"}, timeout=5)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
|
||||
resp = requests.get(signoz.self.host_configs["8080"].get(f"/api/v1/things/{_TARGET_B_ID}"), headers={"Authorization": f"Bearer {token}"}, timeout=5)
|
||||
assert resp.status_code == HTTPStatus.FORBIDDEN, f"expected 403, got {resp.status_code}: {resp.text}"
|
||||
```
|
||||
|
||||
Key semantics to test (they mirror the selector rules on the route side):
|
||||
|
||||
- `read`/`update`/`delete` granted on a specific selector authorize only that instance; `*` authorizes all.
|
||||
- `create` and `list` are collection-scoped — only a `*` grant makes them pass, and `list` on `*` returns every instance including ones the caller can't `read` individually.
|
||||
- Revoking the grant (update the role's transaction groups, or `change_user_role` back) flips the previously-allowed call to 403.
|
||||
|
||||
Module-level constants for names/emails (see `role/03_fga.py` — `_ACTOR_ROLE_NAME`, `_TARGET_A`) keep the setup test and the assertion tests in sync.
|
||||
|
||||
## What "done" looks like
|
||||
|
||||
- Every new route appears in at least one allow case and one deny case.
|
||||
- Suites that create custom roles start with `test_apply_license`.
|
||||
- `make py-fmt && make py-lint` pass.
|
||||
- The suite passes against a `--reuse` stack: `uv run pytest --basetemp=./tmp/ -vv --reuse integration/tests/<suite>/`.
|
||||
155
.claude/skills/authz-configurator/references/route-wiring.md
Normal file
155
.claude/skills/authz-configurator/references/route-wiring.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Route Wiring Reference
|
||||
|
||||
All examples below are real code from `pkg/apiserver/signozapiserver/serviceaccount.go`. Match this style exactly.
|
||||
|
||||
## Basic route: create (wildcard selector, response-phase ID)
|
||||
|
||||
```go
|
||||
if err := router.Handle("/api/v1/service_accounts", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.Create, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Create service account",
|
||||
Description: "This endpoint creates a service account",
|
||||
Request: new(serviceaccounttypes.PostableServiceAccount),
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
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).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
Create checks the wildcard (can the caller create *any* instance?) but still records the new instance's ID for audit via `ResponseJSONPath` — the ID only exists after the handler runs.
|
||||
|
||||
Instance routes (read/update/delete) differ only in verb, `ID: coretypes.PathParam("id")`, and `Selector: coretypes.IDSelector`. List routes use `WildcardSelector` and omit `ID` entirely.
|
||||
|
||||
Pick the `Category` matching the resource's domain — grep `ActionCategory` in `pkg/types/coretypes/` for the registered values (`ActionCategoryAccessControl` is for access-control resources; observability resources use their own categories).
|
||||
|
||||
## ID extractors (`pkg/types/coretypes/extractor.go`)
|
||||
|
||||
| Extractor | Phase | Use for |
|
||||
|---|---|---|
|
||||
| `PathParam("id")` | request | ID in the URL path |
|
||||
| `BodyJSONPath("data.id")` | request | ID in the request body (gjson path) |
|
||||
| `BodyJSONArray("ids")` | request | multiple IDs in a body array |
|
||||
| `ResponseJSONPath("data.id")` | response | ID known only after the handler (create) |
|
||||
| `NewResourceIDExtractor(phase, fn)` | either | ID needs a lookup (see below) |
|
||||
| `OneID(extractor)` | — | lifts a single-ID extractor where a multi-ID one is required (`SourceIDs`/`TargetIDs`/`ChildIDs`) |
|
||||
|
||||
Custom extractor example — the route path carries a service-account-role ID but the check needs the underlying role ID, so the extractor resolves it through the module:
|
||||
|
||||
```go
|
||||
func (provider *provider) roleIDExtractor() coretypes.ResourceIDExtractor {
|
||||
return coretypes.NewResourceIDExtractor(coretypes.PhaseRequest, func(ec coretypes.ExtractorContext) (string, error) {
|
||||
if ec.Request == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ec.Request.Context())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
serviceAccountRoleID, err := valuer.NewUUID(mux.Vars(ec.Request)["id"])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
serviceAccountRole, err := provider.serviceAccountGetter.GetServiceAccountRole(ec.Request.Context(), valuer.MustNewUUID(claims.OrgID), serviceAccountRoleID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return serviceAccountRole.RoleID.String(), nil
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Custom SelectorFunc
|
||||
|
||||
Use one when the extracted ID is not what FGA objects use. Roles are the canonical case: routes receive a role UUID, but FGA role objects are keyed by role *name*. Always include the wildcard alongside the specific selector — authorization on `*` implies authorization on every instance:
|
||||
|
||||
```go
|
||||
func (provider *provider) roleSelector(ctx context.Context, resource coretypes.Resource, id string, orgID valuer.UUID) ([]coretypes.Selector, error) {
|
||||
roleID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
role, err := provider.authzService.Get(ctx, orgID, roleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []coretypes.Selector{
|
||||
resource.Type().MustSelector(role.Name),
|
||||
resource.Type().MustSelector(coretypes.WildCardSelectorString),
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Linking routes: siblings (peer-to-peer)
|
||||
|
||||
Attaching a role to a service account requires `attach` on **both** resources — the same verb checked on each side. Advertise both scopes:
|
||||
|
||||
```go
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
|
||||
// ...
|
||||
handler.WithResourceDefs(handler.AttachDetachSiblingResourceDef{
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
SourceResource: coretypes.ResourceServiceAccount,
|
||||
SourceIDs: coretypes.OneID(coretypes.BodyJSONPath("serviceAccountId")),
|
||||
SourceSelector: coretypes.IDSelector,
|
||||
TargetResource: coretypes.ResourceRole,
|
||||
TargetIDs: coretypes.OneID(coretypes.BodyJSONPath("roleId")),
|
||||
TargetSelector: provider.roleSelector,
|
||||
}),
|
||||
```
|
||||
|
||||
The detach route (`DELETE .../service_account_roles/{id}`) uses `VerbDetach`, and its path `{id}` identifies the **assignment** (the service account role), not the role. Both IDs are resolved from it via lookup-helper extractors — `provider.serviceAccountIDExtractor()` and `provider.roleIDExtractor()` fetch the assignment and return its service account ID and role ID.
|
||||
|
||||
## Linking routes: parent-child
|
||||
|
||||
Creating an API key *under* a service account checks two different things: `create` on the child resource, and `attach` on the parent. Declare both defs in the same `WithResourceDefs`:
|
||||
|
||||
```go
|
||||
handler.WithResourceDefs(
|
||||
handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
ID: coretypes.ResponseJSONPath("data.id"),
|
||||
Selector: coretypes.WildcardSelector,
|
||||
},
|
||||
handler.AttachDetachParentChildResourceDef{
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
ParentResource: coretypes.ResourceServiceAccount,
|
||||
ParentID: coretypes.PathParam("id"),
|
||||
ParentSelector: coretypes.IDSelector,
|
||||
ChildResource: coretypes.ResourceMetaResourceFactorAPIKey,
|
||||
ChildIDs: coretypes.OneID(coretypes.ResponseJSONPath("data.id")),
|
||||
},
|
||||
),
|
||||
```
|
||||
|
||||
Within the parent-child def, the child is only recorded for audit — the authz check on the child comes from its own `BasicResourceDef`. Deletion mirrors this with `VerbDelete` on the child and `VerbDetach` on the parent (see the `/api/v1/service_accounts/{id}/keys/{fid}` DELETE route).
|
||||
|
||||
## Legacy gates — do not use for new routes
|
||||
|
||||
`ViewAccess`/`EditAccess`/`AdminAccess` only check "does the caller hold one of these roles". They give up per-resource granularity, produce no audit resources, and don't advertise scopes. They exist for old routes; new routes use `CheckResources` + `ResourceDef`. `OpenAccess` performs no authorization at all (authentication still applies — used for `/me`-style routes); `CheckWithoutClaims` is for anonymous routes such as public dashboards.
|
||||
|
||||
Full def types live in `pkg/http/handler/resourcedef.go`; middleware behavior in `pkg/http/middleware/authz.go` and `pkg/http/middleware/resource.go`.
|
||||
@@ -46,9 +46,9 @@ One endpoint feeding several widgets stays one handler that reads the request. `
|
||||
|
||||
```ts
|
||||
rest.get(
|
||||
'http://localhost/api/v1/explorer/views',
|
||||
'http://localhost/api/v2/saved_views',
|
||||
response.json((req) =>
|
||||
savedViews(values.savedViews, req.url.searchParams.get('sourcePage') ?? 'logs'),
|
||||
savedViews(values.savedViews, req.url.searchParams.get('source') ?? 'logs'),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
3803
docs/api/openapi.yml
3803
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -93,9 +93,9 @@ tests/
|
||||
│ └── auth.ts
|
||||
├── helpers/ # function helpers + the constants they share with tests
|
||||
│ ├── auth.ts
|
||||
│ └── dashboards.ts
|
||||
│ └── logs-explorer.ts
|
||||
├── testdata/ # static data files (JSON) used by helpers and tests
|
||||
│ └── apm-metrics.json # (example)
|
||||
│ └── traces/large-trace.json # (example)
|
||||
├── tests/ # Playwright .spec.ts files, one dir per feature area
|
||||
│ └── alerts/
|
||||
│ └── alerts.spec.ts # (example)
|
||||
@@ -110,8 +110,8 @@ tests/
|
||||
These two folders look similar but mean different things:
|
||||
|
||||
- **`fixtures/`** holds *Playwright test fixtures* (created via `test.extend({...})`). By the canonical definition, a fixture is "a consistent, predefined set of data, objects, or environmental conditions used to ensure tests run in a stable state" — i.e. setup/teardown that runs *automatically* around each test or worker. `auth.ts` matches: it extends Playwright's `test` with an `authedPage` that's logged-in before every test runs and torn down after. If the only thing in this folder ever is `auth.ts`, that's fine — fixtures are a deliberately small surface.
|
||||
- **`helpers/`** holds plain function helpers that you call *explicitly* from a test or hook — they don't extend Playwright's `test`. This covers both behaviour helpers (e.g. `gotoDashboardsList(page)`) and the constants those helpers and the tests both refer to (e.g. `SEARCH_PLACEHOLDER`). Constants live next to the helpers that use them so a single import line in a test covers both.
|
||||
- **`testdata/`** holds static data files (typically JSON / YAML) consumed by the helpers — for example, `apm-metrics.json`, a real dashboard payload uploaded through the UI by an importer helper.
|
||||
- **`helpers/`** holds plain function helpers that you call *explicitly* from a test or hook — they don't extend Playwright's `test`. This covers both behaviour helpers (e.g. `gotoLogsExplorer(page)`) and the constants those helpers and the tests both refer to (e.g. `LINKED_ROW_CLASS`). Constants live next to the helpers that use them so a single import line in a test covers both.
|
||||
- **`testdata/`** holds static data files (typically JSON / YAML) consumed by the helpers — for example, `traces/large-trace.json`, a real trace the trace-details helpers seed.
|
||||
|
||||
Rule of thumb: if it's a `test.extend` fixture, put it in `fixtures/`. If it's a function you call explicitly (or a constant the function uses), put it in `helpers/`. If it's a static file the helpers read, put it in `testdata/`.
|
||||
|
||||
@@ -128,7 +128,7 @@ test('TC-09 RouteTab switches the url', async ({ routing }) => {
|
||||
});
|
||||
```
|
||||
|
||||
- `driver(name, build)` adds a test-scoped fixture built on `authedPage`.
|
||||
- `driver(name, build, dispose?)` adds a test-scoped fixture built on `authedPage`. `dispose` runs after the test on the same page, so a driver can delete what it seeded.
|
||||
- `seeded(name, open, close)` adds a worker-scoped seed. `open` and `close` run on a throwaway admin page.
|
||||
- A driver reuses helpers by calling them, never by reaching for another fixture.
|
||||
- `drivers/` is AI-owned: coding agents generate and maintain it with the specs, and `.gitattributes` collapses it in diffs.
|
||||
@@ -187,7 +187,7 @@ Each spec follows these principles:
|
||||
});
|
||||
```
|
||||
This is faster and more reliable than a multi-step UI seed. Reach for the UI flow only when the test's *purpose* is asserting that flow.
|
||||
6. **Reusable static data lives in `tests/e2e/testdata/`.** For example, `apm-metrics.json` is a real dashboard payload that `importApmMetricsDashboardViaUI` (in `helpers/dashboards.ts`) uploads through the actual Import JSON UI flow to seed a richly-tagged dashboard for search/list tests.
|
||||
6. **Reusable static data lives in `tests/e2e/testdata/`.** For example, `traces/large-trace.json` is a real trace that `loadLargeTrace` (in `helpers/trace-details.ts`) seeds for the trace-details tests.
|
||||
|
||||
## How to write an E2E test?
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ const preview: Preview = {
|
||||
'Detail',
|
||||
[
|
||||
'Overview',
|
||||
'Panels',
|
||||
'Settings',
|
||||
'Variables',
|
||||
'New Variable',
|
||||
@@ -140,7 +141,7 @@ const preview: Preview = {
|
||||
'Services',
|
||||
['List', 'Detail', 'Top Level Operations', 'Service Map'],
|
||||
'Logs',
|
||||
['Explorer', 'Live Tail', 'Saved Views', 'Pipelines', 'Settings'],
|
||||
['Explorer', 'Live Tail', 'Pipelines', 'Settings'],
|
||||
'Traces',
|
||||
['Explorer', 'Trace Details', 'Funnel Details'],
|
||||
'Metrics',
|
||||
|
||||
@@ -163,9 +163,9 @@
|
||||
"@commitlint/config-conventional": "20.4.4",
|
||||
"@jest/globals": "30.4.1",
|
||||
"@jest/types": "30.2.0",
|
||||
"@storybook/addon-a11y": "10.5.9",
|
||||
"@storybook/addon-docs": "10.5.9",
|
||||
"@storybook/react-vite": "10.5.9",
|
||||
"@storybook/addon-a11y": "10.6.3",
|
||||
"@storybook/addon-docs": "10.6.3",
|
||||
"@storybook/react-vite": "10.6.3",
|
||||
"@storybook/test-runner": "0.24.5",
|
||||
"@testing-library/dom": "8.20.0",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
@@ -213,10 +213,10 @@
|
||||
"redux-mock-store": "1.5.4",
|
||||
"sass": "1.97.3",
|
||||
"sharp": "0.35.4",
|
||||
"storybook": "10.5.9",
|
||||
"storybook": "10.6.3",
|
||||
"stylelint": "17.15.0",
|
||||
"svgo": "4.1.0",
|
||||
"ts-jest": "29.4.9",
|
||||
"ts-jest": "29.4.15",
|
||||
"typescript-plugin-css-modules": "5.2.0",
|
||||
"use-sync-external-store": "1.6.0",
|
||||
"vite-plugin-checker": "0.12.0",
|
||||
|
||||
385
frontend/pnpm-lock.yaml
generated
385
frontend/pnpm-lock.yaml
generated
@@ -8,9 +8,9 @@ overrides:
|
||||
'@babel/core@<=7.29.0': '>=7.29.6 <8'
|
||||
'@istanbuljs/load-nyc-config>js-yaml': '>=4.3.2 <5'
|
||||
'@xmldom/xmldom@<0.8.15': '>=0.8.15 <0.9'
|
||||
brace-expansion@<1.1.18: '>=1.1.18 <2'
|
||||
brace-expansion@>=2.0.0 <2.1.4: '>=2.1.4 <3'
|
||||
brace-expansion@>=5.0.0 <5.0.9: '>=5.0.9 <6'
|
||||
brace-expansion@<1.1.21: '>=1.1.21 <2'
|
||||
brace-expansion@>=2.0.0 <2.1.7: '>=2.1.7 <3'
|
||||
brace-expansion@>=5.0.0 <5.0.12: '>=5.0.12 <6'
|
||||
browserslist@<4.28.7: '>=4.28.7 <5'
|
||||
cookie@<0.7.0: '>=0.7.1 <1'
|
||||
dompurify@<=3.4.12: '>=3.4.13 <4'
|
||||
@@ -364,17 +364,17 @@ importers:
|
||||
specifier: 30.2.0
|
||||
version: 30.2.0
|
||||
'@storybook/addon-a11y':
|
||||
specifier: 10.5.9
|
||||
version: 10.5.9(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
specifier: 10.6.3
|
||||
version: 10.6.3(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
'@storybook/addon-docs':
|
||||
specifier: 10.5.9
|
||||
version: 10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
specifier: 10.6.3
|
||||
version: 10.6.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
'@storybook/react-vite':
|
||||
specifier: 10.5.9
|
||||
version: 10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(esbuild@0.28.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(typescript@5.9.3)
|
||||
specifier: 10.6.3
|
||||
version: 10.6.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(typescript@5.9.3)
|
||||
'@storybook/test-runner':
|
||||
specifier: 0.24.5
|
||||
version: 0.24.5(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3))
|
||||
version: 0.24.5(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3))
|
||||
'@testing-library/dom':
|
||||
specifier: 8.20.0
|
||||
version: 8.20.0
|
||||
@@ -514,8 +514,8 @@ importers:
|
||||
specifier: 0.35.4
|
||||
version: 0.35.4(@types/node@16.18.25)
|
||||
storybook:
|
||||
specifier: 10.5.9
|
||||
version: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
specifier: 10.6.3
|
||||
version: 10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
stylelint:
|
||||
specifier: 17.15.0
|
||||
version: 17.15.0(typescript@5.9.3)
|
||||
@@ -523,8 +523,8 @@ importers:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
ts-jest:
|
||||
specifier: 29.4.9
|
||||
version: 29.4.9(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.2.0)(babel-jest@29.6.4(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@30.4.1)(jest@30.2.0(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3)))(typescript@5.9.3)
|
||||
specifier: 29.4.15
|
||||
version: 29.4.15(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.2.0)(babel-jest@29.6.4(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@30.4.1)(jest@30.2.0(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3)))(typescript@5.9.3)
|
||||
typescript-plugin-css-modules:
|
||||
specifier: 5.2.0
|
||||
version: 5.2.0(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3))(typescript@5.9.3)
|
||||
@@ -1525,8 +1525,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@emnapi/core@1.11.2':
|
||||
resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==}
|
||||
'@emnapi/core@1.11.0':
|
||||
resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==}
|
||||
|
||||
'@emnapi/core@1.8.1':
|
||||
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
||||
@@ -1534,8 +1534,8 @@ packages:
|
||||
'@emnapi/core@1.9.2':
|
||||
resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
|
||||
|
||||
'@emnapi/runtime@1.11.2':
|
||||
resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==}
|
||||
'@emnapi/runtime@1.11.0':
|
||||
resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==}
|
||||
|
||||
'@emnapi/runtime@1.11.3':
|
||||
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
|
||||
@@ -2170,6 +2170,9 @@ packages:
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.6.0':
|
||||
resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
@@ -2452,106 +2455,106 @@ packages:
|
||||
'@oxc-project/types@0.127.0':
|
||||
resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==}
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.24.2':
|
||||
resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==}
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.21.2':
|
||||
resolution: {integrity: sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@oxc-resolver/binding-android-arm64@11.24.2':
|
||||
resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==}
|
||||
'@oxc-resolver/binding-android-arm64@11.21.2':
|
||||
resolution: {integrity: sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@oxc-resolver/binding-darwin-arm64@11.24.2':
|
||||
resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==}
|
||||
'@oxc-resolver/binding-darwin-arm64@11.21.2':
|
||||
resolution: {integrity: sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-resolver/binding-darwin-x64@11.24.2':
|
||||
resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==}
|
||||
'@oxc-resolver/binding-darwin-x64@11.21.2':
|
||||
resolution: {integrity: sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-resolver/binding-freebsd-x64@11.24.2':
|
||||
resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==}
|
||||
'@oxc-resolver/binding-freebsd-x64@11.21.2':
|
||||
resolution: {integrity: sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2':
|
||||
resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==}
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf@11.21.2':
|
||||
resolution: {integrity: sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf@11.24.2':
|
||||
resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==}
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf@11.21.2':
|
||||
resolution: {integrity: sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==}
|
||||
'@oxc-resolver/binding-linux-arm64-gnu@11.21.2':
|
||||
resolution: {integrity: sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.24.2':
|
||||
resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==}
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.21.2':
|
||||
resolution: {integrity: sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==}
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.21.2':
|
||||
resolution: {integrity: sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==}
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.21.2':
|
||||
resolution: {integrity: sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.24.2':
|
||||
resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==}
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.21.2':
|
||||
resolution: {integrity: sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==}
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.21.2':
|
||||
resolution: {integrity: sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==}
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.21.2':
|
||||
resolution: {integrity: sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.24.2':
|
||||
resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==}
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.21.2':
|
||||
resolution: {integrity: sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.24.2':
|
||||
resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==}
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.21.2':
|
||||
resolution: {integrity: sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@oxc-resolver/binding-wasm32-wasi@11.24.2':
|
||||
resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==}
|
||||
'@oxc-resolver/binding-wasm32-wasi@11.21.2':
|
||||
resolution: {integrity: sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@oxc-resolver/binding-win32-arm64-msvc@11.24.2':
|
||||
resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==}
|
||||
'@oxc-resolver/binding-win32-arm64-msvc@11.21.2':
|
||||
resolution: {integrity: sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.24.2':
|
||||
resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==}
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.21.2':
|
||||
resolution: {integrity: sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@@ -3660,44 +3663,26 @@ packages:
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@storybook/addon-a11y@10.5.9':
|
||||
resolution: {integrity: sha512-MvXkoJIVRcZrqhWi+wynYAmFkvTWm5aHdJfYohhgPeEJVDRF91zejtOsN7zPRE/lQvBDY+IiomKxeRN3UhWNVw==}
|
||||
'@storybook/addon-a11y@10.6.3':
|
||||
resolution: {integrity: sha512-nqvjpmu5g+PPtEKwgE0qAi4wfuQ9yngOb/1Gm24pBBZOuRsd3UMEBjKORSuAGrIAz9X3LCFfQi7sCcBFXvFbwg==}
|
||||
peerDependencies:
|
||||
storybook: ^10.5.9
|
||||
storybook: ^10.6.3
|
||||
|
||||
'@storybook/addon-docs@10.5.9':
|
||||
resolution: {integrity: sha512-8sFsMkZYrrdqCLdV+hnwTwDF7RaBsBPRwl4wfc8ve9Q/7Yhi5REe/Xjvd8x1yn6fBPPw9tnID9dx6Agdnr81fw==}
|
||||
'@storybook/addon-docs@10.6.3':
|
||||
resolution: {integrity: sha512-365f712au4w/xvFqEKY6TIvTeGGAY7FKRB8zP4CFa3VSv9oyxHaq/ublFg4YDbCeRagppwl1PZ6k4+PoUor3+A==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
storybook: ^10.5.9
|
||||
storybook: ^10.6.3
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@storybook/builder-vite@10.5.9':
|
||||
resolution: {integrity: sha512-Zg4JbGQiHFPGlFJ9HM+XPgzKmU/RFPCymhohVRJhBBYfmgaQgz0flWWzscseCDpl638MNd8/r/H+nwuoBgSYDg==}
|
||||
'@storybook/builder-vite@10.6.3':
|
||||
resolution: {integrity: sha512-1+WCZZg/fPxxyHDB5sFKniFt9/4RFUIzoNqMujv+BLRROM+gQeyUHvdvoyZOZ51+P8ee+/PgnNKHjsTupV+nOA==}
|
||||
peerDependencies:
|
||||
storybook: ^10.5.9
|
||||
storybook: ^10.6.3
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
'@storybook/csf-plugin@10.5.9':
|
||||
resolution: {integrity: sha512-4H5QIHQVtQYCuL43GCRLGjNQhZpQg9gL03ja0DV80kO2Dn9LEt6ol87bSnSjn4VDgcAXtgTzXFvRLknfVgAAqg==}
|
||||
peerDependencies:
|
||||
esbuild: '>=0.28.1 <0.29.0'
|
||||
rollup: '*'
|
||||
storybook: ^10.5.9
|
||||
vite: '*'
|
||||
webpack: '*'
|
||||
peerDependenciesMeta:
|
||||
esbuild:
|
||||
optional: true
|
||||
rollup:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
webpack:
|
||||
optional: true
|
||||
|
||||
'@storybook/global@5.0.0':
|
||||
resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
|
||||
|
||||
@@ -3706,40 +3691,40 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@storybook/react-dom-shim@10.5.9':
|
||||
resolution: {integrity: sha512-7qZD6CSa64p1m/zX9tG4ALcEHlEk0Bx+5++4RL3MFlRCgCFfAomXsCHTzF0RyF8cI4vl+hS7Y0ryjQchVs6UQA==}
|
||||
'@storybook/react-dom-shim@10.6.3':
|
||||
resolution: {integrity: sha512-cJUuPrieiTl06kzlgaU89iG+CtLAgUTt0Xdvm8qFT9yR2qwafSaswhwYWlZCNX4nWuOu7eK7uzWTwzKqyDBOWQ==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
'@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
storybook: ^10.5.9
|
||||
storybook: ^10.6.3
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@storybook/react-vite@10.5.9':
|
||||
resolution: {integrity: sha512-mDI+UssrOEP8IwgBsefLG+gqW1gZbZEPbpH5PoHK/P1WPi8BfrLEqrFi9H9wmMkal/Vh8SmL1awgOi0ir3XwcQ==}
|
||||
'@storybook/react-vite@10.6.3':
|
||||
resolution: {integrity: sha512-geWwLi9JnaRG2t9bYBfz+94skARzzXNT1Toaf0lk7Oog13rtNK7ay2pWNm9SP6/L2S7mEjoWu9+zHLpwxXvfIw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
storybook: ^10.5.9
|
||||
storybook: ^10.6.3
|
||||
typescript: '>= 4.9.x'
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@storybook/react@10.5.9':
|
||||
resolution: {integrity: sha512-kApGOuNT26NkpioTsr1iT/Q2c44tA7OIsNUSyFqtT7W8k3fRn/jQWfrDegYxty0WG0wxdNMZq2ndRfOOF8HaHw==}
|
||||
'@storybook/react@10.6.3':
|
||||
resolution: {integrity: sha512-3tfAkZNdUyx3I4IjM8dgbNWg4k0gj89Hi1MPiawfTybRRWhvnX2YqV9Ut4mrud7YzRv0v+U7+ZYEvj+maUPaow==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
'@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
storybook: ^10.5.9
|
||||
storybook: ^10.6.3
|
||||
typescript: '>= 4.9.x'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
@@ -4761,14 +4746,14 @@ packages:
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
brace-expansion@1.1.18:
|
||||
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
|
||||
brace-expansion@1.1.21:
|
||||
resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
|
||||
|
||||
brace-expansion@2.1.4:
|
||||
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
|
||||
brace-expansion@2.1.7:
|
||||
resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==}
|
||||
|
||||
brace-expansion@5.0.9:
|
||||
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
|
||||
brace-expansion@5.0.12:
|
||||
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
braces@3.0.3:
|
||||
@@ -6149,8 +6134,8 @@ packages:
|
||||
resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==}
|
||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||
|
||||
handlebars@4.7.9:
|
||||
resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==}
|
||||
handlebars@4.7.10:
|
||||
resolution: {integrity: sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
hasBin: true
|
||||
|
||||
@@ -7207,6 +7192,9 @@ packages:
|
||||
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
magic-string@1.4.3:
|
||||
resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==}
|
||||
|
||||
make-dir@3.1.0:
|
||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7782,8 +7770,8 @@ packages:
|
||||
resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
||||
oxc-resolver@11.24.2:
|
||||
resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==}
|
||||
oxc-resolver@11.21.2:
|
||||
resolution: {integrity: sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==}
|
||||
|
||||
oxfmt@0.73.0:
|
||||
resolution: {integrity: sha512-Ke4xbpuzqIBP4bkd48q815RhqWNJqOBzxVA9199M4qcF0y0LtpUv7UTg1dJ5ea6jw0RBUDZvX3qOUMNmmxfxbw==}
|
||||
@@ -9175,8 +9163,8 @@ packages:
|
||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
storybook@10.5.9:
|
||||
resolution: {integrity: sha512-UfdMKSjEhIKr8LbqYyIE5r7vT/drL/PxN75YaouJ+UG0FssEy6cf49OdTF3kstAqVMHskc+zEqyRoiQHZXHwgA==}
|
||||
storybook@10.6.3:
|
||||
resolution: {integrity: sha512-lNBvcZFG44C98KvQHYzfq1O46m+sl94zY1+na5/9E3ihtNMbvpOgYyMFMIcWYUvOMo3OM4q9jzvB73KU20bU8w==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
@@ -9465,8 +9453,8 @@ packages:
|
||||
ts-easing@0.2.0:
|
||||
resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==}
|
||||
|
||||
ts-jest@29.4.9:
|
||||
resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==}
|
||||
ts-jest@29.4.15:
|
||||
resolution: {integrity: sha512-s2qmEUYFRqbjz1wv0EmccNPVOb45HOFpFuhzQJLKShYVisuTbCFPIIAIV2EuK5rIxb4BejLDbH7dnBZfYjybUQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -11351,7 +11339,7 @@ snapshots:
|
||||
react: 18.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@emnapi/core@1.11.2':
|
||||
'@emnapi/core@1.11.0':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.2.2
|
||||
tslib: 2.8.1
|
||||
@@ -11369,7 +11357,7 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.11.2':
|
||||
'@emnapi/runtime@1.11.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
@@ -12090,6 +12078,8 @@ snapshots:
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.6.0': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.1
|
||||
@@ -12195,10 +12185,10 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)':
|
||||
'@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.2
|
||||
'@emnapi/runtime': 1.11.2
|
||||
'@emnapi/core': 1.11.0
|
||||
'@emnapi/runtime': 1.11.0
|
||||
'@tybys/wasm-util': 0.10.3
|
||||
optional: true
|
||||
|
||||
@@ -12411,65 +12401,65 @@ snapshots:
|
||||
|
||||
'@oxc-project/types@0.127.0': {}
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.24.2':
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-android-arm64@11.24.2':
|
||||
'@oxc-resolver/binding-android-arm64@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-darwin-arm64@11.24.2':
|
||||
'@oxc-resolver/binding-darwin-arm64@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-darwin-x64@11.24.2':
|
||||
'@oxc-resolver/binding-darwin-x64@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-freebsd-x64@11.24.2':
|
||||
'@oxc-resolver/binding-freebsd-x64@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2':
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf@11.24.2':
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-gnu@11.24.2':
|
||||
'@oxc-resolver/binding-linux-arm64-gnu@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.24.2':
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.24.2':
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.24.2':
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.24.2':
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.24.2':
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.24.2':
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.24.2':
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.24.2':
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-wasm32-wasi@11.24.2':
|
||||
'@oxc-resolver/binding-wasm32-wasi@11.21.2':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.2
|
||||
'@emnapi/runtime': 1.11.2
|
||||
'@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)
|
||||
'@emnapi/core': 1.11.0
|
||||
'@emnapi/runtime': 1.11.0
|
||||
'@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-win32-arm64-msvc@11.24.2':
|
||||
'@oxc-resolver/binding-win32-arm64-msvc@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.24.2':
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.21.2':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-android-arm-eabi@0.73.0':
|
||||
@@ -13412,49 +13402,32 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@storybook/addon-a11y@10.5.9(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
|
||||
'@storybook/addon-a11y@10.6.3(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
axe-core: 4.13.0
|
||||
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
storybook: 10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
|
||||
'@storybook/addon-docs@10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
|
||||
'@storybook/addon-docs@10.6.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.1(@types/react@18.0.26)(react@18.2.0)
|
||||
'@storybook/csf-plugin': 10.5.9(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
'@storybook/icons': 2.1.0(react@18.2.0)
|
||||
'@storybook/react-dom-shim': 10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
'@storybook/react-dom-shim': 10.6.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
storybook: 10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
ts-dedent: 2.3.0
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
transitivePeerDependencies:
|
||||
- '@types/react-dom'
|
||||
- esbuild
|
||||
- rollup
|
||||
- vite
|
||||
- webpack
|
||||
|
||||
'@storybook/builder-vite@10.5.9(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
|
||||
'@storybook/builder-vite@10.6.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 10.5.9(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
storybook: 10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
ts-dedent: 2.3.0
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1)
|
||||
transitivePeerDependencies:
|
||||
- esbuild
|
||||
- rollup
|
||||
- webpack
|
||||
|
||||
'@storybook/csf-plugin@10.5.9(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
|
||||
dependencies:
|
||||
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
esbuild: 0.28.1
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1)
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
|
||||
@@ -13462,28 +13435,28 @@ snapshots:
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
|
||||
'@storybook/react-dom-shim@10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
|
||||
'@storybook/react-dom-shim@10.6.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
storybook: 10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@storybook/react-vite@10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(esbuild@0.28.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(typescript@5.9.3)':
|
||||
'@storybook/react-vite@10.6.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(typescript@5.9.3)
|
||||
'@rollup/pluginutils': 5.4.0
|
||||
'@storybook/builder-vite': 10.5.9(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
'@storybook/react': 10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(typescript@5.9.3)
|
||||
'@storybook/builder-vite': 10.6.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
'@storybook/react': 10.6.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(typescript@5.9.3)
|
||||
empathic: 2.0.1
|
||||
magic-string: 0.30.8
|
||||
magic-string: 1.4.3
|
||||
react: 18.2.0
|
||||
react-docgen: 8.0.3
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
resolve: 1.22.11
|
||||
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
storybook: 10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
tsconfig-paths: 4.2.0
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1)
|
||||
optionalDependencies:
|
||||
@@ -13491,20 +13464,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
- esbuild
|
||||
- rollup
|
||||
- supports-color
|
||||
- webpack
|
||||
|
||||
'@storybook/react@10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(typescript@5.9.3)':
|
||||
'@storybook/react@10.6.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
'@storybook/react-dom-shim': 10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
'@storybook/react-dom-shim': 10.6.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
|
||||
react: 18.2.0
|
||||
react-docgen: 8.0.3
|
||||
react-docgen-typescript: 2.4.0(typescript@5.9.3)
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
storybook: 10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
@@ -13512,7 +13483,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@storybook/test-runner@0.24.5(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3))':
|
||||
'@storybook/test-runner@0.24.5(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/generator': 7.29.7
|
||||
@@ -13534,7 +13505,7 @@ snapshots:
|
||||
playwright: 1.57.0
|
||||
playwright-core: 1.57.0
|
||||
rimraf: 3.0.2
|
||||
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
storybook: 10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
|
||||
uuid: 8.3.2
|
||||
transitivePeerDependencies:
|
||||
- '@swc/helpers'
|
||||
@@ -13674,7 +13645,7 @@ snapshots:
|
||||
'@testing-library/jest-dom@6.9.1':
|
||||
dependencies:
|
||||
'@adobe/css-tools': 4.5.0
|
||||
aria-query: 5.1.3
|
||||
aria-query: 5.3.0
|
||||
css.escape: 1.5.1
|
||||
dom-accessibility-api: 0.6.3
|
||||
picocolors: 1.1.1
|
||||
@@ -14662,16 +14633,16 @@ snapshots:
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
brace-expansion@1.1.18:
|
||||
brace-expansion@1.1.21:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
brace-expansion@2.1.4:
|
||||
brace-expansion@2.1.7:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
brace-expansion@5.0.9:
|
||||
brace-expansion@5.0.12:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
|
||||
@@ -16125,7 +16096,7 @@ snapshots:
|
||||
|
||||
graphql@16.8.1: {}
|
||||
|
||||
handlebars@4.7.9:
|
||||
handlebars@4.7.10:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
neo-async: 2.6.2
|
||||
@@ -17498,6 +17469,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
magic-string@1.4.3:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.6.0
|
||||
|
||||
make-dir@3.1.0:
|
||||
dependencies:
|
||||
semver: 6.3.1
|
||||
@@ -17908,19 +17883,19 @@ snapshots:
|
||||
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.9
|
||||
brace-expansion: 5.0.12
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.18
|
||||
brace-expansion: 1.1.21
|
||||
|
||||
minimatch@5.1.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.4
|
||||
brace-expansion: 2.1.7
|
||||
|
||||
minimatch@9.0.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.4
|
||||
brace-expansion: 2.1.7
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
@@ -18304,27 +18279,27 @@ snapshots:
|
||||
'@oxc-parser/binding-win32-ia32-msvc': 0.127.0
|
||||
'@oxc-parser/binding-win32-x64-msvc': 0.127.0
|
||||
|
||||
oxc-resolver@11.24.2:
|
||||
oxc-resolver@11.21.2:
|
||||
optionalDependencies:
|
||||
'@oxc-resolver/binding-android-arm-eabi': 11.24.2
|
||||
'@oxc-resolver/binding-android-arm64': 11.24.2
|
||||
'@oxc-resolver/binding-darwin-arm64': 11.24.2
|
||||
'@oxc-resolver/binding-darwin-x64': 11.24.2
|
||||
'@oxc-resolver/binding-freebsd-x64': 11.24.2
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2
|
||||
'@oxc-resolver/binding-linux-arm64-gnu': 11.24.2
|
||||
'@oxc-resolver/binding-linux-arm64-musl': 11.24.2
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2
|
||||
'@oxc-resolver/binding-linux-riscv64-musl': 11.24.2
|
||||
'@oxc-resolver/binding-linux-s390x-gnu': 11.24.2
|
||||
'@oxc-resolver/binding-linux-x64-gnu': 11.24.2
|
||||
'@oxc-resolver/binding-linux-x64-musl': 11.24.2
|
||||
'@oxc-resolver/binding-openharmony-arm64': 11.24.2
|
||||
'@oxc-resolver/binding-wasm32-wasi': 11.24.2
|
||||
'@oxc-resolver/binding-win32-arm64-msvc': 11.24.2
|
||||
'@oxc-resolver/binding-win32-x64-msvc': 11.24.2
|
||||
'@oxc-resolver/binding-android-arm-eabi': 11.21.2
|
||||
'@oxc-resolver/binding-android-arm64': 11.21.2
|
||||
'@oxc-resolver/binding-darwin-arm64': 11.21.2
|
||||
'@oxc-resolver/binding-darwin-x64': 11.21.2
|
||||
'@oxc-resolver/binding-freebsd-x64': 11.21.2
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf': 11.21.2
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf': 11.21.2
|
||||
'@oxc-resolver/binding-linux-arm64-gnu': 11.21.2
|
||||
'@oxc-resolver/binding-linux-arm64-musl': 11.21.2
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu': 11.21.2
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu': 11.21.2
|
||||
'@oxc-resolver/binding-linux-riscv64-musl': 11.21.2
|
||||
'@oxc-resolver/binding-linux-s390x-gnu': 11.21.2
|
||||
'@oxc-resolver/binding-linux-x64-gnu': 11.21.2
|
||||
'@oxc-resolver/binding-linux-x64-musl': 11.21.2
|
||||
'@oxc-resolver/binding-openharmony-arm64': 11.21.2
|
||||
'@oxc-resolver/binding-wasm32-wasi': 11.21.2
|
||||
'@oxc-resolver/binding-win32-arm64-msvc': 11.21.2
|
||||
'@oxc-resolver/binding-win32-x64-msvc': 11.21.2
|
||||
|
||||
oxfmt@0.73.0:
|
||||
dependencies:
|
||||
@@ -19922,7 +19897,7 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
internal-slot: 1.1.0
|
||||
|
||||
storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0):
|
||||
storybook@10.6.3(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0):
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
'@storybook/icons': 2.1.0(react@18.2.0)
|
||||
@@ -19936,7 +19911,7 @@ snapshots:
|
||||
jsonc-parser: 3.3.1
|
||||
open: 10.2.0
|
||||
oxc-parser: 0.127.0
|
||||
oxc-resolver: 11.24.2
|
||||
oxc-resolver: 11.21.2
|
||||
recast: 0.23.21
|
||||
semver: 7.8.5
|
||||
use-sync-external-store: 1.6.0(react@18.2.0)
|
||||
@@ -20254,11 +20229,11 @@ snapshots:
|
||||
|
||||
ts-easing@0.2.0: {}
|
||||
|
||||
ts-jest@29.4.9(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.2.0)(babel-jest@29.6.4(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@30.4.1)(jest@30.2.0(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3)))(typescript@5.9.3):
|
||||
ts-jest@29.4.15(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.2.0)(babel-jest@29.6.4(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@30.4.1)(jest@30.2.0(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3)))(typescript@5.9.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
handlebars: 4.7.9
|
||||
handlebars: 4.7.10
|
||||
jest: 30.2.0(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2)(@types/node@16.18.25)(typescript@5.9.3))
|
||||
json5: 2.2.3
|
||||
lodash.memoize: 4.1.2
|
||||
|
||||
@@ -20,13 +20,13 @@ overrides:
|
||||
'@xmldom/xmldom@<0.8.15': '>=0.8.15 <0.9'
|
||||
# via: babel-plugin-istanbul > test-exclude@6 > minimatch@3.1.5 (^1.1.7); also glob@7
|
||||
# remove: blocked — babel-plugin-istanbul pins test-exclude@6, which pins minimatch@3
|
||||
brace-expansion@<1.1.18: '>=1.1.18 <2'
|
||||
brace-expansion@<1.1.21: '>=1.1.21 <2'
|
||||
# via: jest > glob@10 > minimatch@9.0.9 (^2.0.1); also minimatch@5.1.9 (^2.0.1)
|
||||
# remove: blocked — jest 30 resolves glob@10 > minimatch@9 internally
|
||||
'brace-expansion@>=2.0.0 <2.1.4': '>=2.1.4 <3'
|
||||
'brace-expansion@>=2.0.0 <2.1.7': '>=2.1.7 <3'
|
||||
# via: eslint-plugin-sonarjs@4.0.2 (minimatch ^10.2.4) > minimatch@10.2.5 (^5.0.5)
|
||||
# remove: blocked — minimatch@10.2.6 (latest) only widens to ^5.0.8, still vulnerable
|
||||
'brace-expansion@>=5.0.0 <5.0.9': '>=5.0.9 <6'
|
||||
'brace-expansion@>=5.0.0 <5.0.12': '>=5.0.12 <6'
|
||||
# via: direct devDep @babel/core > @babel/helper-compilation-targets (browserslist ^4.24.0)
|
||||
# remove: blocked, the range is open so the floor is what pulls the fix.
|
||||
# Also lifts baseline-browser-mapping, which browserslist@4.28.7 floors at ^2.11.20
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
"tooltip_opsgenie_api_key": "Learn how to obtain the API key from your OpsGenie account [here](https://support.atlassian.com/opsgenie/docs/integrate-opsgenie-with-prometheus/).",
|
||||
"tooltip_email_to": "Enter email addresses separated by commas.",
|
||||
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to. It must be an https URL on chat.googleapis.com.",
|
||||
"google_chat_webhook_url_invalid": "Webhook URL must be an https URL on chat.googleapis.com",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to.",
|
||||
"field_jira_site": "Site URL",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net. Only Jira Cloud is supported.",
|
||||
"jira_site_invalid": "Site URL must be an https URL on an atlassian.net domain",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
|
||||
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
|
||||
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
|
||||
"jira_service_account_tip_link": "Learn how",
|
||||
@@ -82,7 +80,6 @@
|
||||
"field_incidentio_description": "Description",
|
||||
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
|
||||
"incidentio_required_fields": "Alert source URL and token are required",
|
||||
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
|
||||
"field_incidentio_metadata": "Additional metadata",
|
||||
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
|
||||
"placeholder_incidentio_metadata_key": "Key",
|
||||
@@ -92,8 +89,8 @@
|
||||
"field_slack_recipient": "Recipient",
|
||||
"field_slack_title": "Title",
|
||||
"field_slack_description": "Description",
|
||||
"field_webhook_username": "User Name (optional)",
|
||||
"field_webhook_password": "Password (optional)",
|
||||
"field_webhook_username": "Username",
|
||||
"field_webhook_password": "Password",
|
||||
"field_pager_routing_key": "Routing Key",
|
||||
"field_pager_description": "Description",
|
||||
"field_pager_severity": "Severity",
|
||||
@@ -112,13 +109,10 @@
|
||||
"help_pager_group": "A cluster or grouping of sources",
|
||||
"help_pager_component": "The part or component of the affected system that is broke",
|
||||
"help_pager_severity": "Severity of the incident, must be one of: must be one of the following: 'critical', 'warning', 'error' or 'info'",
|
||||
"help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.",
|
||||
"help_webhook_password": "Specify a password or bearer token",
|
||||
"help_pager_description": "Shows up as description in pagerduty",
|
||||
"channel_creation_done": "Successfully created the channel",
|
||||
"channel_edit_done": "Channels Edited Successfully",
|
||||
"selected_channel_invalid": "Channel type selected is invalid",
|
||||
"username_no_password": "A Password must be provided with user name",
|
||||
"channel_test_done": "An alert has been sent to this channel",
|
||||
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
||||
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"name_of_the_view": "Name of the view"
|
||||
}
|
||||
@@ -47,7 +47,6 @@
|
||||
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
||||
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
|
||||
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
|
||||
@@ -25,11 +25,9 @@
|
||||
"tooltip_opsgenie_api_key": "Learn how to obtain the API key from your OpsGenie account [here](https://support.atlassian.com/opsgenie/docs/integrate-opsgenie-with-prometheus/).",
|
||||
"tooltip_email_to": "Enter email addresses separated by commas.",
|
||||
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to. It must be an https URL on chat.googleapis.com.",
|
||||
"google_chat_webhook_url_invalid": "Webhook URL must be an https URL on chat.googleapis.com",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to.",
|
||||
"field_jira_site": "Site URL",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net. Only Jira Cloud is supported.",
|
||||
"jira_site_invalid": "Site URL must be an https URL on an atlassian.net domain",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
|
||||
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
|
||||
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
|
||||
"jira_service_account_tip_link": "Learn how",
|
||||
@@ -90,7 +88,8 @@
|
||||
"field_incidentio_description": "Description",
|
||||
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
|
||||
"incidentio_required_fields": "Alert source URL and token are required",
|
||||
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
|
||||
"incidentio_token_bearer_prefix": "Paste the token only, without the Bearer prefix",
|
||||
"incidentio_token_whitespace": "Remove the spaces around the token",
|
||||
"field_incidentio_metadata": "Additional metadata",
|
||||
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
|
||||
"placeholder_incidentio_metadata_key": "Key",
|
||||
@@ -116,6 +115,7 @@
|
||||
"field_slack_field_short": "Short",
|
||||
"add_slack_field": "Add field",
|
||||
"remove_slack_field": "Remove field",
|
||||
"slack_field_value_required": "Enter a value for this field",
|
||||
"field_slack_actions": "Actions",
|
||||
"help_slack_actions": "Buttons rendered under the attachment. A button with a URL links out.",
|
||||
"label_slack_action_text": "Text",
|
||||
@@ -133,6 +133,7 @@
|
||||
"placeholder_slack_action_style": "Style: default, primary or danger",
|
||||
"placeholder_slack_action_confirm": "Confirmation prompt (optional)",
|
||||
"add_slack_action": "Add action",
|
||||
"slack_action_url_or_name_required": "Enter a URL or a name for this button",
|
||||
"remove_slack_action": "Remove action",
|
||||
"field_opsgenie_api_key": "API Key",
|
||||
"field_opsgenie_description": "Description",
|
||||
@@ -143,9 +144,15 @@
|
||||
"help_email_html": "Send email in html format",
|
||||
"field_email_html": "Email body template",
|
||||
"placeholder_email_html": "Email body template",
|
||||
"field_webhook_username": "User Name (optional)",
|
||||
"field_webhook_password": "Password (optional)",
|
||||
"field_webhook_bearer_token": "Bearer token (optional)",
|
||||
"field_webhook_username": "Username",
|
||||
"field_webhook_password": "Password",
|
||||
"field_webhook_bearer_token": "Bearer token",
|
||||
"field_webhook_auth": "Authentication",
|
||||
"option_webhook_auth_none": "None",
|
||||
"option_webhook_auth_basic": "Basic auth",
|
||||
"option_webhook_auth_bearer": "Bearer token",
|
||||
"webhook_basic_auth_required": "Enter a username or password",
|
||||
"webhook_bearer_token_required": "Enter a bearer token",
|
||||
"field_pager_routing_key": "Routing Key",
|
||||
"field_pager_description": "Description",
|
||||
"field_pager_severity": "Severity",
|
||||
@@ -168,9 +175,7 @@
|
||||
"help_pager_group": "A cluster or grouping of sources",
|
||||
"help_pager_component": "The part or component of the affected system that is broke",
|
||||
"help_pager_severity": "Severity of the incident, must be one of: must be one of the following: 'critical', 'warning', 'error' or 'info'",
|
||||
"help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.",
|
||||
"help_webhook_password": "Specify a password or bearer token",
|
||||
"help_webhook_bearer_token": "Sent as an Authorization: Bearer header. Leave the username and password empty when using it.",
|
||||
"help_webhook_bearer_token": "Sent as an Authorization: Bearer header.",
|
||||
"help_pager_description": "Shows up as description in pagerduty",
|
||||
"help_opsgenie_message": "Shows up as message in opsgenie",
|
||||
"help_opsgenie_priority": "Priority of the incident",
|
||||
@@ -178,7 +183,6 @@
|
||||
"channel_creation_done": "Successfully created the channel",
|
||||
"channel_edit_done": "Channels Edited Successfully",
|
||||
"selected_channel_invalid": "Channel type selected is invalid",
|
||||
"username_no_password": "A Password must be provided with user name",
|
||||
"channel_test_done": "An alert has been sent to this channel",
|
||||
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
||||
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name_of_the_view": "Name of the view",
|
||||
"delete_confirm_message": "Are you sure you want to delete {{viewName}} view? Deleting a view is irreversible and cannot be undone."
|
||||
}
|
||||
@@ -50,8 +50,6 @@
|
||||
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
|
||||
"WORKSPACE_ACCESS_RESTRICTED": "SigNoz | Workspace Access Restricted",
|
||||
"SUPPORT": "SigNoz | Support",
|
||||
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
|
||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
||||
"TRACES_FUNNELS": "SigNoz | Traces Funnels",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||
"SHORTCUTS": "SigNoz | Shortcuts",
|
||||
@@ -66,10 +64,8 @@
|
||||
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
|
||||
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
|
||||
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
|
||||
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
|
||||
"API_MONITORING": "SigNoz | External APIs",
|
||||
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
|
||||
@@ -303,10 +303,10 @@ describe('PrivateRoute', () => {
|
||||
it.each([
|
||||
['/logs-save-views', ROUTES.LOGS_EXPLORER],
|
||||
['/traces-save-views', ROUTES.TRACES_EXPLORER],
|
||||
[ROUTES.LOGS_SAVE_VIEWS, ROUTES.LOGS_EXPLORER],
|
||||
[ROUTES.TRACES_SAVE_VIEWS, ROUTES.TRACES_EXPLORER],
|
||||
[ROUTES.METRICS_EXPLORER_VIEWS, ROUTES.METRICS_EXPLORER_EXPLORER],
|
||||
[ROUTES.METER_EXPLORER_VIEWS, ROUTES.METER_EXPLORER],
|
||||
['/logs/saved-views', ROUTES.LOGS_EXPLORER],
|
||||
['/traces/saved-views', ROUTES.TRACES_EXPLORER],
|
||||
['/metrics-explorer/views', ROUTES.METRICS_EXPLORER_EXPLORER],
|
||||
['/meter/explorer/views', ROUTES.METER_EXPLORER],
|
||||
])('should redirect the old views route %s to %s', (from, to) => {
|
||||
renderPrivateRoute({ initialRoute: from });
|
||||
|
||||
|
||||
88
frontend/src/AppRoutes/__tests__/sentryFilters.test.ts
Normal file
88
frontend/src/AppRoutes/__tests__/sentryFilters.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { ErrorEvent } from '@sentry/react';
|
||||
|
||||
import {
|
||||
isExternalIntegrationError,
|
||||
SENTRY_IGNORE_ERRORS,
|
||||
} from '../sentryFilters';
|
||||
|
||||
const APP_BUNDLE = 'https://example.us.signoz.cloud/assets/index-B3QqK3ie.js';
|
||||
|
||||
function eventWithFrames(...filenames: string[]): ErrorEvent {
|
||||
return {
|
||||
type: undefined,
|
||||
exception: {
|
||||
values: [
|
||||
{
|
||||
type: 'TypeError',
|
||||
value: 'Failed to fetch',
|
||||
stacktrace: {
|
||||
frames: filenames.map((filename) => ({ filename, lineno: 1, colno: 1 })),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('isExternalIntegrationError', () => {
|
||||
it.each([
|
||||
[
|
||||
'PostHog recorder',
|
||||
'https://us-assets.i.posthog.com/static/posthog-recorder.js?v=1.298.0',
|
||||
],
|
||||
[
|
||||
'Pylon widget',
|
||||
'https://widget.usepylon.com/widget/00000000-0000-0000-0000-000000000000',
|
||||
],
|
||||
['Appcues', 'https://fast.appcues.com/123456.js'],
|
||||
])('drops an error with a %s frame', (_, url) => {
|
||||
expect(isExternalIntegrationError(eventWithFrames(APP_BUNDLE, url))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('drops it when the top frame is our bundle', () => {
|
||||
expect(
|
||||
isExternalIntegrationError(
|
||||
eventWithFrames(
|
||||
'https://us-assets.i.posthog.com/static/posthog-recorder.js?v=1.298.0',
|
||||
'<anonymous>',
|
||||
APP_BUNDLE,
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps an error from our bundle only', () => {
|
||||
expect(
|
||||
isExternalIntegrationError(eventWithFrames(APP_BUNDLE, '<anonymous>')),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps an error from a host that only contains an integration name', () => {
|
||||
expect(
|
||||
isExternalIntegrationError(
|
||||
eventWithFrames(
|
||||
'https://posthog.example.com/app.js',
|
||||
'https://notusepylon.com/widget.js',
|
||||
),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps an error without a stack trace', () => {
|
||||
expect(isExternalIntegrationError({ type: undefined })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SENTRY_IGNORE_ERRORS', () => {
|
||||
it('ignores the bundled PostHog recorder load failure', () => {
|
||||
expect(
|
||||
SENTRY_IGNORE_ERRORS.some((pattern) =>
|
||||
pattern.test(
|
||||
'Error: Called on script loaded before session recording is available',
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,6 @@ import AppLayout from 'container/AppLayout';
|
||||
import Hex from 'crypto-js/enc-hex';
|
||||
import HmacSHA256 from 'crypto-js/hmac-sha256';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
|
||||
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -38,6 +37,10 @@ import defaultRoutes, {
|
||||
LIST_LICENSES,
|
||||
SUPPORT_ROUTE,
|
||||
} from './routes';
|
||||
import {
|
||||
isExternalIntegrationError,
|
||||
SENTRY_IGNORE_ERRORS,
|
||||
} from './sentryFilters';
|
||||
|
||||
const appRouter = (children: ReactNode): ReactNode => (
|
||||
<Router history={history}>
|
||||
@@ -66,7 +69,6 @@ function App(): JSX.Element {
|
||||
} = useAppContext();
|
||||
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
||||
const isAIAssistantEnabled = useIsAIAssistantEnabled();
|
||||
const isSavedViewEnabled = useSavedViewEnabled();
|
||||
const chatSupport = useChatSupport();
|
||||
|
||||
const { hostname } = window.location;
|
||||
@@ -256,18 +258,8 @@ function App(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
// The bottom strip carries Support, so the floating bubble goes entirely.
|
||||
if (
|
||||
isSavedViewEnabled ||
|
||||
pathname === ROUTES.ONBOARDING ||
|
||||
pathname.startsWith('/public/dashboard/') ||
|
||||
pathname === '/ai-assistant' ||
|
||||
pathname.startsWith('/ai-assistant/')
|
||||
) {
|
||||
window.Pylon?.('hideChatBubble');
|
||||
} else {
|
||||
window.Pylon?.('showChatBubble');
|
||||
}
|
||||
}, [pathname, isSavedViewEnabled]);
|
||||
window.Pylon?.('hideChatBubble');
|
||||
}, [pathname]);
|
||||
|
||||
// Identity for the Pylon widget. Whether this user gets Pylon at all is
|
||||
// `useChatSupport`'s call — this only fills in who they are.
|
||||
@@ -322,16 +314,7 @@ function App(): JSX.Element {
|
||||
tunnel: window.signozBootData.settings.sentry.tunnel,
|
||||
environment: process.env.ENVIRONMENT,
|
||||
release: process.env.VERSION,
|
||||
// A tab that outlived a deploy requests hashed assets the new build no longer
|
||||
// has. `lazyRetry` recovers by reloading once, so this class is not worth
|
||||
// reporting. The stylesheet message is Vite's own; the module ones are the
|
||||
// same failure worded differently by Chromium, Firefox and Safari.
|
||||
ignoreErrors: [
|
||||
/Unable to preload CSS for/,
|
||||
/Failed to fetch dynamically imported module/,
|
||||
/error loading dynamically imported module/,
|
||||
/Importing a module script failed/,
|
||||
],
|
||||
ignoreErrors: SENTRY_IGNORE_ERRORS,
|
||||
integrations: [
|
||||
// Kept for the `transaction` tag used in routing, even though
|
||||
// tracing is disabled. Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
|
||||
@@ -366,6 +349,10 @@ function App(): JSX.Element {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isExternalIntegrationError(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sessionReplayUrl = posthog.get_session_replay_url?.({
|
||||
withTimestamp: true,
|
||||
});
|
||||
|
||||
@@ -26,10 +26,6 @@ export const ServiceMapPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
|
||||
);
|
||||
|
||||
export const LogsSaveViews = Loadable(
|
||||
() => import(/* webpackChunkName: "LogsSaveViews" */ 'pages/LogsModulePage'), // TODO: Add a wrapper so that the same component can be used in traces
|
||||
);
|
||||
|
||||
export const TracesExplorer = Loadable(
|
||||
() =>
|
||||
import(
|
||||
@@ -37,11 +33,6 @@ export const TracesExplorer = Loadable(
|
||||
),
|
||||
);
|
||||
|
||||
export const TracesSaveViews = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Traces Save Views" */ 'pages/TracesModulePage'),
|
||||
);
|
||||
|
||||
export const TracesFunnels = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Traces Funnels" */ 'pages/TracesModulePage'),
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
Login,
|
||||
Logs,
|
||||
LogsIndexToFields,
|
||||
LogsSaveViews,
|
||||
MessagingQueuesMainPage,
|
||||
MeterExplorerPage,
|
||||
MetricsExplorer,
|
||||
@@ -48,7 +47,6 @@ import {
|
||||
TracesExplorer,
|
||||
TracesFunnelDetails,
|
||||
TracesFunnels,
|
||||
TracesSaveViews,
|
||||
UnAuthorized,
|
||||
UsageExplorerPage,
|
||||
WorkspaceAccessRestricted,
|
||||
@@ -120,13 +118,6 @@ const routes: AppRoutes[] = [
|
||||
exact: true,
|
||||
key: 'SERVICE_MAP',
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGS_SAVE_VIEWS,
|
||||
component: LogsSaveViews,
|
||||
isPrivate: true,
|
||||
exact: true,
|
||||
key: 'LOGS_SAVE_VIEWS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
exact: true,
|
||||
@@ -218,13 +209,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'TRACES_EXPLORER',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACES_SAVE_VIEWS,
|
||||
exact: true,
|
||||
component: TracesSaveViews,
|
||||
isPrivate: true,
|
||||
key: 'TRACES_SAVE_VIEWS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACES_FUNNELS,
|
||||
exact: true,
|
||||
@@ -414,13 +398,6 @@ const routes: AppRoutes[] = [
|
||||
key: 'METRICS_EXPLORER_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.METRICS_EXPLORER_VIEWS,
|
||||
exact: true,
|
||||
component: MetricsExplorer,
|
||||
key: 'METRICS_EXPLORER_VIEWS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.METRICS_EXPLORER_VOLUME_CONTROL,
|
||||
exact: true,
|
||||
@@ -443,13 +420,6 @@ const routes: AppRoutes[] = [
|
||||
key: 'METER_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.METER_EXPLORER_VIEWS,
|
||||
exact: true,
|
||||
component: MeterExplorerPage,
|
||||
key: 'METER_EXPLORER_VIEWS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.API_MONITORING,
|
||||
exact: true,
|
||||
|
||||
32
frontend/src/AppRoutes/sentryFilters.ts
Normal file
32
frontend/src/AppRoutes/sentryFilters.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ErrorEvent } from '@sentry/react';
|
||||
|
||||
export const SENTRY_IGNORE_ERRORS = [
|
||||
// A tab that outlived a deploy requests hashed assets the new build no longer
|
||||
// has. `lazyRetry` recovers by reloading once, so this class is not worth
|
||||
// reporting. The stylesheet message is Vite's own; the module ones are the
|
||||
// same failure worded differently by Chromium, Firefox and Safari.
|
||||
/Unable to preload CSS for/,
|
||||
/Failed to fetch dynamically imported module/,
|
||||
/error loading dynamically imported module/,
|
||||
/Importing a module script failed/,
|
||||
// Thrown by the bundled posthog-js when its recorder script fails to load, so
|
||||
// its frames point at our own bundle and the URL check below misses it.
|
||||
/Called on script loaded before session recording is available/,
|
||||
];
|
||||
|
||||
// PostHog's lazy recorder, the Pylon widget and Appcues.
|
||||
const EXTERNAL_SCRIPT_URLS = [
|
||||
/^https?:\/\/[^/]+\.posthog\.com\//,
|
||||
/^https?:\/\/(?:[^/]+\.)?usepylon\.com\//,
|
||||
/^https?:\/\/(?:[^/]+\.)?appcues\.(?:com|net)\//,
|
||||
];
|
||||
|
||||
// Checks every frame, not only the top one: a failed fetch from the recorder
|
||||
// tops out in Sentry's own fetch wrapper, which is bundled with the app.
|
||||
export function isExternalIntegrationError(event: ErrorEvent): boolean {
|
||||
return (event.exception?.values ?? []).some((exception) =>
|
||||
(exception.stacktrace?.frames ?? []).some((frame) =>
|
||||
EXTERNAL_SCRIPT_URLS.some((url) => url.test(frame.filename ?? '')),
|
||||
),
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import {
|
||||
QueryKeyValueRequestProps,
|
||||
QueryKeyValueSuggestionsResponseProps,
|
||||
} from 'types/api/querySuggestions/types';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useGetFieldsValues` hook (or `getFieldsValues` fetcher) from
|
||||
* `api/generated/services/fields` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
export const getValueSuggestions = (
|
||||
props: QueryKeyValueRequestProps,
|
||||
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
|
||||
const { signal, key, searchText, signalSource, metricName } = props;
|
||||
|
||||
const encodedSignal = encodeURIComponent(signal);
|
||||
const encodedKey = encodeURIComponent(key);
|
||||
const encodedMetricName = encodeURIComponent(metricName || '');
|
||||
const encodedSearchText = encodeURIComponent(searchText);
|
||||
const encodedSource = encodeURIComponent(signalSource || '');
|
||||
|
||||
return axios.get(
|
||||
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`,
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
|
||||
|
||||
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
|
||||
axios.delete(`/explorer/views/${uuid}`);
|
||||
@@ -1,9 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { AllViewsProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const getAllViews = (
|
||||
sourcepage: DataSource | 'meter',
|
||||
): Promise<AxiosResponse<AllViewsProps>> =>
|
||||
axios.get(`/explorer/views?sourcePage=${sourcepage}`);
|
||||
@@ -1,24 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { ViewProps } from 'types/api/saveViews/types';
|
||||
|
||||
/**
|
||||
* Fetches a single saved view by ID (`GET /api/v1/explorer/views/{viewId}`).
|
||||
*
|
||||
* Hand-maintained alongside the other `api/saveView/*` clients — explorer views
|
||||
* are not in `docs/api/openapi.yml`, so Orval does not generate a hook here
|
||||
* (unlike e.g. `useGetChannelByID` under `api/generated/services/channels`).
|
||||
*
|
||||
* Used by the AI assistant "Open view" action to load `compositeQuery` and
|
||||
* navigate to the correct explorer without listing every view per source page.
|
||||
* See `container/AIAssistant/components/ActionsSection/utils/openSavedView.ts`.
|
||||
*/
|
||||
export interface GetViewByIdProps {
|
||||
status: string;
|
||||
data: ViewProps;
|
||||
}
|
||||
|
||||
export const getViewById = (
|
||||
viewKey: string,
|
||||
): Promise<AxiosResponse<GetViewByIdProps>> =>
|
||||
axios.get(`/explorer/views/${viewKey}`);
|
||||
@@ -1,16 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
|
||||
|
||||
export const saveView = ({
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
viewName,
|
||||
extraData,
|
||||
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
|
||||
axios.post('/explorer/views', {
|
||||
name: viewName,
|
||||
sourcePage,
|
||||
compositeQuery,
|
||||
extraData,
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import axios from 'api';
|
||||
import {
|
||||
UpdateViewPayloadProps,
|
||||
UpdateViewProps,
|
||||
} from 'types/api/saveViews/types';
|
||||
|
||||
export const updateView = ({
|
||||
compositeQuery,
|
||||
viewName,
|
||||
extraData,
|
||||
sourcePage,
|
||||
viewKey,
|
||||
}: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
|
||||
axios.put(`/explorer/views/${viewKey}`, {
|
||||
name: viewName,
|
||||
compositeQuery,
|
||||
extraData,
|
||||
sourcePage,
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
interface EmptyQuickFilterIconProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function EmptyQuickFilterIcon({
|
||||
width = 32,
|
||||
height = 32,
|
||||
className,
|
||||
}: EmptyQuickFilterIconProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M17.948 6.906h-6.62V6.24h6.62v.667ZM17.948 10.293h-6.62v-.667h6.62v.667Z"
|
||||
fill="#616161"
|
||||
/>
|
||||
<path
|
||||
d="M16.175 29.403H13.62V3.442s.238-.554 1.278-.554 1.278.554 1.278.554v25.96h-.002Z"
|
||||
fill="#9E9E9E"
|
||||
/>
|
||||
<path
|
||||
d="M15.52 2.971v20.974H13.62v1.91c.323.001.614.04.876.097a1.3 1.3 0 0 1 1.024 1.269v2.182h.656V3.442c-.002 0-.14-.318-.658-.471Z"
|
||||
fill="#757575"
|
||||
/>
|
||||
<path
|
||||
d="M22.363 2.737a5.382 5.382 0 0 0-5.382 5.402 5.365 5.365 0 0 0 5.382 5.382 5.378 5.378 0 0 0 5.382-5.382c0-2.975-2.407-5.402-5.382-5.402Z"
|
||||
fill="#2196F3"
|
||||
/>
|
||||
<path
|
||||
d="M24.714 4.749c-.338-.085-1.01-.174-2.349-.174-1.337 0-2.01.087-2.348.174-.2.05-.87.328-.87 1.077v4.618c0 .253.205.46.46.46h.17v.51c0 .15.12.27.268.27h.545c.149 0 .269-.12.269-.27v-.51h3.008v.51c0 .15.12.27.27.27h.544c.149 0 .269-.12.269-.27v-.51h.173a.46.46 0 0 0 .46-.46V5.826c0-.717-.669-1.029-.869-1.077Zm-3.802.366c0-.113.091-.204.205-.204h2.493c.113 0 .204.09.204.204v.362a.204.204 0 0 1-.204.205h-2.493a.204.204 0 0 1-.205-.205v-.362Zm.042 4.864a.138.138 0 0 1-.137.138h-.55a.469.469 0 0 1-.468-.469v-.246c0-.076.062-.138.138-.138h.548c.258 0 .47.209.47.469v.246Zm3.973-.33a.469.469 0 0 1-.468.468h-.55a.138.138 0 0 1-.137-.138v-.246c0-.258.209-.47.469-.47h.549c.075 0 .137.063.137.139v.246Zm.125-2.007c0 .297-.645.817-2.689.817-2.046 0-2.689-.482-2.689-.817V6.277c0-.089.09-.31.311-.31h4.791c.223 0 .276.224.276.31v1.365Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M18.861 24.652h-7.926a.506.506 0 0 1-.507-.507V14.99c0-.28.227-.507.507-.507h7.924c.28 0 .507.227.507.507v9.155c0 .28-.227.507-.505.507Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path
|
||||
opacity=".8"
|
||||
d="M18.006 23.139H11.79l-.017-7.236h6.217l.016 7.236Z"
|
||||
fill="#82AEC0"
|
||||
/>
|
||||
<path d="M14.66 23.234v-7.33h.444v7.33h-.445Z" fill="#F5F5F5" />
|
||||
<path d="M14.66 15.903h-2.887v.605h2.886v-.605Z" fill="#616161" />
|
||||
<path
|
||||
d="M18.03 21.878h-6.264v-.222h6.264v.223ZM18.03 20.412h-6.264v-.222h6.264v.222ZM17.99 18.945h-6.217v-.222h6.217v.222ZM17.99 17.481h-6.224v-.222h6.224v.222Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path
|
||||
d="M17.988 18.534H15.1v.605h2.887v-.605ZM14.672 19.999h-2.877v.604h2.877V20Z"
|
||||
fill="#616161"
|
||||
/>
|
||||
<path
|
||||
d="M10.935 14.817a.173.173 0 0 0-.173.173v9.155c0 .096.077.174.173.174h7.926a.173.173 0 0 0 .171-.174V14.99a.173.173 0 0 0-.173-.173h-7.924Zm-.84.173a.84.84 0 0 1 .84-.84h7.924a.84.84 0 0 1 .84.84v9.155a.84.84 0 0 1-.838.84h-7.926a.84.84 0 0 1-.84-.84V14.99Z"
|
||||
fill="#9E9E9E"
|
||||
/>
|
||||
<path
|
||||
d="M10.968 24.323c-.344.031-.344-.195-.344-.258V15.1c0-.251.204-.455.455-.455h7.693c.21 0 .305.126.262.384 0 0-.026-.165-.226-.165h-7.726a.237.237 0 0 0-.236.236v8.968c0 .21.122.256.122.256Z"
|
||||
fill="#757575"
|
||||
/>
|
||||
<path d="M12.268 4.933H4.695v6.577h7.573V4.933Z" fill="#FFCA28" />
|
||||
<path
|
||||
d="M11.846 4.933c.233 0 .422.189.422.422v5.733a.422.422 0 0 1-.422.422H5.12a.422.422 0 0 1-.423-.422V5.355c0-.233.19-.422.423-.422h6.726Zm0-.445H5.12a.868.868 0 0 0-.867.867v5.733c0 .478.389.867.867.867h6.726a.868.868 0 0 0 .867-.867V5.355a.866.866 0 0 0-.867-.867Z"
|
||||
fill="#9E9E9E"
|
||||
/>
|
||||
<path
|
||||
d="M12.27 7.308H4.696v-.444h7.575v.444ZM12.27 9.579H4.696v-.444h7.575v.444Z"
|
||||
fill="#FFFDE7"
|
||||
/>
|
||||
<path
|
||||
d="M7.066 5.664H5.162v.502h1.904v-.502ZM6.597 10.27H5.162v.503h1.435v-.502ZM11.648 10.27h-1.435v.503h1.435v-.502ZM11.648 5.664h-1.435v.502h1.435v-.502ZM10.302 7.968h-.827v.502h.827v-.502ZM11.648 7.968h-.826v.502h.826v-.502ZM7.802 7.968h-2.64v.502h2.64v-.502Z"
|
||||
fill="#757575"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
EmptyQuickFilterIcon.defaultProps = {
|
||||
width: 32,
|
||||
height: 32,
|
||||
className: '',
|
||||
};
|
||||
|
||||
export default EmptyQuickFilterIcon;
|
||||
1
frontend/src/assets/Logos/caddy.svg
Normal file
1
frontend/src/assets/Logos/caddy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#1F88C0" d="M11.094.47c-.842 0-1.696.092-2.552.288a11.37 11.37 0 0 0-4.87 2.423 10.632 10.632 0 0 0-2.36 2.826A10.132 10.132 0 0 0 .305 8.582c-.398 1.62-.4 3.336-.043 5.048.085.405.183.809.31 1.212a11.85 11.85 0 0 0 1.662 3.729 3.273 3.273 0 0 0-.086.427 3.323 3.323 0 0 0 2.848 3.71 3.279 3.279 0 0 0 1.947-.346c1.045.51 2.17.864 3.339 1.04a11.66 11.66 0 0 0 4.285-.155 11.566 11.566 0 0 0 4.936-2.485 10.643 10.643 0 0 0 2.352-2.894 11.164 11.164 0 0 0 1.356-4.424 11.214 11.214 0 0 0-.498-4.335c.175-.077.338-.175.486-.293a.444.444 89.992 0 0 .001 0c.402-.322.693-.794.777-1.342a2.146 2.146 0 0 0-1.79-2.434 2.115 2.115 0 0 0-1.205.171c-.038-.043-.078-.086-.113-.13a11.693 11.693 0 0 0-3.476-2.93 13.348 13.348 0 0 0-1.76-.81 13.55 13.55 0 0 0-2.06-.613A12.121 12.121 0 0 0 11.093.47Zm.714.328c.345-.004.688.01 1.028.042a9.892 9.892 0 0 1 2.743.639c.984.39 1.89.958 2.707 1.632.803.662 1.502 1.45 2.091 2.328.026.039.048.08.07.12a2.12 2.12 0 0 0-.435 2.646c-.158.114-.97.692-1.634 1.183-.414.308-.733.557-.733.557l.581.68s.296-.276.665-.638c.572-.562 1.229-1.233 1.395-1.403a2.122 2.122 0 0 0 1.907.677 11.229 11.229 0 0 1-.013 4.046 11.41 11.41 0 0 1-1.475 3.897 12.343 12.343 0 0 1-2.079 2.587c-1.19 1.125-2.633 2.022-4.306 2.531a10.826 10.826 0 0 1-3.973.484 11.04 11.04 0 0 1-3.057-.652 3.304 3.304 0 0 0 1.417-2.294 3.275 3.275 0 0 0-.294-1.842c.18-.162.403-.363.656-.6 1.015-.955 2.353-2.303 2.353-2.303l-.47-.599s-1.63.972-2.801 1.728c-.307.198-.573.378-.777.517a3.273 3.273 0 0 0-1.516-.611c-1.507-.198-2.927.672-3.487 2.017a10.323 10.323 0 0 1-.695-1.078A10.92 10.92 0 0 1 .728 14.8a10.35 10.35 0 0 1-.2-1.212c-.164-1.653.103-3.258.629-4.754a12.95 12.95 0 0 1 1.087-2.288c.57-.968 1.248-1.872 2.069-2.656A11.013 11.013 0 0 1 11.808.797Zm-.147 3.257a3.838 3.838 0 0 0-3.82 3.82v2.36h-.94c-.751 0-1.377.625-1.377 1.377v3.8h1.46v-3.718h9.354v6.264H10.02v1.46h6.4c.751 0 1.377-.625 1.377-1.377v-6.43c0-.751-.626-1.377-1.377-1.377h-.94v-2.36a3.838 3.838 0 0 0-3.82-3.819zm0 1.46a2.371 2.371 0 0 1 2.36 2.36v2.36H9.3v-2.36a2.372 2.372 0 0 1 2.36-2.36zm10.141.392a1.253 1.253 0 0 1 1.296 1.434c-.049.319-.217.59-.453.78-.266.213-.61.318-.968.264a1.253 1.253 0 0 1-1.045-1.42 1.255 1.255 0 0 1 1.17-1.058zM5.384 17.425a2.02 2.02 0 0 1 1.917 1.298c.116.3.159.628.114.967a2.015 2.015 0 0 1-2.249 1.728 2.016 2.016 0 0 1-1.727-2.25 2.017 2.017 0 0 1 1.945-1.743z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
frontend/src/assets/Logos/hono.svg
Normal file
1
frontend/src/assets/Logos/hono.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#E36002" d="M12.445.002a45.529 45.529 0 0 0-5.252 8.146 8.595 8.595 0 0 1-.555-.53 27.796 27.796 0 0 0-1.205-1.542 8.762 8.762 0 0 0-1.251 2.12 20.743 20.743 0 0 0-1.448 5.88 8.867 8.867 0 0 0 .338 3.468c1.312 3.48 3.794 5.593 7.445 6.337 3.055.438 5.755-.333 8.097-2.312 2.677-2.59 3.359-5.634 2.047-9.132a33.287 33.287 0 0 0-2.988-5.59A91.34 91.34 0 0 0 12.615.053a.216.216 0 0 0-.17-.051Zm-.336 3.906a50.93 50.93 0 0 1 4.794 6.552c.448.767.817 1.57 1.108 2.41.606 2.386-.044 4.354-1.951 5.904-1.845 1.298-3.87 1.683-6.072 1.156-2.376-.737-3.75-2.335-4.121-4.794a5.107 5.107 0 0 1 .242-2.266c.358-.908.79-1.774 1.3-2.601l1.446-2.121a397.33 397.33 0 0 0 3.254-4.24Z"/></svg>
|
||||
|
After Width: | Height: | Size: 758 B |
1
frontend/src/assets/Logos/pm2.svg
Normal file
1
frontend/src/assets/Logos/pm2.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.6 KiB |
@@ -1,43 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button } from 'antd';
|
||||
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { MessageSquareText } from '@signozhq/icons';
|
||||
|
||||
export default function ChatSupportGateway(): JSX.Element {
|
||||
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
|
||||
useState(false);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="chat-support-gateway">
|
||||
<Button
|
||||
className="chat-support-gateway-btn"
|
||||
onClick={(): void => {
|
||||
logEvent('Disabled Chat Support: Clicked', {
|
||||
source: `chat support icon`,
|
||||
page: pathname,
|
||||
});
|
||||
|
||||
setIsAddCreditCardModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<MessageSquareText size={24} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AddCreditCardModal
|
||||
open={isAddCreditCardModalOpen}
|
||||
onClose={(): void => setIsAddCreditCardModalOpen(false)}
|
||||
onAddCreditCard={(): void => {
|
||||
logEvent('Add Credit card modal: Clicked', {
|
||||
source: `chat support icon`,
|
||||
page: pathname,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button, Col, Popover, Row, Select, Space } from 'antd';
|
||||
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import axios from 'axios';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
|
||||
import useErrorNotification from 'hooks/useErrorNotification';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
|
||||
import MenuItemGenerator from './MenuItemGenerator';
|
||||
import SaveViewWithName from './SaveViewWithName';
|
||||
import {
|
||||
DropDownOverlay,
|
||||
ExplorerCardHeadContainer,
|
||||
OffSetCol,
|
||||
} from './styles';
|
||||
import { ExplorerCardProps } from './types';
|
||||
import { deleteViewHandler } from './utils';
|
||||
import { Ellipsis, Save, Share2, Trash2 } from '@signozhq/icons';
|
||||
|
||||
function ExplorerCard({
|
||||
sourcepage,
|
||||
children,
|
||||
}: ExplorerCardProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [, setCopyUrl] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onCopyUrlHandler = (): void => {
|
||||
setCopyUrl(window.location.href);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
updateAllQueriesOperators,
|
||||
isStagedQueryUpdated,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const {
|
||||
data: viewsData,
|
||||
isLoading,
|
||||
error,
|
||||
isRefetching,
|
||||
refetch: refetchAllView,
|
||||
} = useGetAllViews(sourcepage);
|
||||
|
||||
useErrorNotification(error);
|
||||
|
||||
const handleOpenChange = (newOpen = false): void => {
|
||||
setIsOpen(newOpen);
|
||||
};
|
||||
|
||||
const { viewName, viewKey } = useGetSavedViewParams();
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey:
|
||||
sourcepage === DataSource.TRACES
|
||||
? LOCALSTORAGE.TRACES_LIST_OPTIONS
|
||||
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: sourcepage,
|
||||
aggregateOperator: StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const isQueryUpdated = isStagedQueryUpdated(
|
||||
viewsData?.data?.data,
|
||||
viewKey,
|
||||
options,
|
||||
);
|
||||
|
||||
const { mutateAsync: updateViewAsync } = useUpdateView({
|
||||
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
|
||||
viewKey,
|
||||
extraData: '',
|
||||
sourcePage: sourcepage,
|
||||
viewName,
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteViewAsync } = useDeleteView(viewKey);
|
||||
|
||||
const showErrorNotification = (err: Error): void => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteHandler = (): void =>
|
||||
deleteViewHandler({
|
||||
deleteViewAsync,
|
||||
notifications,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
viewId: viewKey,
|
||||
viewKey,
|
||||
updateAllQueriesOperators,
|
||||
sourcePage: sourcepage,
|
||||
});
|
||||
|
||||
const onUpdateQueryHandler = (): void => {
|
||||
updateViewAsync(
|
||||
{
|
||||
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
|
||||
viewKey,
|
||||
extraData: '',
|
||||
sourcePage: sourcepage,
|
||||
viewName,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'View Updated Successfully',
|
||||
});
|
||||
refetchAllView();
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorNotification(err);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const moreOptionItems: DropdownItemType[] = [
|
||||
{
|
||||
type: 'item',
|
||||
value: 'delete',
|
||||
label: <Typography.Text strong>Delete</Typography.Text>,
|
||||
onClick: onDeleteHandler,
|
||||
prefix: <Trash2 size="md" />,
|
||||
},
|
||||
];
|
||||
|
||||
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
|
||||
const saveButtonIcon = isQueryUpdated ? null : <Save size="md" />;
|
||||
|
||||
const showSaveView = false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSaveView && (
|
||||
<ExplorerCardHeadContainer size="small">
|
||||
<Row align="middle">
|
||||
<Col span={6}>
|
||||
<Space>
|
||||
<Typography>Query Builder</Typography>
|
||||
<TextToolTip
|
||||
url={ExploreHeaderToolTip.url}
|
||||
text={ExploreHeaderToolTip.text}
|
||||
useFilledIcon={false}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<OffSetCol span={18}>
|
||||
<Space size="large">
|
||||
{viewsData?.data.data && viewsData?.data.data.length && (
|
||||
<Space>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
loading={isLoading || isRefetching}
|
||||
showSearch
|
||||
placeholder="Select a view"
|
||||
dropdownStyle={DropDownOverlay}
|
||||
dropdownMatchSelectWidth={false}
|
||||
optionLabelProp="value"
|
||||
value={viewName || undefined}
|
||||
>
|
||||
{viewsData?.data.data.map((view) => (
|
||||
<Select.Option key={view.id} value={view.name}>
|
||||
<MenuItemGenerator
|
||||
viewName={view.name}
|
||||
viewKey={viewKey}
|
||||
createdBy={view.createdBy}
|
||||
uuid={view.id}
|
||||
refetchAllView={refetchAllView}
|
||||
viewData={viewsData.data.data}
|
||||
sourcePage={sourcepage}
|
||||
/>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
)}
|
||||
{isQueryUpdated && (
|
||||
<Button type="primary" icon={<Save />} onClick={onUpdateQueryHandler}>
|
||||
Save changes
|
||||
</Button>
|
||||
)}
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
content={
|
||||
<SaveViewWithName
|
||||
sourcePage={sourcepage}
|
||||
handlePopOverClose={handleOpenChange}
|
||||
refetchAllView={refetchAllView}
|
||||
/>
|
||||
}
|
||||
showArrow={false}
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<Button
|
||||
type={saveButtonType}
|
||||
icon={saveButtonIcon}
|
||||
data-testid="traces-save-view-action"
|
||||
>
|
||||
{isQueryUpdated
|
||||
? SaveButtonText.SAVE_AS_NEW_VIEW
|
||||
: SaveButtonText.SAVE_VIEW}
|
||||
</Button>
|
||||
</Popover>
|
||||
<Share2 onClick={onCopyUrlHandler} size="md" />
|
||||
{viewKey && (
|
||||
<Dropdown
|
||||
items={moreOptionItems}
|
||||
nativeButton
|
||||
align="end"
|
||||
side="bottom"
|
||||
>
|
||||
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
|
||||
</Dropdown>
|
||||
)}
|
||||
</Space>
|
||||
</OffSetCol>
|
||||
</Row>
|
||||
</ExplorerCardHeadContainer>
|
||||
)}
|
||||
|
||||
<div>{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerCard;
|
||||
@@ -1,103 +0,0 @@
|
||||
import { MouseEvent, useCallback } from 'react';
|
||||
import { Trash2 } from '@signozhq/icons';
|
||||
import { Col, Row, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
|
||||
import { MenuItemContainer } from './styles';
|
||||
import { MenuItemLabelGeneratorProps } from './types';
|
||||
import {
|
||||
deleteViewHandler,
|
||||
getViewDetailsUsingViewKey,
|
||||
trimViewName,
|
||||
} from './utils';
|
||||
|
||||
function MenuItemGenerator({
|
||||
viewName,
|
||||
viewKey,
|
||||
createdBy,
|
||||
uuid,
|
||||
viewData,
|
||||
refetchAllView,
|
||||
sourcePage,
|
||||
}: MenuItemLabelGeneratorProps): JSX.Element {
|
||||
const { panelType, redirectWithQueryBuilderData, updateAllQueriesOperators } =
|
||||
useQueryBuilder();
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { mutateAsync: deleteViewAsync } = useDeleteView(uuid);
|
||||
|
||||
const onDeleteHandler = (event: MouseEvent<SVGSVGElement>): void => {
|
||||
event.stopPropagation();
|
||||
deleteViewHandler({
|
||||
deleteViewAsync,
|
||||
notifications,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
viewId: uuid,
|
||||
viewKey,
|
||||
updateAllQueriesOperators,
|
||||
sourcePage,
|
||||
});
|
||||
};
|
||||
|
||||
const onMenuItemSelectHandler = useCallback(
|
||||
({ key }: { key: string }): void => {
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(key, viewData);
|
||||
if (!currentViewDetails) {
|
||||
return;
|
||||
}
|
||||
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
|
||||
|
||||
handleExplorerTabChange(currentPanelType, {
|
||||
query,
|
||||
viewName: name,
|
||||
viewKey: id,
|
||||
});
|
||||
},
|
||||
[viewData, handleExplorerTabChange],
|
||||
);
|
||||
|
||||
const onLabelClickHandler = (): void => {
|
||||
onMenuItemSelectHandler({
|
||||
key: uuid,
|
||||
});
|
||||
};
|
||||
|
||||
const newViewName = trimViewName(viewName);
|
||||
|
||||
return (
|
||||
<MenuItemContainer onClick={onLabelClickHandler}>
|
||||
<Row justify="space-between">
|
||||
<Col span={22}>
|
||||
<Row>
|
||||
<Tooltip title={viewName}>
|
||||
<Typography.Text strong>{newViewName}</Typography.Text>
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row>
|
||||
<Typography.Text color="muted">Created by {createdBy}</Typography.Text>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Typography.Link>
|
||||
<Trash2
|
||||
role="img"
|
||||
aria-label="Delete view"
|
||||
onClick={onDeleteHandler}
|
||||
size="md"
|
||||
/>
|
||||
</Typography.Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</MenuItemContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default MenuItemGenerator;
|
||||
@@ -1,79 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Card, Form } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSaveView } from 'hooks/saveViews/useSaveView';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
|
||||
import { SaveButton } from './styles';
|
||||
import { SaveViewFormProps, SaveViewWithNameProps } from './types';
|
||||
import { saveViewHandler } from './utils';
|
||||
|
||||
function SaveViewWithName({
|
||||
sourcePage,
|
||||
handlePopOverClose,
|
||||
refetchAllView,
|
||||
}: SaveViewWithNameProps): JSX.Element {
|
||||
const [form] = Form.useForm<SaveViewFormProps>();
|
||||
const { t } = useTranslation(['explorer']);
|
||||
const { currentQuery, panelType, redirectWithQueryBuilderData } =
|
||||
useQueryBuilder();
|
||||
const { notifications } = useNotifications();
|
||||
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||
|
||||
const { isLoading, mutateAsync: saveViewAsync } = useSaveView({
|
||||
viewName: form.getFieldValue('viewName'),
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
extraData: '',
|
||||
});
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
saveViewHandler({
|
||||
compositeQuery,
|
||||
handlePopOverClose,
|
||||
extraData: '',
|
||||
notifications,
|
||||
panelType: panelType || PANEL_TYPES.LIST,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
saveViewAsync,
|
||||
sourcePage,
|
||||
viewName: form.getFieldValue('viewName'),
|
||||
form,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Typography>{t('name_of_the_view')}</Typography>
|
||||
<Form form={form} onFinish={onSaveHandler} requiredMark>
|
||||
<Form.Item
|
||||
name={['viewName']}
|
||||
required
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please enter view name',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="Enter Name" />
|
||||
</Form.Item>
|
||||
<SaveButton
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
loading={isLoading}
|
||||
data-testid="save-view-name-action-button"
|
||||
>
|
||||
Save
|
||||
</SaveButton>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SaveViewWithName;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { ViewProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const viewMockData: ViewProps[] = [
|
||||
{
|
||||
id: 'view1',
|
||||
name: 'View 1',
|
||||
createdBy: 'User 1',
|
||||
category: 'category 1',
|
||||
compositeQuery: {} as ICompositeMetricQuery,
|
||||
createdAt: '2021-07-07T06:31:00.000Z',
|
||||
updatedAt: '2021-07-07T06:33:00.000Z',
|
||||
extraData: '',
|
||||
sourcePage: DataSource.TRACES,
|
||||
tags: [],
|
||||
updatedBy: 'User 1',
|
||||
},
|
||||
{
|
||||
id: 'view2',
|
||||
name: 'View 2',
|
||||
createdBy: 'User 2',
|
||||
category: 'category 2',
|
||||
compositeQuery: {} as ICompositeMetricQuery,
|
||||
createdAt: '2021-07-07T06:30:00.000Z',
|
||||
updatedAt: '2021-07-07T06:30:00.000Z',
|
||||
extraData: '',
|
||||
sourcePage: DataSource.TRACES,
|
||||
tags: [],
|
||||
updatedBy: 'User 2',
|
||||
},
|
||||
];
|
||||
@@ -1,9 +0,0 @@
|
||||
export const ExploreHeaderToolTip = {
|
||||
url: 'https://signoz.io/docs/querying/overview/?utm_source=product&utm_medium=new-query-builder',
|
||||
text: 'More details on how to use query builder',
|
||||
};
|
||||
|
||||
export const SaveButtonText = {
|
||||
SAVE_AS_NEW_VIEW: 'Save as new view',
|
||||
SAVE_VIEW: 'Save view',
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Button, Card, Col } from 'antd';
|
||||
import styled, { CSSProperties } from 'styled-components';
|
||||
|
||||
export const ExplorerCardHeadContainer = styled(Card)`
|
||||
margin: 1rem 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const OffSetCol = styled(Col)`
|
||||
text-align: right;
|
||||
`;
|
||||
|
||||
export const SaveButton = styled(Button)`
|
||||
&&& {
|
||||
margin: 1rem 0;
|
||||
width: 5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DropDownOverlay: CSSProperties = {
|
||||
maxHeight: '20rem',
|
||||
overflowY: 'auto',
|
||||
width: '20rem',
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
export const MenuItemContainer = styled(Card)`
|
||||
padding: 0;
|
||||
`;
|
||||
@@ -1,96 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { viewMockData } from '../__mock__/viewData';
|
||||
import ExplorerCard from '../ExplorerCard';
|
||||
|
||||
const historyReplace = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
|
||||
}),
|
||||
useHistory: (): any => ({
|
||||
...jest.requireActual('react-router-dom').useHistory(),
|
||||
replace: historyReplace,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
|
||||
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useGetAllViews', () => ({
|
||||
useGetAllViews: jest.fn(() => ({
|
||||
data: { data: { data: viewMockData } },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isRefetching: false,
|
||||
refetch: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useUpdateView', () => ({
|
||||
useUpdateView: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useDeleteView', () => ({
|
||||
useDeleteView: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock usePreferenceSync
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 1,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ExplorerCard', () => {
|
||||
it('renders a card with a title and a description', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a save view button', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { viewMockData } from '../__mock__/viewData';
|
||||
import MenuItemGenerator from '../MenuItemGenerator';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('antd', () => ({
|
||||
...jest.requireActual('antd'),
|
||||
useForm: jest.fn().mockReturnValue({
|
||||
onFinish: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('MenuItemGenerator', () => {
|
||||
it('should render MenuItemGenerator component', () => {
|
||||
const screen = render(
|
||||
<MockQueryClientProvider>
|
||||
<MenuItemGenerator
|
||||
viewName={viewMockData[0].name}
|
||||
viewKey={viewMockData[0].id}
|
||||
createdBy={viewMockData[0].createdBy}
|
||||
uuid={viewMockData[0].id}
|
||||
refetchAllView={jest.fn()}
|
||||
viewData={viewMockData}
|
||||
sourcePage={DataSource.TRACES}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(viewMockData[0].name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onMenuItemSelectHandler on click of MenuItemGenerator', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<MenuItemGenerator
|
||||
viewName={viewMockData[0].name}
|
||||
viewKey={viewMockData[0].id}
|
||||
createdBy={viewMockData[0].createdBy}
|
||||
uuid={viewMockData[0].id}
|
||||
refetchAllView={jest.fn()}
|
||||
viewData={viewMockData}
|
||||
sourcePage={DataSource.TRACES}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
const spanElement = screen.getByRole('img', {
|
||||
name: /delete view/i,
|
||||
});
|
||||
|
||||
expect(spanElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import SaveViewWithName from '../SaveViewWithName';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
|
||||
}),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
|
||||
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useSaveView', () => ({
|
||||
useSaveView: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('SaveViewWithName', () => {
|
||||
it('should render SaveViewWithName component', () => {
|
||||
const screen = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SaveViewWithName
|
||||
sourcePage={DataSource.TRACES}
|
||||
handlePopOverClose={jest.fn()}
|
||||
refetchAllView={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Save')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call saveViewAsync on click of Save button', () => {
|
||||
const screen = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SaveViewWithName
|
||||
sourcePage={DataSource.TRACES}
|
||||
handlePopOverClose={jest.fn()}
|
||||
refetchAllView={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Save'));
|
||||
|
||||
expect(screen.getByText('Save')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
import { UseMutateAsyncFunction } from 'react-query';
|
||||
import { FormInstance } from 'antd';
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
DeleteViewPayloadProps,
|
||||
SaveViewPayloadProps,
|
||||
SaveViewProps,
|
||||
ViewProps,
|
||||
} from 'types/api/saveViews/types';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
export interface ExplorerCardProps {
|
||||
sourcepage: DataSource;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export type GetViewDetailsUsingViewKey = (
|
||||
viewKey: string,
|
||||
data: ViewProps[] | undefined,
|
||||
) =>
|
||||
| {
|
||||
query: Query;
|
||||
name: string;
|
||||
id: string;
|
||||
panelType: PANEL_TYPES;
|
||||
extraData?: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
export interface IsQueryUpdatedInViewProps {
|
||||
viewKey: string;
|
||||
data: ViewProps[] | undefined;
|
||||
stagedQuery: Query | null;
|
||||
currentPanelType: PANEL_TYPES | null;
|
||||
options: OptionsQuery;
|
||||
}
|
||||
|
||||
export interface SaveViewWithNameProps {
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
handlePopOverClose: VoidFunction;
|
||||
refetchAllView: VoidFunction;
|
||||
}
|
||||
|
||||
export interface SaveViewFormProps {
|
||||
viewName: string;
|
||||
}
|
||||
|
||||
export interface MenuItemLabelGeneratorProps {
|
||||
viewName: string;
|
||||
viewKey: string;
|
||||
createdBy: string;
|
||||
uuid: string;
|
||||
viewData: ViewProps[];
|
||||
refetchAllView: VoidFunction;
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
}
|
||||
|
||||
export interface SaveViewHandlerProps {
|
||||
viewName: string;
|
||||
compositeQuery: ICompositeMetricQuery;
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
extraData: string;
|
||||
panelType: PANEL_TYPES | null;
|
||||
notifications: NotificationInstance;
|
||||
refetchAllView: SaveViewWithNameProps['refetchAllView'];
|
||||
saveViewAsync: UseMutateAsyncFunction<
|
||||
AxiosResponse<SaveViewPayloadProps>,
|
||||
Error,
|
||||
SaveViewProps,
|
||||
SaveViewPayloadProps
|
||||
>;
|
||||
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
|
||||
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
|
||||
form: FormInstance<SaveViewFormProps>;
|
||||
}
|
||||
|
||||
export interface DeleteViewHandlerProps {
|
||||
deleteViewAsync: UseMutateAsyncFunction<DeleteViewPayloadProps, Error, string>;
|
||||
refetchAllView: MenuItemLabelGeneratorProps['refetchAllView'];
|
||||
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
|
||||
notifications: NotificationInstance;
|
||||
panelType: PANEL_TYPES | null;
|
||||
viewKey: string;
|
||||
viewId: string;
|
||||
updateAllQueriesOperators: QueryBuilderContextType['updateAllQueriesOperators'];
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
DeleteViewHandlerProps,
|
||||
GetViewDetailsUsingViewKey,
|
||||
IsQueryUpdatedInViewProps,
|
||||
SaveViewHandlerProps,
|
||||
} from './types';
|
||||
|
||||
export const showErrorNotification = (
|
||||
notifications: NotificationInstance,
|
||||
err: Error,
|
||||
): void => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
|
||||
});
|
||||
};
|
||||
|
||||
export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
|
||||
viewKey,
|
||||
data,
|
||||
) => {
|
||||
const selectedView = data?.find((view) => view.id === viewKey);
|
||||
if (selectedView) {
|
||||
const { compositeQuery, name, id, extraData } = selectedView;
|
||||
const query = mapQueryDataFromApi(compositeQuery);
|
||||
return { query, name, id, panelType: compositeQuery.panelType, extraData };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const omitIdFromQuery = (query: Query | null): any => ({
|
||||
...query,
|
||||
builder: {
|
||||
...query?.builder,
|
||||
queryData: query?.builder.queryData.map((queryData) => {
|
||||
const { id: _aggregateAttributeId, ...rest } =
|
||||
queryData.aggregateAttribute || {};
|
||||
const newAggregateAttribute = rest;
|
||||
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
||||
const { id: _groupByAttributeId, ...rest } = groupByAttribute;
|
||||
return rest;
|
||||
});
|
||||
const newItems = queryData.filters?.items?.map((item) => {
|
||||
const { id: _itemId, ...newItem } = item;
|
||||
if (item.key) {
|
||||
const { id: _keyId, ...rest } = item.key;
|
||||
return {
|
||||
...newItem,
|
||||
key: rest,
|
||||
};
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
return {
|
||||
...queryData,
|
||||
aggregateAttribute: newAggregateAttribute,
|
||||
groupBy: newGroupByAttributes,
|
||||
filters: {
|
||||
...queryData.filters,
|
||||
items: newItems,
|
||||
},
|
||||
limit: queryData.limit ? queryData.limit : 0,
|
||||
offset: queryData.offset ? queryData.offset : 0,
|
||||
pageSize: queryData.pageSize ? queryData.pageSize : 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const isQueryUpdatedInView = ({
|
||||
viewKey,
|
||||
data,
|
||||
stagedQuery,
|
||||
currentPanelType,
|
||||
options,
|
||||
}: IsQueryUpdatedInViewProps): boolean => {
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
|
||||
if (!currentViewDetails) {
|
||||
return false;
|
||||
}
|
||||
const { query, panelType, extraData } = currentViewDetails;
|
||||
|
||||
// Omitting id from aggregateAttribute and groupBy
|
||||
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
|
||||
|
||||
if (
|
||||
updatedCurrentQuery?.builder === undefined ||
|
||||
updatedCurrentQuery.clickhouse_sql === undefined ||
|
||||
updatedCurrentQuery.promql === undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
panelType !== currentPanelType ||
|
||||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
|
||||
!isEqual(query.clickhouse_sql, updatedCurrentQuery?.clickhouse_sql) ||
|
||||
!isEqual(query.promql, updatedCurrentQuery?.promql) ||
|
||||
!isEqual(
|
||||
options?.selectColumns,
|
||||
extraData && JSON.parse(extraData)?.selectColumns,
|
||||
) ||
|
||||
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
|
||||
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
|
||||
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
|
||||
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
|
||||
);
|
||||
};
|
||||
|
||||
export const saveViewHandler = ({
|
||||
saveViewAsync,
|
||||
refetchAllView,
|
||||
notifications,
|
||||
handlePopOverClose,
|
||||
viewName,
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
extraData,
|
||||
redirectWithQueryBuilderData,
|
||||
panelType,
|
||||
form,
|
||||
}: SaveViewHandlerProps): void => {
|
||||
saveViewAsync(
|
||||
{
|
||||
viewName,
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
extraData,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
refetchAllView();
|
||||
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
|
||||
[QueryParams.panelTypes]: panelType,
|
||||
[QueryParams.viewName]: viewName,
|
||||
[QueryParams.viewKey]: data.data.data,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'View Saved Successfully',
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorNotification(notifications, err);
|
||||
},
|
||||
onSettled: () => {
|
||||
handlePopOverClose();
|
||||
form.resetFields();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteViewHandler = ({
|
||||
deleteViewAsync,
|
||||
refetchAllView,
|
||||
redirectWithQueryBuilderData,
|
||||
notifications,
|
||||
panelType,
|
||||
viewKey,
|
||||
viewId,
|
||||
updateAllQueriesOperators,
|
||||
sourcePage,
|
||||
}: DeleteViewHandlerProps): void => {
|
||||
deleteViewAsync(viewKey, {
|
||||
onSuccess: () => {
|
||||
if (viewId === viewKey) {
|
||||
redirectWithQueryBuilderData(
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap[sourcePage],
|
||||
panelType || PANEL_TYPES.LIST,
|
||||
sourcePage,
|
||||
),
|
||||
{
|
||||
[QueryParams.viewName]: '',
|
||||
[QueryParams.panelTypes]: panelType,
|
||||
[QueryParams.viewKey]: '',
|
||||
},
|
||||
);
|
||||
}
|
||||
notifications.success({
|
||||
message: 'View Deleted Successfully',
|
||||
});
|
||||
refetchAllView();
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorNotification(notifications, err);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const trimViewName = (viewName: string): string => {
|
||||
if (viewName.length > 20) {
|
||||
return `${viewName.substring(0, 20)}...`;
|
||||
}
|
||||
return viewName;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DrawerProps } from 'antd';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getBuilderVariant } from 'lib/query/queryMode';
|
||||
import {
|
||||
BuilderVariant,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderField } from './queryBuilderFields.types';
|
||||
@@ -64,20 +59,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
[config],
|
||||
);
|
||||
|
||||
const isAIQuery =
|
||||
getBuilderVariant(currentQuery.builder) === BuilderVariant.AI;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
|
||||
// AI queries are traces-only; BAR must not force metrics on them.
|
||||
if (isAIQuery) {
|
||||
handleSetConfig(newPanelType, DataSource.TRACES);
|
||||
return;
|
||||
}
|
||||
if (newPanelType === PANEL_TYPES.BAR) {
|
||||
handleSetConfig(PANEL_TYPES.BAR, DataSource.METRICS);
|
||||
return;
|
||||
}
|
||||
handleSetConfig(newPanelType, currentDataSource);
|
||||
}
|
||||
}, [
|
||||
@@ -86,7 +69,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
initialDataSource,
|
||||
currentDataSource,
|
||||
newPanelType,
|
||||
isAIQuery,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -130,7 +130,6 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
updateAllQueriesOperators,
|
||||
updateQueriesData,
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
} as unknown as QueryBuilderContextType;
|
||||
|
||||
|
||||
@@ -136,7 +136,6 @@ describe('MetricsSelect - signal source switching (standalone)', () => {
|
||||
updateAllQueriesOperators: ((q: any) => q) as any,
|
||||
updateQueriesData: ((q: any) => q) as any,
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
});
|
||||
});
|
||||
@@ -293,7 +292,6 @@ describe('DataSource change - Logs to Traces', () => {
|
||||
updateAllQueriesOperators: ((q: any) => q) as any,
|
||||
updateQueriesData: ((q: any) => q) as any,
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('../QueryV2/QueryV2', () => ({ QueryV2: (): null => null }));
|
||||
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
|
||||
|
||||
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
|
||||
const aiQuery: Query = {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: initialQueriesMap.traces.builder.queryData.map((query) => ({
|
||||
...query,
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
function mockContext({
|
||||
panelType = null,
|
||||
initialDataSource = null,
|
||||
currentQuery = initialQueriesMap.metrics,
|
||||
}: {
|
||||
panelType?: PANEL_TYPES | null;
|
||||
initialDataSource?: DataSource | null;
|
||||
currentQuery?: Query;
|
||||
}): jest.Mock {
|
||||
const handleSetConfig = jest.fn();
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
currentQuery,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
handleSetConfig,
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
handleRunQuery: jest.fn(),
|
||||
} as unknown as ReturnType<typeof useQueryBuilder>);
|
||||
return handleSetConfig;
|
||||
}
|
||||
|
||||
describe('QueryBuilderV2 provider config sync', () => {
|
||||
it('leaves the data source unset for a bar panel without a static config', () => {
|
||||
const handleSetConfig = mockContext({});
|
||||
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.BAR} version="v3" />);
|
||||
|
||||
expect(handleSetConfig).toHaveBeenCalledWith(PANEL_TYPES.BAR, null);
|
||||
});
|
||||
|
||||
it('syncs the static data source', () => {
|
||||
const handleSetConfig = mockContext({});
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
version="v3"
|
||||
panelType={PANEL_TYPES.BAR}
|
||||
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(handleSetConfig).toHaveBeenCalledWith(
|
||||
PANEL_TYPES.BAR,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps an AI query on traces for a bar panel', () => {
|
||||
const handleSetConfig = mockContext({ currentQuery: aiQuery });
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
version="v3"
|
||||
panelType={PANEL_TYPES.BAR}
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(handleSetConfig).toHaveBeenCalledWith(
|
||||
PANEL_TYPES.BAR,
|
||||
DataSource.TRACES,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not reset the config when the provider already matches', () => {
|
||||
const handleSetConfig = mockContext({
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
initialDataSource: DataSource.TRACES,
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
version="v3"
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(handleSetConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,230 +0,0 @@
|
||||
.checkbox-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.search {
|
||||
input {
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
--input-focus-background: var(--l2-background);
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
--input-font-size: 14px;
|
||||
--input-border-color: var(--l1-border);
|
||||
--input-focus-border-color: var(--primary-background);
|
||||
--input-focus-outline-width: 0;
|
||||
--input-focus-outline-offset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-header-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
|
||||
.left-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.title {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.right-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 48px;
|
||||
|
||||
.clear-all {
|
||||
font-size: 12px;
|
||||
color: var(--accent-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.values {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.filter-separator {
|
||||
height: 1px;
|
||||
background-color: var(--l1-border);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 24px;
|
||||
|
||||
.checkbox-value-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: calc(100% - 24px);
|
||||
cursor: pointer;
|
||||
|
||||
.value-string {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.filter-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
.value-string {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.only-btn,
|
||||
.toggle-btn {
|
||||
cursor: not-allowed;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
// Stack "Only"/"All" and "Toggle" in a single cell so revealing
|
||||
// one swaps in place instead of shifting the row layout.
|
||||
.value-actions {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-items: end;
|
||||
|
||||
> * {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
}
|
||||
|
||||
.only-btn,
|
||||
.toggle-btn {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 21px;
|
||||
opacity: 0;
|
||||
transform: translateX(4px);
|
||||
// `display` is animated as a discrete property so the button
|
||||
// stays mounted through its fade-out on exit.
|
||||
transition:
|
||||
opacity 0.16s ease,
|
||||
transform 0.16s ease,
|
||||
display 0.16s allow-discrete;
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hovering the label/value area reveals the "Only"/"All" action.
|
||||
.checkbox-value-section:hover .only-btn {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
|
||||
// Entry animation start state (fires on the display switch).
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
// Hovering the checkbox reveals the "Toggle" action.
|
||||
[data-slot='checkbox']:hover ~ .checkbox-value-section .toggle-btn {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.only-btn,
|
||||
.toggle-btn {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.show-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.show-more-text {
|
||||
color: var(--accent-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.go-to-docs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 44px;
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
|
||||
&-message {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
&__button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
margin: 0 0 4px;
|
||||
padding: 0;
|
||||
&-text {
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label-false {
|
||||
width: 2px;
|
||||
height: 11px;
|
||||
border-radius: 2px;
|
||||
background: var(--danger-background);
|
||||
}
|
||||
|
||||
.label-true {
|
||||
width: 2px;
|
||||
height: 11px;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-forest-500);
|
||||
}
|
||||
@@ -1,476 +0,0 @@
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { FiltersType, QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
|
||||
import { quickFiltersAttributeValuesResponse } from 'mocks-server/__mockdata__/customQuickFilters';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import CheckboxFilter from './Checkbox';
|
||||
|
||||
// Mock the query builder hook
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
const mockUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
|
||||
// Mock the aggregate values hook
|
||||
jest.mock('hooks/queryBuilder/useGetAggregateValues');
|
||||
|
||||
const mockUseGetAggregateValues = jest.mocked(useGetAggregateValues);
|
||||
|
||||
// Mock the key value suggestions hook
|
||||
jest.mock('hooks/querySuggestions/useGetQueryKeyValueSuggestions');
|
||||
|
||||
const mockUseGetQueryKeyValueSuggestions = jest.mocked(
|
||||
useGetQueryKeyValueSuggestions,
|
||||
);
|
||||
|
||||
interface MockFilterConfig {
|
||||
title: string;
|
||||
attributeKey: {
|
||||
key: string;
|
||||
dataType: DataTypes;
|
||||
type: string;
|
||||
};
|
||||
dataSource: DataSource;
|
||||
defaultOpen: boolean;
|
||||
type: FiltersType;
|
||||
}
|
||||
|
||||
const SERVICE_NAME_KEY = 'service.name';
|
||||
const OTEL_DEMO = 'otel-demo';
|
||||
const SAMPLE_FLASK = 'sample-flask';
|
||||
const OTLP_PYTHON = 'otlp-python';
|
||||
const MQ_KAFKA = 'mq-kafka';
|
||||
|
||||
const MOCK_SERVICE_NAMES = [MQ_KAFKA, OTEL_DEMO, OTLP_PYTHON, SAMPLE_FLASK];
|
||||
|
||||
const createMockFilter = (
|
||||
overrides: Partial<MockFilterConfig> = {},
|
||||
): MockFilterConfig => ({
|
||||
title: 'Service Name',
|
||||
attributeKey: {
|
||||
key: SERVICE_NAME_KEY,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
dataSource: DataSource.LOGS,
|
||||
defaultOpen: false,
|
||||
type: FiltersType.CHECKBOX,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockQueryBuilderData = (hasActiveFilters = false): any => ({
|
||||
lastUsedQuery: 0,
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: {
|
||||
items: hasActiveFilters
|
||||
? [
|
||||
{
|
||||
key: {
|
||||
key: SERVICE_NAME_KEY,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
op: 'in',
|
||||
value: [OTEL_DEMO, SAMPLE_FLASK],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
});
|
||||
|
||||
describe('CheckboxFilter - User Flows', () => {
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Default mock implementations for useGetAggregateValues
|
||||
mockUseGetAggregateValues.mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
stringAttributeValues: MOCK_SERVICE_NAMES,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
} as unknown as UseQueryResult<SuccessResponse<IAttributeValuesResponse>>);
|
||||
|
||||
// Default mock implementations for useGetQueryKeyValueSuggestions
|
||||
// Returns data in the format expected by the hook
|
||||
mockUseGetQueryKeyValueSuggestions.mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
values: {
|
||||
stringValues: MOCK_SERVICE_NAMES,
|
||||
numberValues: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
} as any);
|
||||
|
||||
// Setup MSW server for API calls
|
||||
server.use(
|
||||
rest.get('*/api/v3/autocomplete/attribute_values', (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should auto-open filter and prioritize checked items with visual separator when user opens page with active filters', async () => {
|
||||
// Mock query builder with active filters
|
||||
mockUseQueryBuilder.mockReturnValue(createMockQueryBuilderData(true) as any);
|
||||
|
||||
const mockFilter = createMockFilter({ defaultOpen: false });
|
||||
|
||||
render(
|
||||
<CheckboxFilter
|
||||
filter={mockFilter}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
/>,
|
||||
);
|
||||
|
||||
// User should see the filter is automatically opened (not collapsed)
|
||||
expect(screen.getByText('Service Name')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// User should see visual separator between checked and unchecked items
|
||||
expect(screen.getByTestId('filter-separator')).toBeInTheDocument();
|
||||
|
||||
// User should see checked items at the top
|
||||
await waitFor(() => {
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
expect(checkboxes).toHaveLength(4); // Ensure we have exactly 4 checkboxes
|
||||
expect(checkboxes[0]).toBeChecked(); // otel-demo should be first and checked
|
||||
expect(checkboxes[1]).toBeChecked(); // sample-flask should be second and checked
|
||||
expect(checkboxes[2]).not.toBeChecked(); // mq-kafka should be unchecked
|
||||
expect(checkboxes[3]).not.toBeChecked(); // otlp-python should be unchecked
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect user preference when user manually toggles filter over auto-open behavior', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
// Mock query builder with active filters
|
||||
mockUseQueryBuilder.mockReturnValue(createMockQueryBuilderData(true) as any);
|
||||
|
||||
const mockFilter = createMockFilter({ defaultOpen: false });
|
||||
|
||||
render(
|
||||
<CheckboxFilter
|
||||
filter={mockFilter}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Initially auto-opened due to active filters
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// User manually closes the filter
|
||||
await user.click(screen.getByText('Service Name'));
|
||||
|
||||
// User should see filter is now closed (respecting user preference)
|
||||
expect(
|
||||
screen.queryByPlaceholderText('Filter values'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// User manually opens the filter again
|
||||
await user.click(screen.getByText('Service Name'));
|
||||
|
||||
// User should see filter is now open (respecting user preference)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update query filters when a checkbox is clicked', async () => {
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
|
||||
// Start with no active filters so clicking a checkbox creates one
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
...createMockQueryBuilderData(false),
|
||||
redirectWithQueryBuilderData,
|
||||
} as any);
|
||||
|
||||
const mockFilter = createMockFilter({ defaultOpen: true });
|
||||
|
||||
render(
|
||||
<CheckboxFilter
|
||||
filter={mockFilter}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for checkboxes to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('checkbox')).toHaveLength(4);
|
||||
});
|
||||
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
|
||||
// User unchecks the first value (`mq-kafka`)
|
||||
await userEvent.click(checkboxes[0]);
|
||||
|
||||
// Composite query params (query builder data) should be updated via redirectWithQueryBuilderData
|
||||
expect(redirectWithQueryBuilderData).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [updatedQuery] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
const updatedFilters = updatedQuery.builder.queryData[0].filters;
|
||||
|
||||
expect(updatedFilters.items).toHaveLength(1);
|
||||
expect(updatedFilters.items[0].key.key).toBe(SERVICE_NAME_KEY);
|
||||
// When unchecking from an "all selected" state, we use a NOT_IN filter for that value
|
||||
expect(updatedFilters.items[0].op).toBe('not in');
|
||||
expect(updatedFilters.items[0].value).toBe('mq-kafka');
|
||||
});
|
||||
|
||||
it('should set an IN filter with only the clicked value when using Only', async () => {
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
|
||||
// Existing filter: service.name IN ['mq-kafka', 'otel-demo']
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
lastUsedQuery: 0,
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
key: SERVICE_NAME_KEY,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
op: 'in',
|
||||
value: ['mq-kafka', 'otel-demo'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
redirectWithQueryBuilderData,
|
||||
} as any);
|
||||
|
||||
const mockFilter = createMockFilter({ defaultOpen: true });
|
||||
|
||||
render(
|
||||
<CheckboxFilter
|
||||
filter={mockFilter}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for values to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('mq-kafka')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on the value label to trigger the "Only" behavior
|
||||
await userEvent.click(screen.getByText('mq-kafka'));
|
||||
|
||||
expect(redirectWithQueryBuilderData).toHaveBeenCalledTimes(1);
|
||||
const [updatedQuery] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
const updatedFilters = updatedQuery.builder.queryData[0].filters;
|
||||
|
||||
expect(updatedFilters.items).toHaveLength(1);
|
||||
expect(updatedFilters.items[0].key.key).toBe(SERVICE_NAME_KEY);
|
||||
expect(updatedFilters.items[0].op).toBe('in');
|
||||
expect(updatedFilters.items[0].value).toBe('mq-kafka');
|
||||
});
|
||||
|
||||
it('should clear filters for the attribute when using All', async () => {
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
|
||||
// Existing filter: service.name IN ['mq-kafka']
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
lastUsedQuery: 0,
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
key: SERVICE_NAME_KEY,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
op: 'in',
|
||||
value: ['mq-kafka'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
redirectWithQueryBuilderData,
|
||||
} as any);
|
||||
|
||||
const mockFilter = createMockFilter({ defaultOpen: true });
|
||||
|
||||
render(
|
||||
<CheckboxFilter
|
||||
filter={mockFilter}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('mq-kafka')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Only one value is selected, so clicking it should switch to "All" (no filter for this key)
|
||||
await userEvent.click(screen.getByText('mq-kafka'));
|
||||
|
||||
expect(redirectWithQueryBuilderData).toHaveBeenCalledTimes(1);
|
||||
const [updatedQuery] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
const updatedFilters = updatedQuery.builder.queryData[0].filters;
|
||||
|
||||
const filtersForServiceName = updatedFilters.items.filter(
|
||||
(item: any) => item.key?.key === SERVICE_NAME_KEY,
|
||||
);
|
||||
|
||||
expect(filtersForServiceName).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should match filter when query uses resource. prefix (resource.service.name matches service.name)', async () => {
|
||||
// Filter config uses unprefixed key (service.name)
|
||||
// Query has filter with resource. prefix (resource.service.name)
|
||||
// Checkbox should recognize the match and show checked state
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
lastUsedQuery: 0,
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
key: 'resource.service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
op: 'in',
|
||||
value: [OTEL_DEMO],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
} as any);
|
||||
|
||||
const mockFilter = createMockFilter({ defaultOpen: false });
|
||||
|
||||
render(
|
||||
<CheckboxFilter
|
||||
filter={mockFilter}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Filter should auto-open because it has active filters (key match via prefix stripping)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// otel-demo should be checked (filter uses resource.service.name IN [otel-demo])
|
||||
// Checked items are sorted to the top, so otel-demo is first
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
expect(checkboxes[0]).toBeChecked();
|
||||
expect(screen.getByText(OTEL_DEMO)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should extend an existing IN filter when checking an additional value', async () => {
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
|
||||
// Existing filter: service.name IN 'mq-kafka'
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
lastUsedQuery: 0,
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
key: SERVICE_NAME_KEY,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
op: 'in',
|
||||
value: 'mq-kafka',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
redirectWithQueryBuilderData,
|
||||
} as any);
|
||||
|
||||
const mockFilter = createMockFilter({ defaultOpen: true });
|
||||
|
||||
render(
|
||||
<CheckboxFilter
|
||||
filter={mockFilter}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for checkboxes to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('checkbox')).toHaveLength(4);
|
||||
});
|
||||
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
|
||||
// First checkbox corresponds to 'mq-kafka' (already selected),
|
||||
// second will be 'otel-demo' which we now select additionally.
|
||||
await userEvent.click(checkboxes[1]);
|
||||
|
||||
expect(redirectWithQueryBuilderData).toHaveBeenCalledTimes(1);
|
||||
const [updatedQuery] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
const updatedFilters = updatedQuery.builder.queryData[0].filters;
|
||||
const [filterForServiceName] = updatedFilters.items;
|
||||
|
||||
expect(filterForServiceName.key.key).toBe(SERVICE_NAME_KEY);
|
||||
expect(filterForServiceName.op).toBe('in');
|
||||
expect(filterForServiceName.value).toStrictEqual(['mq-kafka', 'otel-demo']);
|
||||
});
|
||||
});
|
||||
@@ -1,177 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFilterChangeEventData,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import CheckboxFilterHeader from './CheckboxFilterHeader';
|
||||
import CheckboxValueRow from './CheckboxValueRow';
|
||||
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
|
||||
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
|
||||
import useCheckboxDisclosure from './useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from './useCheckboxFilterActions';
|
||||
import useCheckboxFilterState, {
|
||||
FILTER_DISABLED_REASON,
|
||||
} from './useCheckboxFilterState';
|
||||
import useCheckboxFilterValues from './useCheckboxFilterValues';
|
||||
|
||||
import './Checkbox.styles.scss';
|
||||
|
||||
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
|
||||
|
||||
interface ICheckboxProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
const { source, filter, onFilterChange, onQuickFilterChange } = props;
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
isSomeFilterPresentForCurrentAttribute,
|
||||
visibleItemsCount,
|
||||
onToggleOpen,
|
||||
onShowMore,
|
||||
} = useCheckboxDisclosure({ filter, activeQueryIndex });
|
||||
|
||||
const { attributeValues, isLoading } = useCheckboxFilterValues({
|
||||
filter,
|
||||
source,
|
||||
searchText,
|
||||
isOpen,
|
||||
});
|
||||
|
||||
const { currentFilterState, isFilterDisabled, isMultipleValuesTrueForTheKey } =
|
||||
useCheckboxFilterState({ filter, attributeValues, activeQueryIndex });
|
||||
|
||||
const { onChange, onClear } = useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
onQuickFilterChange,
|
||||
});
|
||||
|
||||
const setSearchTextDebounced = useDebouncedFn((...args) => {
|
||||
setSearchText(args[0] as string);
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
// Sort checked items to the top, then unchecked items
|
||||
const currentAttributeKeys = useMemo(() => {
|
||||
const checkedValues = attributeValues.filter(
|
||||
(val) => currentFilterState[val],
|
||||
);
|
||||
const uncheckedValues = attributeValues.filter(
|
||||
(val) => !currentFilterState[val],
|
||||
);
|
||||
return [...checkedValues, ...uncheckedValues].slice(0, visibleItemsCount);
|
||||
}, [attributeValues, currentFilterState, visibleItemsCount]);
|
||||
|
||||
// Count of checked values in the currently visible items
|
||||
const checkedValuesCount = useMemo(
|
||||
() => currentAttributeKeys.filter((val) => currentFilterState[val]).length,
|
||||
[currentAttributeKeys, currentFilterState],
|
||||
);
|
||||
|
||||
const isEmptyStateWithDocsEnabled =
|
||||
SOURCES_WITH_EMPTY_STATE_ENABLED.includes(source) &&
|
||||
!searchText &&
|
||||
!attributeValues.length;
|
||||
|
||||
return (
|
||||
<div className="checkbox-filter">
|
||||
<CheckboxFilterHeader
|
||||
title={filter.title}
|
||||
isOpen={isOpen}
|
||||
showClearAll={!!attributeValues.length}
|
||||
onToggleOpen={onToggleOpen}
|
||||
onClear={onClear}
|
||||
/>
|
||||
{isOpen && isLoading && !attributeValues.length && (
|
||||
<section className="loading">
|
||||
<Skeleton paragraph={{ rows: 4 }} />
|
||||
</section>
|
||||
)}
|
||||
{isOpen && !isLoading && (
|
||||
<>
|
||||
{!isEmptyStateWithDocsEnabled && (
|
||||
<section className="search">
|
||||
<Input
|
||||
placeholder="Filter values"
|
||||
onChange={(e): void => setSearchTextDebounced(e.target.value)}
|
||||
disabled={isFilterDisabled}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
{attributeValues.length > 0 ? (
|
||||
<section className="values">
|
||||
{currentAttributeKeys.map((value: string, index: number) => (
|
||||
<Fragment key={value}>
|
||||
{index === checkedValuesCount && checkedValuesCount > 0 && (
|
||||
<div
|
||||
key="separator"
|
||||
className="filter-separator"
|
||||
data-testid="filter-separator"
|
||||
/>
|
||||
)}
|
||||
<CheckboxValueRow
|
||||
value={value}
|
||||
checked={currentFilterState[value]}
|
||||
disabled={isFilterDisabled}
|
||||
disabledTooltip={FILTER_DISABLED_REASON}
|
||||
title={filter.title}
|
||||
onlyButtonLabel={
|
||||
isSomeFilterPresentForCurrentAttribute
|
||||
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||
? 'All'
|
||||
: 'Only'
|
||||
: 'Only'
|
||||
}
|
||||
customRendererForValue={filter.customRendererForValue}
|
||||
onCheckboxChange={(checked): void => onChange(value, checked, false)}
|
||||
onOnlyOrAllClick={(): void =>
|
||||
onChange(value, currentFilterState[value], true)
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</section>
|
||||
) : isEmptyStateWithDocsEnabled ? (
|
||||
<LogsQuickFilterEmptyState attributeKey={filter.attributeKey.key} />
|
||||
) : (
|
||||
<section className="no-data">
|
||||
<Typography.Text>No values found</Typography.Text>{' '}
|
||||
</section>
|
||||
)}
|
||||
{visibleItemsCount < attributeValues?.length && (
|
||||
<section className="show-more">
|
||||
<Typography.Text className="show-more-text" onClick={onShowMore}>
|
||||
Show More...
|
||||
</Typography.Text>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CheckboxFilter.defaultProps = {
|
||||
onFilterChange: null,
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
|
||||
interface CheckboxFilterHeaderProps {
|
||||
title: string;
|
||||
isOpen: boolean;
|
||||
showClearAll: boolean;
|
||||
onToggleOpen: () => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
function CheckboxFilterHeader({
|
||||
title,
|
||||
isOpen,
|
||||
showClearAll,
|
||||
onToggleOpen,
|
||||
onClear,
|
||||
}: CheckboxFilterHeaderProps): JSX.Element {
|
||||
return (
|
||||
<section className="filter-header-checkbox" onClick={onToggleOpen}>
|
||||
<section className="left-action">
|
||||
{isOpen ? (
|
||||
<ChevronDown size={13} cursor="pointer" />
|
||||
) : (
|
||||
<ChevronRight size={13} cursor="pointer" />
|
||||
)}
|
||||
<Typography.Text className="title">{title}</Typography.Text>
|
||||
</section>
|
||||
<section className="right-action">
|
||||
{isOpen && showClearAll && (
|
||||
<Typography.Text
|
||||
className="clear-all"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onClear();
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
</Typography.Text>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckboxFilterHeader;
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Button } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
|
||||
interface CheckboxValueRowProps {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
disabled: boolean;
|
||||
disabledTooltip?: string;
|
||||
title: string;
|
||||
onlyButtonLabel: string;
|
||||
customRendererForValue?: (value: string) => JSX.Element;
|
||||
onCheckboxChange: (checked: boolean) => void;
|
||||
onOnlyOrAllClick: () => void;
|
||||
}
|
||||
|
||||
function CheckboxValueRow({
|
||||
value,
|
||||
checked,
|
||||
disabled,
|
||||
disabledTooltip,
|
||||
title,
|
||||
onlyButtonLabel,
|
||||
customRendererForValue,
|
||||
onCheckboxChange,
|
||||
onOnlyOrAllClick,
|
||||
}: CheckboxValueRowProps): JSX.Element {
|
||||
return (
|
||||
<div className="value">
|
||||
<Checkbox
|
||||
color="primary"
|
||||
disabledTooltip={disabledTooltip}
|
||||
onChange={(isChecked): void => onCheckboxChange(isChecked === true)}
|
||||
value={checked}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cx('checkbox-value-section', disabled ? 'filter-disabled' : '')}
|
||||
onClick={(): void => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
onOnlyOrAllClick();
|
||||
}}
|
||||
>
|
||||
<div className={`${title} label-${value}`} />
|
||||
{customRendererForValue ? (
|
||||
customRendererForValue(value)
|
||||
) : (
|
||||
<Tooltip title={String(value)} side="top" align="start">
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="value-actions">
|
||||
<Button type="text" className="only-btn">
|
||||
{onlyButtonLabel}
|
||||
</Button>
|
||||
<Button type="text" className="toggle-btn">
|
||||
Toggle
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CheckboxValueRow.defaultProps = {
|
||||
disabledTooltip: undefined,
|
||||
customRendererForValue: undefined,
|
||||
};
|
||||
|
||||
export default CheckboxValueRow;
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import EmptyQuickFilterIcon from 'assets/CustomIcons/EmptyQuickFilterIcon';
|
||||
import { ArrowUpRight } from '@signozhq/icons';
|
||||
|
||||
const QUICK_FILTER_DOC_PATHS: Record<string, string> = {
|
||||
severity_text: 'severity-text',
|
||||
'deployment.environment': 'environment',
|
||||
'service.name': 'service-name',
|
||||
'host.name': 'hostname',
|
||||
'k8s.cluster.name': 'k8s-cluster-name',
|
||||
'k8s.deployment.name': 'k8s-deployment-name',
|
||||
'k8s.namespace.name': 'k8s-namespace-name',
|
||||
'k8s.pod.name': 'k8s-pod-name',
|
||||
};
|
||||
|
||||
function LogsQuickFilterEmptyState({
|
||||
attributeKey,
|
||||
}: {
|
||||
attributeKey: string;
|
||||
}): JSX.Element {
|
||||
const handleLearnMoreClick = (): void => {
|
||||
const section = QUICK_FILTER_DOC_PATHS[attributeKey];
|
||||
|
||||
window.open(
|
||||
`https://signoz.io/docs/logs-management/features/logs-quick-filters#${section}`,
|
||||
'_blank',
|
||||
);
|
||||
};
|
||||
return (
|
||||
<section className="go-to-docs">
|
||||
<div className="go-to-docs__container">
|
||||
<div className="go-to-docs__container-icon">
|
||||
<EmptyQuickFilterIcon />
|
||||
</div>
|
||||
<div className="go-to-docs__container-message">
|
||||
{`You'd need to parse out this attribute to start getting them as a fast
|
||||
filter.`}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
className="go-to-docs__button"
|
||||
onClick={handleLearnMoreClick}
|
||||
>
|
||||
<div className="go-to-docs__button-text">Learn more</div>
|
||||
<ArrowUpRight size={14} color={Color.BG_ROBIN_400} />
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsQuickFilterEmptyState;
|
||||
@@ -97,7 +97,7 @@ interface ToggleAction {
|
||||
isOnlyOrAllClicked?: boolean;
|
||||
previousState?: CheckedState;
|
||||
sectionType?: SectionType;
|
||||
source?: QuickFiltersSource;
|
||||
quickFilterSource?: QuickFiltersSource;
|
||||
attributeValues?: string[];
|
||||
}
|
||||
|
||||
@@ -117,7 +117,8 @@ function runToggle(c: ToggleCase): { items: SimpleItem[]; expression: string } {
|
||||
currentQuery: buildQuery(initialItems, initialExpression),
|
||||
activeQueryIndex: 0,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
source: c.action.source ?? QuickFiltersSource.LOGS_EXPLORER,
|
||||
quickFilterSource:
|
||||
c.action.quickFilterSource ?? QuickFiltersSource.LOGS_EXPLORER,
|
||||
attributeValues: c.action.attributeValues ?? ['a', 'b', 'c'],
|
||||
value: c.action.value,
|
||||
checked: c.action.checked,
|
||||
@@ -162,7 +163,7 @@ const TOGGLE_CASES: ToggleCase[] = [
|
||||
action: {
|
||||
value: 'a',
|
||||
checked: false,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
quickFilterSource: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
// `nin` is what the source asks for, but re-deriving the expression
|
||||
// normalises it. Nothing observes the difference: both infra pages send
|
||||
@@ -313,7 +314,7 @@ const TOGGLE_CASES: ToggleCase[] = [
|
||||
action: {
|
||||
value: 'b',
|
||||
checked: false,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
quickFilterSource: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
|
||||
@@ -46,14 +46,18 @@ export function removeManagedClauses(expression: string, key: string): string {
|
||||
}
|
||||
|
||||
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
|
||||
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
|
||||
const SOURCES_WITH_SHORT_OPERATORS: QuickFiltersSource[] = [
|
||||
QuickFiltersSource.INFRA_MONITORING,
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the correct NOT_IN operator value based on source.
|
||||
* InfraMonitoring backend expects 'nin', others expect 'not in'.
|
||||
*/
|
||||
export function getNotInOperator(source: QuickFiltersSource): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(source)) {
|
||||
export function getNotInOperator(
|
||||
quickFilterSource: QuickFiltersSource,
|
||||
): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(quickFilterSource)) {
|
||||
return 'nin';
|
||||
}
|
||||
return getOperatorValue('NOT_IN');
|
||||
@@ -134,7 +138,7 @@ export function applyCheckboxToggle({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
source,
|
||||
quickFilterSource,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
@@ -145,7 +149,7 @@ export function applyCheckboxToggle({
|
||||
currentQuery: Query;
|
||||
activeQueryIndex: number;
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
value: string;
|
||||
checked: boolean;
|
||||
@@ -240,7 +244,7 @@ export function applyCheckboxToggle({
|
||||
if (sectionType === SectionType.RELATED) {
|
||||
const newFilter: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getNotInOperator(source),
|
||||
op: getNotInOperator(quickFilterSource),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
@@ -380,7 +384,7 @@ export function applyCheckboxToggle({
|
||||
if (!checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getNotInOperator(source),
|
||||
op: getNotInOperator(quickFilterSource),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
@@ -404,7 +408,9 @@ export function applyCheckboxToggle({
|
||||
// checked=true → user wants to select (IN), checked=false → exclude (NOT IN)
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: checked ? getOperatorValue(OPERATORS.IN) : getNotInOperator(source),
|
||||
op: checked
|
||||
? getOperatorValue(OPERATORS.IN)
|
||||
: getNotInOperator(quickFilterSource),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const DEFAULT_VISIBLE_ITEMS_COUNT = 10;
|
||||
@@ -2,10 +2,9 @@ import { useMemo, useState } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import { DEFAULT_VISIBLE_ITEMS_COUNT } from './constants';
|
||||
import { isKeyMatch } from './utils';
|
||||
|
||||
const DEFAULT_VISIBLE_ITEMS_COUNT = 10;
|
||||
|
||||
interface UseCheckboxDisclosureProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
activeQueryIndex: number;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SectionType } from './v2/itemRules';
|
||||
|
||||
interface UseCheckboxFilterActionsProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
activeQueryIndex: number;
|
||||
onFilterChange?: ((query: Query) => void) | null;
|
||||
@@ -38,7 +38,7 @@ interface UseCheckboxFilterActionsReturn {
|
||||
*/
|
||||
function useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
quickFilterSource,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
@@ -65,7 +65,7 @@ function useCheckboxFilterActions({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
source,
|
||||
quickFilterSource,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseCheckboxFilterValuesProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
searchText: string;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
interface UseCheckboxFilterValuesReturn {
|
||||
attributeValues: string[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function useCheckboxFilterValues({
|
||||
filter,
|
||||
source,
|
||||
searchText,
|
||||
isOpen,
|
||||
}: UseCheckboxFilterValuesProps): UseCheckboxFilterValuesReturn {
|
||||
const { data, isLoading } = useGetAggregateValues(
|
||||
{
|
||||
aggregateOperator: filter.aggregateOperator || 'noop',
|
||||
dataSource: filter.dataSource || DataSource.LOGS,
|
||||
aggregateAttribute: filter.aggregateAttribute || '',
|
||||
attributeKey: filter.attributeKey.key,
|
||||
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
||||
tagType: filter.attributeKey.type || '',
|
||||
searchText: searchText ?? '',
|
||||
},
|
||||
{
|
||||
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: keyValueSuggestions, isLoading: isLoadingKeyValueSuggestions } =
|
||||
useGetQueryKeyValueSuggestions({
|
||||
key: filter.attributeKey.key,
|
||||
signal: filter.dataSource || DataSource.LOGS,
|
||||
signalSource: 'meter',
|
||||
options: {
|
||||
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
});
|
||||
|
||||
const attributeValues: string[] = useMemo(() => {
|
||||
const dataType = filter.attributeKey.dataType || DataTypes.String;
|
||||
|
||||
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
|
||||
// Process the response data
|
||||
const responseData = keyValueSuggestions?.data as any;
|
||||
const values = responseData.data?.values || {};
|
||||
const stringValues = values.stringValues || [];
|
||||
const numberValues = values.numberValues || [];
|
||||
|
||||
// Generate options from string values - explicitly handle empty strings
|
||||
const stringOptions = stringValues
|
||||
// Strict filtering for empty string - we'll handle it as a special case if needed
|
||||
.filter(
|
||||
(value: string | null | undefined): value is string =>
|
||||
value !== null && value !== undefined && value !== '',
|
||||
);
|
||||
|
||||
// Generate options from number values
|
||||
const numberOptions = numberValues
|
||||
.filter(
|
||||
(value: number | null | undefined): value is number =>
|
||||
value !== null && value !== undefined,
|
||||
)
|
||||
.map((value: number) => value.toString());
|
||||
|
||||
// Combine all options and make sure we don't have duplicate labels
|
||||
return [...stringOptions, ...numberOptions];
|
||||
}
|
||||
|
||||
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
|
||||
return (data?.payload?.[key] || []).filter(
|
||||
(val) => val !== undefined && val !== null,
|
||||
);
|
||||
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
|
||||
|
||||
return {
|
||||
attributeValues,
|
||||
isLoading: isLoading || isLoadingKeyValueSuggestions,
|
||||
};
|
||||
}
|
||||
|
||||
export default useCheckboxFilterValues;
|
||||
@@ -6,44 +6,6 @@
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.sectionActions {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transform: translateX(4px);
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 0.16s ease,
|
||||
transform 0.16s ease,
|
||||
width 0s linear 0.16s;
|
||||
}
|
||||
|
||||
.checkboxFilter:hover .sectionActions,
|
||||
.sectionActions.sectionActionsPinned {
|
||||
width: auto;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: auto;
|
||||
transition:
|
||||
opacity 0.16s ease,
|
||||
transform 0.16s ease;
|
||||
}
|
||||
|
||||
.sectionActionsPinned .sectionActionHoverOnly {
|
||||
opacity: 0;
|
||||
transform: translateX(4px);
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 0.16s ease,
|
||||
transform 0.16s ease;
|
||||
}
|
||||
|
||||
.checkboxFilter:hover .sectionActionsPinned .sectionActionHoverOnly {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.search {
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 {
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
QuickFiltersSource,
|
||||
} from '../../../types';
|
||||
import CheckboxFilterV2 from './CheckboxFilterV2';
|
||||
import { DEFAULT_VISIBLE_ITEMS_COUNT } from '../constants';
|
||||
|
||||
export const DEFAULT_FILTER: IQuickFiltersConfig = {
|
||||
type: FiltersType.CHECKBOX,
|
||||
@@ -20,7 +20,6 @@ export const DEFAULT_FILTER: IQuickFiltersConfig = {
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
};
|
||||
|
||||
@@ -30,6 +29,13 @@ export const DEFAULT_USE_FIELD_APIS: QuickFilterCheckboxUseFieldApis = {
|
||||
existingQuery: null,
|
||||
};
|
||||
|
||||
// Enough extra values to push a section past the count that shows its search box.
|
||||
// Named to sort last, so a test's own values stay within the first page.
|
||||
export const EXTRA_VALUES = Array.from(
|
||||
{ length: DEFAULT_VISIBLE_ITEMS_COUNT },
|
||||
(_, index) => `zz-extra-value-${index + 1}`,
|
||||
);
|
||||
|
||||
export function mockFieldsValuesAPI(response: {
|
||||
relatedValues?: (string | null)[];
|
||||
stringValues?: (string | null)[];
|
||||
@@ -150,7 +156,7 @@ export function renderWithFilter(
|
||||
return render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Skeleton } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { LoaderCircle } from '@signozhq/icons';
|
||||
import {
|
||||
@@ -17,6 +16,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { NON_SELECTED_OPERATORS } from '../checkboxFilterQuery';
|
||||
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
|
||||
import { DEFAULT_VISIBLE_ITEMS_COUNT } from '../constants';
|
||||
import useCheckboxDisclosure from '../useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from '../useCheckboxFilterActions';
|
||||
import useCheckboxFilterState from '../useCheckboxFilterState';
|
||||
@@ -33,7 +33,7 @@ import styles from './CheckboxFilterV2.module.scss';
|
||||
|
||||
interface CheckboxFilterV2Props {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
|
||||
useFieldApis: QuickFilterCheckboxUseFieldApis;
|
||||
@@ -42,19 +42,18 @@ interface CheckboxFilterV2Props {
|
||||
export default function CheckboxFilterV2(
|
||||
props: CheckboxFilterV2Props,
|
||||
): JSX.Element {
|
||||
const { source, filter, onFilterChange, onQuickFilterChange, useFieldApis } =
|
||||
props;
|
||||
const {
|
||||
quickFilterSource,
|
||||
filter,
|
||||
onFilterChange,
|
||||
onQuickFilterChange,
|
||||
useFieldApis,
|
||||
} = props;
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [isSearchOpen, setIsSearchOpen] = useState<boolean>(false);
|
||||
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
|
||||
|
||||
const handleToggleSearch = (): void => {
|
||||
setIsSearchOpen((prev) => !prev);
|
||||
setSearchText('');
|
||||
};
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@@ -81,7 +80,7 @@ export default function CheckboxFilterV2(
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace: useFieldApis.metricNamespace,
|
||||
source,
|
||||
quickFilterSource,
|
||||
startUnixMilli: useFieldApis.startUnixMilli,
|
||||
endUnixMilli: useFieldApis.endUnixMilli,
|
||||
enabled: isOpen,
|
||||
@@ -110,7 +109,7 @@ export default function CheckboxFilterV2(
|
||||
|
||||
const { onChange, onClear } = useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
quickFilterSource,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
@@ -121,6 +120,14 @@ export default function CheckboxFilterV2(
|
||||
setSearchText(args[0] as string);
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
const handleToggleOpen = (): void => {
|
||||
if (isOpen) {
|
||||
setSearchTextDebounced.cancel();
|
||||
setSearchText('');
|
||||
}
|
||||
onToggleOpen();
|
||||
};
|
||||
|
||||
const currentFilterOp = useMemo(() => {
|
||||
const filterSync = currentQuery?.builder.queryData?.[
|
||||
activeQueryIndex
|
||||
@@ -166,19 +173,22 @@ export default function CheckboxFilterV2(
|
||||
relatedExclusions,
|
||||
});
|
||||
|
||||
// Kept while a search is typed, so narrowing the list does not remove the box,
|
||||
// and while a refetch still shows the previous, narrowed results.
|
||||
const wasSearchShown = useRef(false);
|
||||
const isSearchShown =
|
||||
searchText !== '' ||
|
||||
totalCount > DEFAULT_VISIBLE_ITEMS_COUNT ||
|
||||
(isFetching && wasSearchShown.current);
|
||||
wasSearchShown.current = isSearchShown;
|
||||
|
||||
return (
|
||||
<div className={styles.checkboxFilter} data-testid="checkbox-filter-v2">
|
||||
<CheckboxFilterV2Header
|
||||
title={filter.title}
|
||||
isOpen={isOpen}
|
||||
actionsClassName={classNames(styles.sectionActions, {
|
||||
[styles.sectionActionsPinned]: isSearchOpen,
|
||||
})}
|
||||
resetActionClassName={
|
||||
isSearchOpen ? styles.sectionActionHoverOnly : undefined
|
||||
}
|
||||
onToggleOpen={onToggleOpen}
|
||||
onToggleSearch={handleToggleSearch}
|
||||
canReset={isSomeFilterPresentForCurrentAttribute}
|
||||
onToggleOpen={handleToggleOpen}
|
||||
onClear={onClear}
|
||||
/>
|
||||
{isOpen && isLoading && !hasLoadedOnce.current && (
|
||||
@@ -188,10 +198,9 @@ export default function CheckboxFilterV2(
|
||||
)}
|
||||
{isOpen && (!isLoading || hasLoadedOnce.current) && (
|
||||
<>
|
||||
{isSearchOpen && (
|
||||
{isSearchShown && (
|
||||
<section className={styles.search}>
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="Filter values"
|
||||
onChange={(e): void => setSearchTextDebounced(e.target.value)}
|
||||
disabled={isFilterDisabled}
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import { useState } from 'react';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ChevronDown, ChevronRight, Search, Undo2 } from '@signozhq/icons';
|
||||
import { ChevronDown, ChevronRight, Undo2 } from '@signozhq/icons';
|
||||
|
||||
import { SectionActionButton } from '../../shared/SectionActionButton/SectionActionButton';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './CheckboxFilterV2Header.module.scss';
|
||||
|
||||
interface CheckboxFilterHeaderProps {
|
||||
title: string;
|
||||
isOpen: boolean;
|
||||
actionsClassName?: string;
|
||||
resetActionClassName?: string;
|
||||
canReset: boolean;
|
||||
onToggleOpen: () => void;
|
||||
onToggleSearch: () => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export function CheckboxFilterV2Header({
|
||||
title,
|
||||
isOpen,
|
||||
actionsClassName,
|
||||
resetActionClassName,
|
||||
canReset,
|
||||
onToggleOpen,
|
||||
onToggleSearch,
|
||||
onClear,
|
||||
}: CheckboxFilterHeaderProps): JSX.Element {
|
||||
const [isTitleTruncated, setIsTitleTruncated] = useState(false);
|
||||
@@ -64,21 +58,14 @@ export function CheckboxFilterV2Header({
|
||||
)}
|
||||
<Tooltip title={isTitleTruncated ? title : undefined}>{titleText}</Tooltip>
|
||||
</section>
|
||||
{isOpen && (
|
||||
<section className={classNames(styles.rightAction, actionsClassName)}>
|
||||
{isOpen && canReset && (
|
||||
<section className={styles.rightAction}>
|
||||
<SectionActionButton
|
||||
icon={<Undo2 size={14} />}
|
||||
className={resetActionClassName}
|
||||
tooltip="Reset"
|
||||
onClick={onClear}
|
||||
testId="checkbox-filter-clear-all"
|
||||
/>
|
||||
<SectionActionButton
|
||||
icon={<Search size={14} />}
|
||||
tooltip="Search"
|
||||
onClick={onToggleSearch}
|
||||
testId="checkbox-filter-search-toggle"
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
quickFilterSource={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -44,7 +44,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
quickFilterSource={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -59,6 +59,8 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
expect(params.get('endUnixMilli')).toBe(
|
||||
String(DEFAULT_USE_FIELD_APIS.endUnixMilli),
|
||||
);
|
||||
expect(params.get('signal')).toBeNull();
|
||||
expect(params.get('source')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps non-AI sources on the signal-wide endpoint', async () => {
|
||||
@@ -70,7 +72,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'custom.query = "value"',
|
||||
@@ -82,7 +82,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: null,
|
||||
@@ -116,7 +116,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -154,7 +154,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -185,7 +185,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -222,7 +222,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -264,7 +264,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import {
|
||||
TelemetrytypesSignalDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { rest, server } 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';
|
||||
|
||||
setupServer();
|
||||
|
||||
describe('CheckboxFilterV2 - field APIs', () => {
|
||||
it('sends the signal and source of the quick filter source to the signal-wide endpoint', async () => {
|
||||
const requests: URLSearchParams[] = [];
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
|
||||
requests.push(req.url.searchParams);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { values: { stringValues: ['ingest'] } },
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.METER_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText('ingest')).resolves.toBeInTheDocument();
|
||||
expect(requests[0].get('signal')).toBe(TelemetrytypesSignalDTO.metrics);
|
||||
expect(requests[0].get('source')).toBe(TelemetrytypesSourceDTO.meter);
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
buildQueryBuilderOverrides,
|
||||
DEFAULT_FILTER,
|
||||
DEFAULT_USE_FIELD_APIS,
|
||||
EXTRA_VALUES,
|
||||
getFilterFromCall,
|
||||
mockFieldsValuesAPI,
|
||||
renderWithFilter,
|
||||
@@ -18,6 +19,25 @@ import {
|
||||
|
||||
setupServer();
|
||||
|
||||
const activeEnvironmentFilter = buildQueryBuilderOverrides({
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
key: { key: 'deployment.environment' },
|
||||
op: 'in',
|
||||
value: ['production'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
describe('CheckboxFilterV2 - interactions', () => {
|
||||
describe('search functionality', () => {
|
||||
it('filters values based on search text', async () => {
|
||||
@@ -30,7 +50,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
|
||||
const values =
|
||||
searchTextReceived === ''
|
||||
? ['production', 'staging', 'development']
|
||||
? ['production', 'staging', 'development', ...EXTRA_VALUES]
|
||||
: ['production'];
|
||||
|
||||
return res(
|
||||
@@ -52,7 +72,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -60,7 +80,6 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
await screen.findByTestId('checkbox-value-row-production');
|
||||
expect(screen.getByTestId('checkbox-value-row-staging')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByTestId('checkbox-filter-search-toggle'));
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'prod');
|
||||
|
||||
@@ -73,6 +92,41 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
screen.queryByTestId('checkbox-value-row-staging'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('checkbox-filter-search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no search box when every value fits', async () => {
|
||||
mockFieldsValuesAPI({ stringValues: EXTRA_VALUES });
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByTestId(`checkbox-value-row-${EXTRA_VALUES[0]}`);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('checkbox-filter-search'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the search box once there are more values than fit', async () => {
|
||||
mockFieldsValuesAPI({ stringValues: ['production', ...EXTRA_VALUES] });
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('checkbox-filter-search'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters values via search while preserving existingQuery context', async () => {
|
||||
@@ -92,7 +146,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
data: {
|
||||
values: {
|
||||
relatedValues: ['production'],
|
||||
stringValues: ['staging', 'development'],
|
||||
stringValues: ['staging', 'development', ...EXTRA_VALUES],
|
||||
numberValues: [],
|
||||
},
|
||||
},
|
||||
@@ -119,7 +173,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -144,7 +198,6 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
// Related values now appear in "Related" section (no badge, uses divider instead)
|
||||
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByTestId('checkbox-filter-search-toggle'));
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'prod');
|
||||
|
||||
@@ -172,7 +225,10 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
data: {
|
||||
values: {
|
||||
relatedValues: [],
|
||||
stringValues: searchText === '' ? ['prod', 'staging'] : ['prod-match'],
|
||||
stringValues:
|
||||
searchText === ''
|
||||
? ['prod', 'staging', ...EXTRA_VALUES]
|
||||
: ['prod-match'],
|
||||
numberValues: [],
|
||||
},
|
||||
},
|
||||
@@ -184,7 +240,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -194,7 +250,6 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-prod');
|
||||
|
||||
await user.click(screen.getByTestId('checkbox-filter-search-toggle'));
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'prod');
|
||||
|
||||
@@ -220,7 +275,8 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
data: {
|
||||
values: {
|
||||
relatedValues: [],
|
||||
stringValues: searchText === '' ? ['prod', 'staging'] : [],
|
||||
stringValues:
|
||||
searchText === '' ? ['prod', 'staging', ...EXTRA_VALUES] : [],
|
||||
numberValues: [],
|
||||
},
|
||||
},
|
||||
@@ -232,14 +288,13 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-prod');
|
||||
|
||||
await user.click(screen.getByTestId('checkbox-filter-search-toggle'));
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'xyz-no-match');
|
||||
|
||||
@@ -275,7 +330,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -316,7 +371,9 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
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'
|
||||
searchText === ''
|
||||
? ['pod-a-v2', 'pod-b-v2', 'pod-c-v2', ...EXTRA_VALUES]
|
||||
: ['pod-a-v2']; // filtered to match 'pod-a'
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
@@ -337,7 +394,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -347,7 +404,6 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-pod-a-v1');
|
||||
|
||||
await user.click(screen.getByTestId('checkbox-filter-search-toggle'));
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'pod-a');
|
||||
|
||||
@@ -384,7 +440,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -412,7 +468,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -425,6 +481,113 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
expect(header).toHaveAttribute('data-state', 'open');
|
||||
await screen.findByTestId('checkbox-value-row-production');
|
||||
});
|
||||
|
||||
it('opens a closed filter when the query filters on its key', async () => {
|
||||
mockFieldsValuesAPI({
|
||||
stringValues: ['production', 'staging'],
|
||||
});
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
{ queryBuilderOverrides: activeEnvironmentFilter },
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('checkbox-filter-header')).toHaveAttribute(
|
||||
'data-state',
|
||||
'open',
|
||||
);
|
||||
await expect(
|
||||
screen.findByTestId('checkbox-value-row-production'),
|
||||
).resolves.toHaveAttribute('data-state', 'checked');
|
||||
});
|
||||
|
||||
it('keeps the user toggle over the auto-open', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
mockFieldsValuesAPI({
|
||||
stringValues: ['production'],
|
||||
});
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
{ queryBuilderOverrides: activeEnvironmentFilter },
|
||||
);
|
||||
|
||||
const header = screen.getByTestId('checkbox-filter-header');
|
||||
await screen.findByTestId('checkbox-value-row-production');
|
||||
|
||||
await user.click(header);
|
||||
|
||||
expect(header).toHaveAttribute('data-state', 'closed');
|
||||
expect(
|
||||
screen.queryByTestId('checkbox-value-row-production'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await user.click(header);
|
||||
|
||||
expect(header).toHaveAttribute('data-state', 'open');
|
||||
await screen.findByTestId('checkbox-value-row-production');
|
||||
});
|
||||
|
||||
it('clears the search when the filter is collapsed', async () => {
|
||||
const user = userEvent.setup();
|
||||
const values = Array.from(
|
||||
{ length: 12 },
|
||||
(_, i) => `value-${String(i).padStart(2, '0')}`,
|
||||
);
|
||||
|
||||
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: values.filter((value) => value.includes(searchText)),
|
||||
numberValues: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-value-00');
|
||||
await user.type(screen.getByTestId('checkbox-filter-search'), '11');
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByTestId('checkbox-value-row-value-00'),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
|
||||
const header = screen.getByTestId('checkbox-filter-header');
|
||||
await user.click(header);
|
||||
await user.click(header);
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-value-00');
|
||||
expect(screen.getByTestId('checkbox-filter-search')).toHaveValue('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('show more functionality', () => {
|
||||
@@ -438,7 +601,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -464,7 +627,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -489,7 +652,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -520,7 +683,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
expect(screen.getByTestId('checkbox-filter-clear-all')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the reset action when expanded even with no active filter', async () => {
|
||||
it('hides reset when the section has no filter applied', async () => {
|
||||
mockFieldsValuesAPI({
|
||||
stringValues: ['production'],
|
||||
});
|
||||
@@ -528,52 +691,16 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-production');
|
||||
|
||||
// Reset is always available on an expanded section now (hover-gated via
|
||||
// CSS), not conditional on an active filter.
|
||||
expect(screen.getByTestId('checkbox-filter-clear-all')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not dispatch on clear when the key has no filter', 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: buildQueryBuilderOverrides({
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: { items: [], op: 'AND' },
|
||||
filter: { expression: '' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-production');
|
||||
await user.click(screen.getByTestId('checkbox-filter-clear-all'));
|
||||
|
||||
expect(onFilterChange).not.toHaveBeenCalled();
|
||||
expect(
|
||||
screen.queryByTestId('checkbox-filter-clear-all'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onFilterChange when clear clicked', async () => {
|
||||
@@ -587,7 +714,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
onFilterChange={onFilterChange}
|
||||
/>,
|
||||
@@ -634,7 +761,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
onFilterChange={onFilterChange}
|
||||
/>,
|
||||
@@ -710,7 +837,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
onFilterChange={onFilterChange}
|
||||
/>,
|
||||
@@ -844,7 +971,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, customRendererForValue: customRenderer }}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -55,7 +55,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -106,7 +106,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -140,7 +140,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -185,7 +185,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -221,7 +221,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -273,7 +273,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -321,7 +321,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -373,7 +373,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -424,7 +424,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -466,7 +466,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -521,7 +521,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
|
||||
@@ -9,6 +9,7 @@ import CheckboxFilterV2 from '../CheckboxFilterV2';
|
||||
import {
|
||||
DEFAULT_FILTER,
|
||||
DEFAULT_USE_FIELD_APIS,
|
||||
EXTRA_VALUES,
|
||||
mockFieldsValuesAPI,
|
||||
mockFieldsValuesAPILoading,
|
||||
setupServer,
|
||||
@@ -24,7 +25,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -46,7 +47,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={closedFilter}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -89,7 +90,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
data: {
|
||||
values: {
|
||||
relatedValues: [],
|
||||
stringValues: ['production', 'staging'],
|
||||
stringValues: ['production', 'staging', ...EXTRA_VALUES],
|
||||
numberValues: [],
|
||||
},
|
||||
},
|
||||
@@ -103,14 +104,13 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-production');
|
||||
|
||||
await user.click(screen.getByTestId('checkbox-filter-search-toggle'));
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'prod');
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -152,7 +152,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -172,7 +172,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -195,7 +195,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -7,8 +7,8 @@ describe('CheckboxFilterV2Header', () => {
|
||||
const defaultProps = {
|
||||
title: 'Environment',
|
||||
isOpen: false,
|
||||
canReset: true,
|
||||
onToggleOpen: jest.fn(),
|
||||
onToggleSearch: jest.fn(),
|
||||
onClear: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -30,12 +30,9 @@ describe('CheckboxFilterV2Header', () => {
|
||||
expect(header).toHaveAttribute('data-state', 'closed');
|
||||
});
|
||||
|
||||
it('does not render the section actions when collapsed', () => {
|
||||
it('does not render reset when collapsed', () => {
|
||||
render(<CheckboxFilterV2Header {...defaultProps} isOpen={false} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('checkbox-filter-search-toggle'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('checkbox-filter-clear-all'),
|
||||
).not.toBeInTheDocument();
|
||||
@@ -50,14 +47,19 @@ describe('CheckboxFilterV2Header', () => {
|
||||
expect(header).toHaveAttribute('data-state', 'open');
|
||||
});
|
||||
|
||||
it('renders both search and reset actions when expanded', () => {
|
||||
it('renders reset when the section has a filter applied', () => {
|
||||
render(<CheckboxFilterV2Header {...defaultProps} isOpen />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('checkbox-filter-search-toggle'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('checkbox-filter-clear-all')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides reset when there is nothing to reset', () => {
|
||||
render(<CheckboxFilterV2Header {...defaultProps} isOpen canReset={false} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('checkbox-filter-clear-all'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactions', () => {
|
||||
@@ -99,25 +101,6 @@ describe('CheckboxFilterV2Header', () => {
|
||||
expect(onToggleOpen).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onToggleSearch on search click without toggling open', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onToggleSearch = jest.fn();
|
||||
const onToggleOpen = jest.fn();
|
||||
render(
|
||||
<CheckboxFilterV2Header
|
||||
{...defaultProps}
|
||||
isOpen
|
||||
onToggleSearch={onToggleSearch}
|
||||
onToggleOpen={onToggleOpen}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('checkbox-filter-search-toggle'));
|
||||
|
||||
expect(onToggleSearch).toHaveBeenCalledTimes(1);
|
||||
expect(onToggleOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClear on reset click without toggling open', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClear = jest.fn();
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useMemo } from 'react';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldValuesConfig } from 'api/querySuggestions/types';
|
||||
import { QUICK_FILTER_SOURCE_TO_SIGNAL } from 'components/QuickFilters/constants';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { useFieldValuesSuggestion } from 'hooks/querySuggestions/useFieldValuesSuggestion';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseFieldValuesProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
searchText: string;
|
||||
existingQuery?: string;
|
||||
metricNamespace?: string;
|
||||
source?: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
startUnixMilli?: number;
|
||||
endUnixMilli?: number;
|
||||
enabled: boolean;
|
||||
@@ -38,12 +38,13 @@ export function useFieldValues({
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
source,
|
||||
quickFilterSource,
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
enabled,
|
||||
}: UseFieldValuesProps): UseFieldValuesReturn {
|
||||
const isAIObservability = source === QuickFiltersSource.AI_OBSERVABILITY;
|
||||
const isAIObservability =
|
||||
quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY;
|
||||
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
@@ -59,14 +60,12 @@ export function useFieldValues({
|
||||
endUnixMilli,
|
||||
}
|
||||
: {
|
||||
signal: filter.dataSource
|
||||
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
|
||||
: undefined,
|
||||
signal: QUICK_FILTER_SOURCE_TO_SIGNAL[quickFilterSource],
|
||||
name: filter.attributeKey.key,
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
|
||||
source: QUICK_FILTERS_SOURCE_TO_SOURCE[quickFilterSource],
|
||||
startUnixMilli,
|
||||
// This field does not affect the backend but I wanted to keep it here
|
||||
// in case we add the support in the future
|
||||
|
||||
@@ -31,11 +31,11 @@ export type { FilterType };
|
||||
function Duration({
|
||||
filter,
|
||||
onFilterChange,
|
||||
source,
|
||||
quickFilterSource,
|
||||
}: {
|
||||
filter: IQuickFiltersConfig;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
}): JSX.Element {
|
||||
const [selectedFilters, setSelectedFilters] =
|
||||
useState<
|
||||
@@ -52,7 +52,7 @@ function Duration({
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const syncSelectedFilters = useMemo((): FilterType => {
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
import { isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
||||
import useActiveQueryIndex from './hooks/useActiveQueryIndex';
|
||||
import CheckboxV2 from './FilterRenderers/Checkbox/v2/CheckboxFilterV2';
|
||||
import Duration from './FilterRenderers/Duration/Duration';
|
||||
@@ -50,15 +49,18 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
className,
|
||||
config,
|
||||
handleFilterVisibilityChange,
|
||||
source,
|
||||
quickFilterSource,
|
||||
onFilterChange,
|
||||
onQuickFilterChange,
|
||||
signal,
|
||||
showFilterCollapse = true,
|
||||
showQueryName = true,
|
||||
useFieldApis,
|
||||
savedViewsHeader,
|
||||
} = props;
|
||||
const savedFiltersSource =
|
||||
quickFilterSource === QuickFiltersSource.INFRA_MONITORING
|
||||
? undefined
|
||||
: quickFilterSource;
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [params, setParams] = useApiMonitoringParams();
|
||||
const showIP = params.showIP ?? true;
|
||||
@@ -69,7 +71,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
customFilters,
|
||||
refetchCustomFilters,
|
||||
isCustomFiltersLoading,
|
||||
} = useFilterConfig({ signal, config });
|
||||
} = useFilterConfig({ savedFiltersSource, config });
|
||||
const {
|
||||
deniedPermissions: deniedSettingsPermissions,
|
||||
isLoading: isSettingsChecking,
|
||||
@@ -113,15 +115,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
// Show dropdown in ListView only for TRACES_EXPLORER source
|
||||
const shouldShowDropdownInListView =
|
||||
isListView && source === QuickFiltersSource.TRACES_EXPLORER;
|
||||
isListView && quickFilterSource === QuickFiltersSource.TRACES_EXPLORER;
|
||||
|
||||
// AI observability builds a single query in the row-level views, so there is
|
||||
// no query for the selector to switch between.
|
||||
const isAIObservabilityRowView =
|
||||
source === QuickFiltersSource.AI_OBSERVABILITY &&
|
||||
quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY &&
|
||||
(isListView || panelType === PANEL_TYPES.TRACE);
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
|
||||
|
||||
// clear all the filters for the query which is in sync with filters
|
||||
const handleReset = (): void => {
|
||||
@@ -291,7 +293,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
const renderContent = (): JSX.Element => (
|
||||
<>
|
||||
{source === QuickFiltersSource.API_MONITORING && (
|
||||
{quickFilterSource === QuickFiltersSource.API_MONITORING && (
|
||||
<div className="api-quick-filters-header">
|
||||
<Typography.Text>Show IP addresses</Typography.Text>
|
||||
<Switch
|
||||
@@ -310,55 +312,27 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<section className="filters">
|
||||
{filterConfig.map((filter) => {
|
||||
switch (filter.type) {
|
||||
case FiltersType.CHECKBOX:
|
||||
return useFieldApis ? (
|
||||
<CheckboxV2
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
onQuickFilterChange={onQuickFilterChange}
|
||||
useFieldApis={useFieldApis}
|
||||
/>
|
||||
) : (
|
||||
<Checkbox
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
onQuickFilterChange={onQuickFilterChange}
|
||||
/>
|
||||
);
|
||||
case FiltersType.DURATION:
|
||||
return (
|
||||
<Duration
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
quickFilterSource={quickFilterSource}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
case FiltersType.SLIDER:
|
||||
return <Slider key={filter.attributeKey.key} />;
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
default:
|
||||
return useFieldApis ? (
|
||||
return (
|
||||
<CheckboxV2
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
quickFilterSource={quickFilterSource}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
onQuickFilterChange={onQuickFilterChange}
|
||||
useFieldApis={useFieldApis}
|
||||
/>
|
||||
) : (
|
||||
<Checkbox
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
onQuickFilterChange={onQuickFilterChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
@@ -392,14 +366,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<div className="quick-filters-container">
|
||||
<div className="quick-filters">
|
||||
{savedViewsHeader}
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
{quickFilterSource !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
<section className="header">
|
||||
{renderLeftActions()}
|
||||
{renderRightActions()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{signal ? (
|
||||
{savedFiltersSource ? (
|
||||
<AuthZGuardContent
|
||||
checks={[QuickFilterReadPermission]}
|
||||
fallbackOnLoading={filtersSkeleton}
|
||||
@@ -420,9 +394,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isSettingsOpen && (
|
||||
{isSettingsOpen && savedFiltersSource && (
|
||||
<QuickFiltersSettings
|
||||
signal={signal}
|
||||
savedFiltersSource={savedFiltersSource}
|
||||
setIsSettingsOpen={setIsSettingsOpen}
|
||||
customFilters={customFilters}
|
||||
refetchCustomFilters={refetchCustomFilters}
|
||||
@@ -436,9 +410,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
QuickFilters.defaultProps = {
|
||||
onFilterChange: null,
|
||||
signal: '',
|
||||
config: [],
|
||||
showFilterCollapse: true,
|
||||
showQueryName: true,
|
||||
useFieldApis: undefined,
|
||||
};
|
||||
|
||||
@@ -5,10 +5,10 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import SavedViewsHeader from 'container/SavedViews/SavedViewsHeader';
|
||||
import SavedViewsPanel from 'container/SavedViews/SavedViewsPanel';
|
||||
import SavedViewsRestore from 'container/SavedViews/SavedViewsRestore';
|
||||
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { SIDEBAR_TOOLTIP_DELAY_MS } from './constants';
|
||||
import { useCloseOnOutsidePress } from './useCloseOnOutsidePress';
|
||||
|
||||
import styles from './QuickFiltersLayout.module.scss';
|
||||
|
||||
@@ -49,10 +49,9 @@ function QuickFiltersLayout({
|
||||
children,
|
||||
}: QuickFiltersLayoutProps): JSX.Element {
|
||||
const [isViewsListOpen, setIsViewsListOpen] = useState(false);
|
||||
const isSavedViewEnabled = useSavedViewEnabled();
|
||||
|
||||
const hasQuickFilters = !!quickFilterProps;
|
||||
const hasSavedViews = !!savedViewProps && isSavedViewEnabled;
|
||||
const hasSavedViews = !!savedViewProps;
|
||||
|
||||
const showSidebar = showFilters && (hasQuickFilters || hasSavedViews);
|
||||
|
||||
@@ -61,12 +60,20 @@ function QuickFiltersLayout({
|
||||
|
||||
const isSliding = hasQuickFilters && isViewsListOpen;
|
||||
|
||||
const openViews = (): void => setIsViewsListOpen(true);
|
||||
const toggleViews = (): void => setIsViewsListOpen((isOpen) => !isOpen);
|
||||
const closeViews = (): void => setIsViewsListOpen(false);
|
||||
const markSidebarPress = useCloseOnOutsidePress(
|
||||
showSidebar && isSliding,
|
||||
closeViews,
|
||||
);
|
||||
|
||||
const savedViewsHeader = hasSavedViews ? (
|
||||
<SavedViewsHeader
|
||||
{...savedViewProps}
|
||||
onOpenViews={
|
||||
isViewsListVisible ? undefined : (): void => setIsViewsListOpen(true)
|
||||
}
|
||||
onOpenViews={isViewsListVisible ? undefined : openViews}
|
||||
isViewsOpen={isViewsListOpen}
|
||||
onToggleViews={isViewsListPinned ? undefined : toggleViews}
|
||||
onCollapse={hasQuickFilters ? undefined : onToggleFilters}
|
||||
/>
|
||||
) : undefined;
|
||||
@@ -77,6 +84,7 @@ function QuickFiltersLayout({
|
||||
{showSidebar && (
|
||||
<aside
|
||||
className={cx(styles.sidebar, { [styles.isStatic]: !hasQuickFilters })}
|
||||
onPointerDownCapture={markSidebarPress}
|
||||
data-testid="quick-filters-layout-filters"
|
||||
>
|
||||
<TooltipProvider delay={SIDEBAR_TOOLTIP_DELAY_MS}>
|
||||
@@ -98,9 +106,7 @@ function QuickFiltersLayout({
|
||||
{hasSavedViews && isViewsListVisible && (
|
||||
<SavedViewsPanel
|
||||
{...savedViewProps}
|
||||
onClose={
|
||||
isViewsListPinned ? undefined : (): void => setIsViewsListOpen(false)
|
||||
}
|
||||
onClose={isViewsListPinned ? undefined : closeViews}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import { QuickFiltersSource } from '../../types';
|
||||
@@ -20,10 +19,6 @@ jest.mock('../QuickFiltersLayout.module.scss', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSavedViewEnabled', () => ({
|
||||
useSavedViewEnabled: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
jest.mock('container/SavedViews/SavedViewsRestore', () => ({
|
||||
__esModule: true,
|
||||
default: ({ source }: { source: string }): JSX.Element => (
|
||||
@@ -34,17 +29,17 @@ jest.mock('container/SavedViews/SavedViewsRestore', () => ({
|
||||
jest.mock('../../QuickFilters', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
source,
|
||||
quickFilterSource,
|
||||
savedViewsHeader,
|
||||
handleFilterVisibilityChange,
|
||||
}: {
|
||||
source: string;
|
||||
quickFilterSource: string;
|
||||
savedViewsHeader?: React.ReactNode;
|
||||
handleFilterVisibilityChange?: () => void;
|
||||
}): JSX.Element => (
|
||||
<div data-testid="quick-filters">
|
||||
{savedViewsHeader}
|
||||
{source}
|
||||
{quickFilterSource}
|
||||
{handleFilterVisibilityChange && (
|
||||
<Button
|
||||
size="md"
|
||||
@@ -62,7 +57,8 @@ jest.mock('../../QuickFilters', () => ({
|
||||
}));
|
||||
|
||||
const quickFilterProps = {
|
||||
source: QuickFiltersSource.TRACES_EXPLORER,
|
||||
quickFilterSource: QuickFiltersSource.TRACES_EXPLORER,
|
||||
useFieldApis: { startUnixMilli: 0, endUnixMilli: 0 },
|
||||
};
|
||||
const savedViewProps = { source: SavedviewtypesSourceDTO.traces };
|
||||
|
||||
@@ -70,10 +66,6 @@ const drawer = (): HTMLElement =>
|
||||
screen.getByTestId('quick-filters-layout-drawer');
|
||||
|
||||
describe('QuickFiltersLayout', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(useSavedViewEnabled).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('renders QuickFilters with the given props inside the filters pane', () => {
|
||||
render(
|
||||
<QuickFiltersLayout showFilters quickFilterProps={quickFilterProps}>
|
||||
@@ -148,35 +140,6 @@ describe('QuickFiltersLayout', () => {
|
||||
});
|
||||
|
||||
describe('saved views', () => {
|
||||
it('ignores savedViewProps while the flag is off', () => {
|
||||
jest.mocked(useSavedViewEnabled).mockReturnValue(false);
|
||||
const { unmount } = render(
|
||||
<QuickFiltersLayout
|
||||
showFilters
|
||||
quickFilterProps={quickFilterProps}
|
||||
savedViewProps={savedViewProps}
|
||||
>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('saved-views-header')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('saved-views-panel')).not.toBeInTheDocument();
|
||||
|
||||
// Saved views only, so nothing is left to show.
|
||||
unmount();
|
||||
render(
|
||||
<QuickFiltersLayout showFilters savedViewProps={savedViewProps}>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('quick-filters-layout-filters'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('saved-views-restore')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the panel from the header and slides the quick filters drawer, keeping the same QuickFilters node', async () => {
|
||||
render(
|
||||
<QuickFiltersLayout
|
||||
@@ -217,6 +180,62 @@ describe('QuickFiltersLayout', () => {
|
||||
expect(screen.getByTestId('saved-views-open')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens and closes the panel from the view name', async () => {
|
||||
render(
|
||||
<QuickFiltersLayout
|
||||
showFilters
|
||||
quickFilterProps={quickFilterProps}
|
||||
savedViewProps={savedViewProps}
|
||||
>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const toggle = await screen.findByTestId('saved-views-name-toggle');
|
||||
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||||
await user.click(toggle);
|
||||
|
||||
expect(screen.getByTestId('saved-views-panel')).toBeInTheDocument();
|
||||
expect(drawer()).toHaveClass('isOpen');
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
await user.click(toggle);
|
||||
|
||||
expect(screen.queryByTestId('saved-views-panel')).not.toBeInTheDocument();
|
||||
expect(drawer()).not.toHaveClass('isOpen');
|
||||
});
|
||||
|
||||
it('closes the panel on a click outside the sidebar, not inside it', async () => {
|
||||
render(
|
||||
<>
|
||||
<button type="button" data-testid="page-tab">
|
||||
Pipelines
|
||||
</button>
|
||||
<QuickFiltersLayout
|
||||
showFilters
|
||||
quickFilterProps={quickFilterProps}
|
||||
savedViewProps={savedViewProps}
|
||||
>
|
||||
<div data-testid="page-content">content</div>
|
||||
</QuickFiltersLayout>
|
||||
</>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.click(await screen.findByTestId('saved-views-name-toggle'));
|
||||
await user.click(screen.getByTestId('saved-views-panel-header'));
|
||||
expect(screen.getByTestId('saved-views-panel')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByTestId('page-tab'));
|
||||
expect(screen.queryByTestId('saved-views-panel')).not.toBeInTheDocument();
|
||||
expect(drawer()).not.toHaveClass('isOpen');
|
||||
|
||||
await user.click(await screen.findByTestId('saved-views-name-toggle'));
|
||||
await user.click(screen.getByTestId('page-content'));
|
||||
expect(screen.queryByTestId('saved-views-panel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hands the source to the header and the panel', async () => {
|
||||
render(
|
||||
<QuickFiltersLayout
|
||||
@@ -257,6 +276,9 @@ describe('QuickFiltersLayout', () => {
|
||||
expect(screen.getByTestId('saved-views-header')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('saved-views-panel')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('saved-views-open')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('saved-views-name-toggle'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('saved-views-panel-header'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createPortal } from 'react-dom';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import { useCloseOnOutsidePress } from '../useCloseOnOutsidePress';
|
||||
|
||||
function Harness({
|
||||
enabled,
|
||||
onClose,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
onClose: () => void;
|
||||
}): JSX.Element {
|
||||
const markInside = useCloseOnOutsidePress(enabled, onClose);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onPointerDownCapture={markInside}>
|
||||
<button type="button" data-testid="inside">
|
||||
inside
|
||||
</button>
|
||||
{createPortal(
|
||||
<button type="button" data-testid="portal">
|
||||
menu item
|
||||
</button>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
<button type="button" data-testid="outside">
|
||||
outside
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
describe('useCloseOnOutsidePress', () => {
|
||||
it('closes on a press outside', async () => {
|
||||
const onClose: jest.MockedFunction<() => void> = jest.fn();
|
||||
render(<Harness enabled onClose={onClose} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId('outside'));
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('stays open on a press inside, including a portal rendered from inside', async () => {
|
||||
const onClose: jest.MockedFunction<() => void> = jest.fn();
|
||||
render(<Harness enabled onClose={onClose} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId('inside'));
|
||||
await userEvent.click(screen.getByTestId('portal'));
|
||||
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing while disabled', async () => {
|
||||
const onClose: jest.MockedFunction<() => void> = jest.fn();
|
||||
render(<Harness enabled={false} onClose={onClose} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId('outside'));
|
||||
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Calls `onClose` on any press outside the element given the returned handler as
|
||||
* `onPointerDownCapture`. Menus and dialogs rendered in portals still count as
|
||||
* inside: React events follow the component tree, so their presses reach the
|
||||
* handler before the document listener runs.
|
||||
*/
|
||||
export function useCloseOnOutsidePress(
|
||||
enabled: boolean,
|
||||
onClose: () => void,
|
||||
): () => void {
|
||||
const isPressInside = useRef(false);
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return undefined;
|
||||
}
|
||||
isPressInside.current = false;
|
||||
|
||||
const handlePointerDown = (): void => {
|
||||
if (isPressInside.current) {
|
||||
isPressInside.current = false;
|
||||
return;
|
||||
}
|
||||
onCloseRef.current();
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', handlePointerDown);
|
||||
return (): void =>
|
||||
document.removeEventListener('pointerdown', handlePointerDown);
|
||||
}, [enabled]);
|
||||
|
||||
return useCallback((): void => {
|
||||
isPressInside.current = true;
|
||||
}, []);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
QuickfiltertypesSourceDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldKeysConfig } from 'api/querySuggestions/types';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { QUICK_FILTER_SOURCE_TO_SIGNAL } from 'components/QuickFilters/constants';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import {
|
||||
@@ -13,7 +15,6 @@ import {
|
||||
FieldDataType,
|
||||
TelemetryFieldKey,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
function OtherFiltersSkeleton(): JSX.Element {
|
||||
return (
|
||||
@@ -32,18 +33,20 @@ function OtherFiltersSkeleton(): JSX.Element {
|
||||
}
|
||||
|
||||
function OtherFilters({
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
inputValue,
|
||||
addedFilters,
|
||||
setAddedFilters,
|
||||
}: {
|
||||
signal: SignalType | undefined;
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO;
|
||||
inputValue: string;
|
||||
addedFilters: TelemetryFieldKey[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
}): JSX.Element {
|
||||
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
|
||||
const isAIObservability = signal === SignalType.AI_OBSERVABILITY;
|
||||
const isMeterDataSource =
|
||||
savedFiltersSource === QuickfiltertypesSourceDTO.meter;
|
||||
const isAIObservability =
|
||||
savedFiltersSource === QuickfiltertypesSourceDTO.ai_observability;
|
||||
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
@@ -53,9 +56,7 @@ function OtherFilters({
|
||||
? { searchText: inputValue }
|
||||
: {
|
||||
searchText: inputValue,
|
||||
signal: signal
|
||||
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
|
||||
: undefined,
|
||||
signal: QUICK_FILTER_SOURCE_TO_SIGNAL[savedFiltersSource],
|
||||
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Button as SignozButton } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { SignalType } from '../types';
|
||||
import AddedFilters from './AddedFilters';
|
||||
import useQuickFilterSettings from './hooks/useQuickFilterSettings';
|
||||
import OtherFilters from './OtherFilters';
|
||||
@@ -12,12 +12,12 @@ import OtherFilters from './OtherFilters';
|
||||
import './QuickFiltersSettings.styles.scss';
|
||||
|
||||
function QuickFiltersSettings({
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
setIsSettingsOpen,
|
||||
customFilters,
|
||||
refetchCustomFilters,
|
||||
}: {
|
||||
signal: SignalType | undefined;
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO;
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
@@ -37,7 +37,7 @@ function QuickFiltersSettings({
|
||||
setIsSettingsOpen,
|
||||
customFilters,
|
||||
refetchCustomFilters,
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -73,7 +73,7 @@ function QuickFiltersSettings({
|
||||
setAddedFilters={setAddedFilters}
|
||||
/>
|
||||
<OtherFilters
|
||||
signal={signal}
|
||||
savedFiltersSource={savedFiltersSource}
|
||||
inputValue={debouncedInputValue}
|
||||
addedFilters={addedFilters}
|
||||
setAddedFilters={setAddedFilters}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { SignalType } from '../../types';
|
||||
import OtherFilters from '../OtherFilters';
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
@@ -43,10 +43,12 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
);
|
||||
});
|
||||
|
||||
function renderOtherFilters(signal: SignalType): void {
|
||||
function renderOtherFilters(
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO,
|
||||
): void {
|
||||
render(
|
||||
<OtherFilters
|
||||
signal={signal}
|
||||
savedFiltersSource={savedFiltersSource}
|
||||
inputValue=""
|
||||
addedFilters={[]}
|
||||
setAddedFilters={jest.fn()}
|
||||
@@ -55,7 +57,7 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
}
|
||||
|
||||
it('reads AI observability keys from their own endpoint', async () => {
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
renderOtherFilters(QuickfiltertypesSourceDTO.ai_observability);
|
||||
|
||||
await expect(
|
||||
screen.findByText('gen_ai.request.model'),
|
||||
@@ -64,7 +66,7 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
});
|
||||
|
||||
it('does not narrow the AI keys by fieldContext', async () => {
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
renderOtherFilters(QuickfiltertypesSourceDTO.ai_observability);
|
||||
|
||||
// A `trace` context would return only the computed per-trace aggregates,
|
||||
// which cannot be filtered on.
|
||||
@@ -73,7 +75,7 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
});
|
||||
|
||||
it('keeps other signals on the signal-wide keys endpoint', async () => {
|
||||
renderOtherFilters(SignalType.TRACES);
|
||||
renderOtherFilters(QuickfiltertypesSourceDTO.traces);
|
||||
|
||||
await expect(screen.findByText('http.route')).resolves.toBeInTheDocument();
|
||||
await waitFor(() => expect(aiKeysParams).toBeUndefined());
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const SIGNAL_DATA_SOURCE_MAP = {
|
||||
[SignalType.LOGS]: DataSource.LOGS,
|
||||
[SignalType.TRACES]: DataSource.TRACES,
|
||||
[SignalType.EXCEPTIONS]: DataSource.TRACES,
|
||||
[SignalType.API_MONITORING]: DataSource.TRACES,
|
||||
[SignalType.METER_EXPLORER]: DataSource.METRICS,
|
||||
[SignalType.AI_OBSERVABILITY]: DataSource.TRACES,
|
||||
};
|
||||
@@ -4,8 +4,8 @@ import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesFieldDataTypeDTO,
|
||||
QuickfiltertypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
@@ -16,7 +16,7 @@ interface UseQuickFilterSettingsProps {
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
signal?: SignalType;
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO;
|
||||
}
|
||||
|
||||
interface UseQuickFilterSettingsReturn {
|
||||
@@ -37,7 +37,7 @@ const useQuickFilterSettings = ({
|
||||
customFilters,
|
||||
setIsSettingsOpen,
|
||||
refetchCustomFilters,
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => {
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const [debouncedInputValue, setDebouncedInputValue] = useState<string>('');
|
||||
@@ -113,21 +113,19 @@ const useQuickFilterSettings = ({
|
||||
);
|
||||
|
||||
const handleSaveChanges = useCallback((): void => {
|
||||
if (signal) {
|
||||
updateCustomFilters({
|
||||
pathParams: { source: signal },
|
||||
data: {
|
||||
// Send only the stored TelemetryFieldKey fields; the composite `key`
|
||||
// is UI-only.
|
||||
filters: addedFilters.map((filter) => ({
|
||||
name: filter.name,
|
||||
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
|
||||
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [addedFilters, signal, updateCustomFilters]);
|
||||
updateCustomFilters({
|
||||
pathParams: { source: savedFiltersSource },
|
||||
data: {
|
||||
// Send only the stored TelemetryFieldKey fields; the composite `key`
|
||||
// is UI-only.
|
||||
filters: addedFilters.map((filter) => ({
|
||||
name: filter.name,
|
||||
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
|
||||
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}, [addedFilters, savedFiltersSource, updateCustomFilters]);
|
||||
|
||||
return {
|
||||
handleSettingsClose,
|
||||
|
||||
16
frontend/src/components/QuickFilters/constants.ts
Normal file
16
frontend/src/components/QuickFilters/constants.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { QuickFiltersSource } from './types';
|
||||
|
||||
export const QUICK_FILTER_SOURCE_TO_SIGNAL: Record<
|
||||
QuickFiltersSource,
|
||||
TelemetrytypesSignalDTO
|
||||
> = {
|
||||
[QuickFiltersSource.LOGS_EXPLORER]: TelemetrytypesSignalDTO.logs,
|
||||
[QuickFiltersSource.TRACES_EXPLORER]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.EXCEPTIONS]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.API_MONITORING]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.AI_OBSERVABILITY]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.METER_EXPLORER]: TelemetrytypesSignalDTO.metrics,
|
||||
[QuickFiltersSource.INFRA_MONITORING]: TelemetrytypesSignalDTO.metrics,
|
||||
};
|
||||
@@ -10,26 +10,26 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
* In ListView most sources use index 0; TRACES_EXPLORER and every non-ListView
|
||||
* mode track the last focused query.
|
||||
*/
|
||||
function useActiveQueryIndex(source: QuickFiltersSource): number {
|
||||
function useActiveQueryIndex(quickFilterSource: QuickFiltersSource): number {
|
||||
const { lastUsedQuery, panelType } = useQueryBuilder();
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
|
||||
return useMemo(() => {
|
||||
// AI observability builds a single query in the row-level views, so its
|
||||
// filters always drive the first one there.
|
||||
if (source === QuickFiltersSource.AI_OBSERVABILITY) {
|
||||
if (quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY) {
|
||||
return isListView || panelType === PANEL_TYPES.TRACE
|
||||
? 0
|
||||
: lastUsedQuery || 0;
|
||||
}
|
||||
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
return quickFilterSource === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, panelType, source, lastUsedQuery]);
|
||||
}, [isListView, panelType, quickFilterSource, lastUsedQuery]);
|
||||
}
|
||||
|
||||
export default useActiveQueryIndex;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGetQuickFilters } from 'api/generated/services/quick-filter';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { IQuickFiltersConfig, SignalType } from '../types';
|
||||
import { IQuickFiltersConfig } from '../types';
|
||||
import { getFilterConfig } from '../utils';
|
||||
|
||||
interface UseFilterConfigProps {
|
||||
signal?: SignalType;
|
||||
savedFiltersSource?: QuickfiltertypesSourceDTO;
|
||||
config: IQuickFiltersConfig[];
|
||||
}
|
||||
interface UseFilterConfigReturn {
|
||||
@@ -18,7 +19,7 @@ interface UseFilterConfigReturn {
|
||||
}
|
||||
|
||||
const useFilterConfig = ({
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
config,
|
||||
}: UseFilterConfigProps): UseFilterConfigReturn => {
|
||||
const {
|
||||
@@ -26,8 +27,8 @@ const useFilterConfig = ({
|
||||
data,
|
||||
refetch,
|
||||
} = useGetQuickFilters(
|
||||
{ source: signal ?? '' },
|
||||
{ query: { enabled: !!signal } },
|
||||
{ source: savedFiltersSource ?? '' },
|
||||
{ query: { enabled: !!savedFiltersSource } },
|
||||
);
|
||||
|
||||
const customFilters = useMemo<TelemetryFieldKey[]>(
|
||||
@@ -35,11 +36,11 @@ const useFilterConfig = ({
|
||||
[data],
|
||||
);
|
||||
|
||||
const isDynamicFilters = !!signal;
|
||||
const isDynamicFilters = !!savedFiltersSource;
|
||||
|
||||
const filterConfig = useMemo(
|
||||
() => getFilterConfig(signal, customFilters, config),
|
||||
[config, customFilters, signal],
|
||||
() => getFilterConfig(customFilters, config),
|
||||
[config, customFilters],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,11 +11,13 @@ import {
|
||||
import { rest, type RequestHandler } from 'msw';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { attributeValuesResponse } from '@/storybook/msw/__story_mockdata__/attributes';
|
||||
import { fieldKeysResponse } from '@/storybook/msw/__story_mockdata__/fields';
|
||||
import {
|
||||
fieldKeysResponse,
|
||||
fieldValuesResponse,
|
||||
} from '@/storybook/msw/__story_mockdata__/fields';
|
||||
import { quickFiltersResponse } from '@/storybook/msw/__story_mockdata__/quickFilters';
|
||||
|
||||
import { FiltersType } from '../types';
|
||||
import { FiltersType, QuickFilterCheckboxUseFieldApis } from '../types';
|
||||
|
||||
const customFilters = [
|
||||
{
|
||||
@@ -61,13 +63,15 @@ export const checkboxConfig = [
|
||||
},
|
||||
];
|
||||
|
||||
export const attributeValuesHandler = (
|
||||
values: readonly string[],
|
||||
): RequestHandler =>
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/attribute_values',
|
||||
(_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(attributeValuesResponse(values))),
|
||||
export const fieldApis: QuickFilterCheckboxUseFieldApis = {
|
||||
startUnixMilli: 1700000000000,
|
||||
endUnixMilli: 1700003600000,
|
||||
existingQuery: null,
|
||||
};
|
||||
|
||||
export const fieldValuesHandler = (values: readonly string[]): RequestHandler =>
|
||||
rest.get('http://localhost/api/v1/fields/values', (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(fieldValuesResponse(values))),
|
||||
);
|
||||
|
||||
export const handlers = [
|
||||
@@ -82,9 +86,18 @@ export const handlers = [
|
||||
rest.get('http://localhost/api/v1/fields/keys', (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(fieldKeysResponse(['k8s.namespace.name']))),
|
||||
),
|
||||
attributeValuesHandler(['checkout', 'frontend', 'payments']),
|
||||
fieldValuesHandler(['checkout', 'frontend', 'payments']),
|
||||
];
|
||||
|
||||
export const noSavedFiltersHandler = rest.get(
|
||||
'http://localhost/api/v2/quick_filters/logs',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json(quickFiltersResponse(QuickfiltertypesSourceDTO.logs, [])),
|
||||
),
|
||||
);
|
||||
|
||||
export const loadingFiltersHandlers = [
|
||||
rest.get('http://localhost/api/v2/quick_filters/logs', (_req, res, ctx) =>
|
||||
res(ctx.delay('infinite')),
|
||||
|
||||
@@ -7,15 +7,17 @@ import type { GlobalMockArgs } from '@/storybook/globals';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import {
|
||||
attributeValuesHandler,
|
||||
checkboxConfig,
|
||||
fieldApis,
|
||||
fieldValuesHandler,
|
||||
handlers,
|
||||
LONG_FILTER_VALUES,
|
||||
loadingFiltersHandlers,
|
||||
queryBuilder,
|
||||
selectedServiceQueryBuilder,
|
||||
noSavedFiltersHandler,
|
||||
} from './QuickFilters.stories.mocks';
|
||||
import { QuickFiltersSource, SignalType } from '../types';
|
||||
import { QuickFiltersSource } from '../types';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Quick Filters',
|
||||
@@ -32,8 +34,8 @@ const meta = {
|
||||
args: {
|
||||
config: checkboxConfig,
|
||||
handleFilterVisibilityChange: (): void => undefined,
|
||||
signal: SignalType.LOGS,
|
||||
source: QuickFiltersSource.LOGS_EXPLORER,
|
||||
quickFilterSource: QuickFiltersSource.LOGS_EXPLORER,
|
||||
useFieldApis: fieldApis,
|
||||
},
|
||||
parameters: {
|
||||
msw: { handlers },
|
||||
@@ -92,13 +94,16 @@ export const LoadingFilters: Story = {
|
||||
|
||||
/** Empty: a loaded quick-filter configuration with no filters has no result rows. */
|
||||
export const NoResults: Story = {
|
||||
args: { config: [], signal: undefined },
|
||||
args: { config: [] },
|
||||
parameters: {
|
||||
msw: { handlers: [noSavedFiltersHandler, ...handlers] },
|
||||
},
|
||||
};
|
||||
|
||||
/** Selection: an expanded checkbox shows the actual selected service values. */
|
||||
export const SelectedExpandedCheckbox: Story = {
|
||||
args: { signal: undefined },
|
||||
parameters: {
|
||||
msw: { handlers: [noSavedFiltersHandler, ...handlers] },
|
||||
signoz: {
|
||||
queryBuilder: selectedServiceQueryBuilder,
|
||||
},
|
||||
@@ -109,12 +114,18 @@ export const SelectedExpandedCheckbox: Story = {
|
||||
* Every tooltip the panel renders, held open: the reveal on each truncated
|
||||
* filter value. Nothing bounds those values, so the panel is answered with
|
||||
* service names long enough to be cut. The Service name filter carries them, so
|
||||
* the signal that would add the workspace's own dynamic filters is left off.
|
||||
* the workspace's own saved filter list is answered empty.
|
||||
*/
|
||||
export const Tooltips: TooltipsStory = {
|
||||
args: { signal: undefined, tooltipsOpen: true },
|
||||
args: { tooltipsOpen: true },
|
||||
parameters: {
|
||||
msw: { handlers: [attributeValuesHandler(LONG_FILTER_VALUES), ...handlers] },
|
||||
msw: {
|
||||
handlers: [
|
||||
noSavedFiltersHandler,
|
||||
fieldValuesHandler(LONG_FILTER_VALUES),
|
||||
...handlers,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import {
|
||||
otherFiltersResponse,
|
||||
quickFiltersAttributeValuesResponse,
|
||||
quickFiltersFieldValuesResponse,
|
||||
quickFiltersListResponse,
|
||||
} from 'mocks-server/__mockdata__/customQuickFilters';
|
||||
import { server } from 'mocks-server/server';
|
||||
@@ -24,8 +24,8 @@ import { rest } from 'msw';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from '../types';
|
||||
import { QuickFiltersConfig } from './constants';
|
||||
import { QuickFiltersSource } from '../types';
|
||||
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
@@ -35,10 +35,8 @@ jest.mock('container/ApiMonitoring/queryParams');
|
||||
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/logs`;
|
||||
const fieldsKeysURL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const attributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
|
||||
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
|
||||
|
||||
const NOT_AUTHORIZED_TEXT = /is not authorized to perform/i;
|
||||
@@ -71,11 +69,8 @@ beforeEach(() => {
|
||||
rest.get(fieldsKeysURL, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(otherFiltersResponse)),
|
||||
),
|
||||
rest.get(attributeValuesURL, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
||||
),
|
||||
rest.get(fieldsValuesURL, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
||||
res(ctx.status(200), ctx.json(quickFiltersFieldValuesResponse)),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -85,12 +80,12 @@ afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function renderWithSignal(): void {
|
||||
function renderWithSavedList(): void {
|
||||
render(
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
signal={SIGNAL}
|
||||
quickFilterSource={QuickFiltersSource.LOGS_EXPLORER}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
@@ -98,9 +93,10 @@ function renderWithSignal(): void {
|
||||
function renderStaticConfig(): void {
|
||||
render(
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={QuickFiltersConfig}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
@@ -110,7 +106,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
it('shows the inline denial instead of the filters, header stays', async () => {
|
||||
server.use(setupAuthzDeny(QuickFilterReadPermission));
|
||||
|
||||
renderWithSignal();
|
||||
renderWithSavedList();
|
||||
|
||||
await expect(
|
||||
screen.findByText(NOT_AUTHORIZED_TEXT),
|
||||
@@ -135,7 +131,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
it('renders the filters but disables the settings trigger', async () => {
|
||||
server.use(setupAuthzDeny(QuickFilterUpdatePermission));
|
||||
|
||||
renderWithSignal();
|
||||
renderWithSavedList();
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -160,7 +156,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
it('renders the filters and opens settings from the trigger', async () => {
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
renderWithSignal();
|
||||
renderWithSavedList();
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -178,7 +174,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('static config pages (no signal)', () => {
|
||||
describe('static config pages (no saved list)', () => {
|
||||
it('is not gated even when every permission is denied', async () => {
|
||||
server.use(setupAuthzDenyAll());
|
||||
|
||||
@@ -197,7 +193,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => res(ctx.delay('infinite'))),
|
||||
);
|
||||
|
||||
renderWithSignal();
|
||||
renderWithSavedList();
|
||||
|
||||
await waitFor(() =>
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import {
|
||||
ApiMonitoringParams,
|
||||
useApiMonitoringParams,
|
||||
} from 'container/ApiMonitoring/queryParams';
|
||||
import { getHostsQuickFiltersConfig } from 'container/InfraMonitoringHostsV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
otherFiltersResponse,
|
||||
quickFiltersAttributeValuesResponse,
|
||||
quickFiltersFieldValuesResponse,
|
||||
quickFiltersListResponse,
|
||||
} from 'mocks-server/__mockdata__/customQuickFilters';
|
||||
import { server } from 'mocks-server/server';
|
||||
@@ -16,8 +19,8 @@ import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
|
||||
import { QuickFiltersConfig } from './constants';
|
||||
import { IQuickFiltersConfig, QuickFiltersSource } from '../types';
|
||||
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
@@ -33,11 +36,10 @@ const mockSetApiMonitoringParams = jest.fn() as jest.MockedFunction<
|
||||
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/logs`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/logs`;
|
||||
const anyQuickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/:source`;
|
||||
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
|
||||
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
|
||||
|
||||
const FILTER_OS_DESCRIPTION = 'os.description';
|
||||
@@ -55,6 +57,9 @@ const setupServer = (): void => {
|
||||
rest.get(quickFiltersListURL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(quickFiltersListResponse)),
|
||||
),
|
||||
rest.get(anyQuickFiltersListURL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: { filters: [] } })),
|
||||
),
|
||||
rest.get(quickFiltersSuggestionsURL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(otherFiltersResponse)),
|
||||
),
|
||||
@@ -62,56 +67,31 @@ const setupServer = (): void => {
|
||||
putHandler(await req.json());
|
||||
return res(ctx.status(200), ctx.json({}));
|
||||
}),
|
||||
rest.get(quickFiltersAttributeValuesURL, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
||||
),
|
||||
rest.get(fieldsValuesURL, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
||||
res(ctx.status(200), ctx.json(quickFiltersFieldValuesResponse)),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
function TestQuickFilters({
|
||||
signal = SignalType.LOGS,
|
||||
quickFilterSource = QuickFiltersSource.EXCEPTIONS,
|
||||
config = QuickFiltersConfig,
|
||||
}: {
|
||||
signal?: SignalType;
|
||||
quickFilterSource?: QuickFiltersSource;
|
||||
config?: IQuickFiltersConfig[];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
quickFilterSource={quickFilterSource}
|
||||
config={config}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
signal={signal}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TestQuickFilters.defaultProps = {
|
||||
signal: '',
|
||||
config: QuickFiltersConfig,
|
||||
};
|
||||
|
||||
function TestQuickFiltersApiMonitoring({
|
||||
signal = SignalType.LOGS,
|
||||
config = QuickFiltersConfig,
|
||||
}: {
|
||||
signal?: SignalType;
|
||||
config?: IQuickFiltersConfig[];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
config={config}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
signal={signal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TestQuickFiltersApiMonitoring.defaultProps = {
|
||||
signal: '',
|
||||
quickFilterSource: QuickFiltersSource.EXCEPTIONS,
|
||||
config: QuickFiltersConfig,
|
||||
};
|
||||
|
||||
@@ -289,10 +269,14 @@ describe('Quick Filters', () => {
|
||||
it('toggles Show IP addresses and updates API Monitoring params', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFiltersApiMonitoring />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.API_MONITORING} />,
|
||||
);
|
||||
|
||||
// Switch should be rendered and initially checked
|
||||
expect(screen.getByText('Show IP addresses')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText('Show IP addresses'),
|
||||
).resolves.toBeInTheDocument();
|
||||
const toggle = screen.getByRole('switch');
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
|
||||
@@ -310,7 +294,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('loads the custom filters correctly', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Filters for')).toBeInTheDocument();
|
||||
expect(screen.getByText(QUERY_NAME)).toBeInTheDocument();
|
||||
@@ -370,7 +356,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
),
|
||||
);
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -398,7 +386,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -419,7 +409,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('removes a filter from ADDED FILTERS and moves it to OTHER FILTERS', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -448,7 +440,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('restores original filter state on Discard', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -490,7 +484,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('saves the updated filters by calling PUT with correct payload', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -527,7 +523,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
pointerEventsCheck: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<TestQuickFilters signal={SIGNAL} />);
|
||||
const { getByTestId } = render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
expect(screen.getByText('Duration')).toBeInTheDocument();
|
||||
|
||||
@@ -581,7 +579,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
});
|
||||
|
||||
describe('Quick Filters refetch behavior', () => {
|
||||
it('fetches custom filters on every mount when signal is provided', async () => {
|
||||
it('fetches custom filters on every mount for a page with a saved list', async () => {
|
||||
let getCalls = 0;
|
||||
|
||||
server.use(
|
||||
@@ -591,14 +589,18 @@ describe('Quick Filters refetch behavior', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { unmount } = render(<TestQuickFilters signal={SIGNAL} />);
|
||||
const { unmount } = render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
).resolves.toBeInTheDocument();
|
||||
@@ -606,19 +608,24 @@ describe('Quick Filters refetch behavior', () => {
|
||||
expect(getCalls).toBe(2);
|
||||
});
|
||||
|
||||
it('does not fetch custom filters when signal is undefined', async () => {
|
||||
it('does not fetch custom filters for a page without a saved list', async () => {
|
||||
let getCalls = 0;
|
||||
|
||||
server.use(
|
||||
rest.get(quickFiltersListURL, (_req, res, ctx) => {
|
||||
rest.get(anyQuickFiltersListURL, (_req, res, ctx) => {
|
||||
getCalls += 1;
|
||||
return res(ctx.status(200), ctx.json(quickFiltersListResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestQuickFilters signal={undefined} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.INFRA_MONITORING} />,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getCalls).toBe(0));
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(getCalls).toBe(0);
|
||||
});
|
||||
|
||||
it('refetches custom filters after saving settings', async () => {
|
||||
@@ -637,7 +644,9 @@ describe('Quick Filters refetch behavior', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -689,7 +698,9 @@ describe('Quick Filters refetch behavior', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -720,10 +731,47 @@ describe('Quick Filters refetch behavior', () => {
|
||||
),
|
||||
);
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} config={[]} />);
|
||||
render(
|
||||
<TestQuickFilters
|
||||
quickFilterSource={QuickFiltersSource.LOGS_EXPLORER}
|
||||
config={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText('No filters found'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Quick Filters field APIs', () => {
|
||||
it('sends the page signal for every filter, the infra Environment filter included', async () => {
|
||||
const requests: URLSearchParams[] = [];
|
||||
server.use(
|
||||
rest.get(fieldsValuesURL, (req, res, ctx) => {
|
||||
requests.push(req.url.searchParams);
|
||||
return res(ctx.status(200), ctx.json(quickFiltersFieldValuesResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<QuickFilters
|
||||
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={getHostsQuickFiltersConfig()}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(requests.map((params) => params.get('name'))).toContain(
|
||||
INFRA_MONITORING_ATTR_KEYS.DEPLOYMENT_ENVIRONMENT,
|
||||
),
|
||||
);
|
||||
const environment = requests.find(
|
||||
(params) =>
|
||||
params.get('name') === INFRA_MONITORING_ATTR_KEYS.DEPLOYMENT_ENVIRONMENT,
|
||||
);
|
||||
expect(environment?.get('signal')).toBe(TelemetrytypesSignalDTO.metrics);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { FiltersType } from '../types';
|
||||
import { FiltersType, QuickFilterCheckboxUseFieldApis } from '../types';
|
||||
|
||||
export const QuickFiltersConfig = [
|
||||
{
|
||||
@@ -24,3 +24,9 @@ export const QuickFiltersConfig = [
|
||||
defaultOpen: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const QuickFiltersFieldApis: QuickFilterCheckboxUseFieldApis = {
|
||||
startUnixMilli: 1700000000000,
|
||||
endUnixMilli: 1700003600000,
|
||||
existingQuery: null,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export enum FiltersType {
|
||||
SLIDER = 'SLIDER',
|
||||
@@ -19,15 +19,6 @@ export enum SpecficFilterOperations {
|
||||
ONLY = 'ONLY',
|
||||
}
|
||||
|
||||
export enum SignalType {
|
||||
TRACES = 'traces',
|
||||
LOGS = 'logs',
|
||||
API_MONITORING = 'api_monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai_observability',
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing export from signozhq/ui/checkbox, TODO(H4ad): Add and remove this type definition
|
||||
*/
|
||||
@@ -39,7 +30,6 @@ export interface IQuickFiltersConfig {
|
||||
attributeKey: BaseAutocompleteData;
|
||||
aggregateOperator?: string;
|
||||
aggregateAttribute?: string;
|
||||
dataSource?: DataSource;
|
||||
customRendererForValue?: (value: string) => JSX.Element;
|
||||
defaultOpen: boolean;
|
||||
}
|
||||
@@ -53,31 +43,30 @@ export interface QuickFilterChangeEventData {
|
||||
export interface IQuickFiltersProps {
|
||||
config: IQuickFiltersConfig[];
|
||||
handleFilterVisibilityChange?: () => void;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
|
||||
/** Pass to fetch quick filters for this signal; omit to use `config` as-is */
|
||||
signal?: SignalType;
|
||||
className?: string;
|
||||
showFilterCollapse?: boolean;
|
||||
showQueryName?: boolean;
|
||||
useFieldApis?: QuickFilterCheckboxUseFieldApis;
|
||||
useFieldApis: QuickFilterCheckboxUseFieldApis;
|
||||
savedViewsHeader?: ReactNode;
|
||||
}
|
||||
|
||||
export enum QuickFiltersSource {
|
||||
LOGS_EXPLORER = 'logs-explorer',
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai-observability',
|
||||
}
|
||||
export const QuickFiltersSource = {
|
||||
LOGS_EXPLORER: QuickfiltertypesSourceDTO.logs,
|
||||
TRACES_EXPLORER: QuickfiltertypesSourceDTO.traces,
|
||||
EXCEPTIONS: QuickfiltertypesSourceDTO.exceptions,
|
||||
API_MONITORING: QuickfiltertypesSourceDTO.api_monitoring,
|
||||
METER_EXPLORER: QuickfiltertypesSourceDTO.meter,
|
||||
AI_OBSERVABILITY: QuickfiltertypesSourceDTO.ai_observability,
|
||||
/** No saved filter list: renders `config` as given. */
|
||||
INFRA_MONITORING: 'infra-monitoring',
|
||||
} as const;
|
||||
|
||||
export type QuickFiltersSource =
|
||||
(typeof QuickFiltersSource)[keyof typeof QuickFiltersSource];
|
||||
|
||||
/**
|
||||
* Opt-in: fetch values from the /v1/fields/values API instead of /v3/autocomplete/attribute_values
|
||||
*/
|
||||
export type QuickFilterCheckboxUseFieldApis = {
|
||||
startUnixMilli: number;
|
||||
endUnixMilli: number;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
|
||||
|
||||
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
|
||||
import { FiltersType, IQuickFiltersConfig } from './types';
|
||||
|
||||
const FILTER_TITLE_MAP: Record<string, string> = {
|
||||
duration_nano: 'Duration',
|
||||
@@ -50,11 +49,10 @@ const getFilterType = (att: TelemetryFieldKey): FiltersType => {
|
||||
};
|
||||
|
||||
export const getFilterConfig = (
|
||||
signal?: SignalType,
|
||||
customFilters?: TelemetryFieldKey[],
|
||||
config?: IQuickFiltersConfig[],
|
||||
): IQuickFiltersConfig[] => {
|
||||
if (!customFilters?.length || !signal) {
|
||||
if (!customFilters?.length) {
|
||||
return config || [];
|
||||
}
|
||||
|
||||
@@ -63,7 +61,6 @@ export const getFilterConfig = (
|
||||
({
|
||||
type: getFilterType(att),
|
||||
title: getFilterName(att.name),
|
||||
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
|
||||
attributeKey: {
|
||||
id: att.name,
|
||||
key: att.name,
|
||||
|
||||
@@ -21,7 +21,6 @@ export enum LOCALSTORAGE {
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
|
||||
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
|
||||
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
||||
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
|
||||
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
||||
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
|
||||
@@ -47,5 +46,4 @@ export enum LOCALSTORAGE {
|
||||
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
|
||||
DASHBOARD_V2_PANEL_COLUMN_WIDTHS = 'DASHBOARD_V2_PANEL_COLUMN_WIDTHS',
|
||||
LLM_ATTRIBUTE_MAPPING_TEST_SPAN = 'LLM_ATTRIBUTE_MAPPING_TEST_SPAN',
|
||||
SAVED_VIEW_ENABLED = 'SAVED_VIEW_ENABLED',
|
||||
}
|
||||
|
||||
@@ -50,8 +50,6 @@ const ROUTES = {
|
||||
ROLE_EDIT: '/settings/roles/:roleId/edit',
|
||||
MEMBERS_SETTINGS: '/settings/members',
|
||||
SUPPORT: '/support',
|
||||
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
||||
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
||||
TRACES_FUNNELS: '/traces/funnels',
|
||||
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
|
||||
WORKSPACE_LOCKED: '/workspace-locked',
|
||||
@@ -69,7 +67,6 @@ const ROUTES = {
|
||||
MESSAGING_QUEUES_OVERVIEW: '/messaging-queues/overview',
|
||||
METRICS_EXPLORER: '/metrics-explorer/summary',
|
||||
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
||||
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
||||
METRICS_EXPLORER_VOLUME_CONTROL: '/metrics-explorer/volume-control',
|
||||
API_MONITORING_BASE: '/api-monitoring',
|
||||
API_MONITORING: '/api-monitoring/explorer',
|
||||
@@ -77,7 +74,6 @@ const ROUTES = {
|
||||
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
||||
METER: '/meter',
|
||||
METER_EXPLORER: '/meter/explorer',
|
||||
METER_EXPLORER_VIEWS: '/meter/explorer/views',
|
||||
HOME_PAGE: '/',
|
||||
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
|
||||
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
.trigger {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 10;
|
||||
transition: transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user