Compare commits
5 Commits
test/uplot
...
backup/tra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
681e96df82 | ||
|
|
ef36279e1c | ||
|
|
c64af15ac6 | ||
|
|
f45d683ec2 | ||
|
|
edca5d2e3e |
@@ -1,15 +0,0 @@
|
||||
---
|
||||
paths:
|
||||
- "frontend/src/**/*.tsx"
|
||||
---
|
||||
|
||||
# Storybook
|
||||
|
||||
Page stories are written with the `signoz-page-story` skill ([`.claude/skills/signoz-page-story/SKILL.md`](../skills/signoz-page-story/SKILL.md)); the API surface is [`frontend/src/storybook/README.md`](../../frontend/src/storybook/README.md).
|
||||
|
||||
- **A UI change lands with its story.** Any change that alters what a page renders — a new page, a new state, a new branch, a changed layout — updates the story of every page that renders it, or creates one where the page has none. Go through the `signoz-page-story` skill; never hand-write a page story outside it.
|
||||
- **New page, new story. New state, new control or variant.** The skill decides which: a control on a response, a variant worth linking to, or a `play` story for a state only a click reaches.
|
||||
- **A shared component is covered through its pages.** A change under `components/` or `container/` with no story of its own updates the stories of the pages that render it.
|
||||
- **Already covered, say so.** When the existing story and its controls already reach the changed state, no story edit is needed; state that in the PR description.
|
||||
- **Opt out only when there is no visual diff.** Skip stories only when the change renders nothing differently: types, api clients, hooks with no UI branch, tests. Deleting a page deletes its stories.
|
||||
- **Prove it with a visual diff when asked.** Before/after screenshots and "did this touch other screens" go through `sbshot-visual-diff`, which can only show what a story renders.
|
||||
@@ -1,142 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,114 +0,0 @@
|
||||
# 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>/`.
|
||||
@@ -1,155 +0,0 @@
|
||||
# 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`.
|
||||
@@ -1,104 +0,0 @@
|
||||
---
|
||||
name: sbshot-visual-diff
|
||||
description: Shoot Storybook stories with sbshot at two git refs and produce a diff a reviewer can read. Use when asked for a visual diff, before/after screenshots, a screenshot comparison for a PR, or to prove a change did not alter screens it should not have.
|
||||
---
|
||||
|
||||
# sbshot visual diffs
|
||||
|
||||
`npx -y github:SigNoz/sbshot` shoots Storybook stories and pixel-diffs two runs. Use it. Do not hand-roll a Playwright screenshot loop or a canvas pixel-compare: sbshot already handles story selection, themes, settling, retries, diffing, tagging, a viewer and a static export, and its runs are the format the rest of the team can open.
|
||||
|
||||
sbshot is not installed by any package. It is run through `npx`, so nothing it knows travels into this repo on its own. This file is where that knowledge lives.
|
||||
|
||||
## The shape of a run
|
||||
|
||||
```
|
||||
capture <storybook> --name-run <ref> # once per git ref
|
||||
diff <baseline> <after> # pairs them by relative path
|
||||
tag <diff> <tag> <story-id>... # what the reviewer should look at
|
||||
export <diff> --out <dir> # a static site, hostable
|
||||
```
|
||||
|
||||
`<storybook>` is a running dev server (`http://localhost:6006`), a build directory holding `index.json`, or a project directory with `.storybook/`. The frontend has **no `build-storybook` script**, only `storybook dev -p 6006`, so point `capture` at a dev server.
|
||||
|
||||
## Cover the change with stories first
|
||||
|
||||
**A diff can only show what a story renders.** A screen, state or variant with no story is invisible to sbshot: it produces no pair, so it reads as "nothing changed" rather than "never looked at". This is the failure mode that makes a clean-looking diff worthless.
|
||||
|
||||
Before capturing, list the screens and states the change touches and check each has a story. For anything missing, write it first — the `signoz-page-story` skill covers how. New states reached only by an interaction need a `play` function, or the capture shoots the page at rest and the state never appears.
|
||||
|
||||
Then confirm it worked: the two runs' story counts should differ by exactly the stories the change adds. A new screen that produced no new story id is a screen nobody is reviewing. Say the count in the summary, so a reader can tell coverage from silence.
|
||||
|
||||
## Rules that are not optional
|
||||
|
||||
**One freshly started Storybook per ref.** Never start one server and switch git branches under it. Vite's HMR reloads the page, but some modules do not re-initialise, so the shot differs silently from a clean boot and the difference looks like a real change. Settle time does not fix this and is not the variable. Use one worktree and one port per ref, and stop each server before the next.
|
||||
|
||||
**Capture both themes: `--theme dark,light`.** Without it sbshot shoots the story's own default and everything lands in a single `default/` folder, so every regression in the other theme is invisible. Themes double the shot count, so scope with `--stories` or `--title`, never by dropping a theme.
|
||||
|
||||
**Baseline is the merge-base, not `origin/main`.** `git merge-base origin/main <branch>`. Against a moved main, every commit main gained since the branch's base shows up reversed, as though the branch reverted it.
|
||||
|
||||
**The percentage sorts the list, it does not tell you what matters.** A dialog that dims the page scores high for a one-line change, and a whole new form section can score low. Open the shots and say what changed on each screen. The biggest number is usually the least interesting one.
|
||||
|
||||
**If you cannot explain a pair, assume the capture is at fault, not the code.** Open it, crop both sides and compare them before calling it noise. A pixel count and a good guess are not evidence.
|
||||
|
||||
## Noise, and the floor that proves it
|
||||
|
||||
The same story does not always render identically twice. Pairs often move for reasons that have nothing to do with the change:
|
||||
|
||||
- a focus ring left on whatever a `play` function clicked last
|
||||
- a scrollbar appearing, or its thumb resting a few pixels off
|
||||
- a toast, tooltip or dialog caught mid-fade
|
||||
- a text caret blinking, or a selection highlight
|
||||
- an animation or spinner at a different frame
|
||||
- relative timestamps ("2 days ago") crossing a boundary
|
||||
- hover or active styling from where the pointer happened to rest
|
||||
- a late-loading font or image shifting a line of text
|
||||
|
||||
Do not argue about these one at a time. **Measure them**: shoot the same commit twice and diff those two runs against each other. Anything that moves there moved for no reason, so it is noise.
|
||||
|
||||
```
|
||||
capture <storybook> --name-run noise-a
|
||||
capture <storybook> --name-run noise-b # same tree, its own fresh server
|
||||
diff noise-a noise-b --name-diff noise
|
||||
diff <baseline> <after> --noise noise # marks the movers flaky
|
||||
```
|
||||
|
||||
**The noise run has to cover the same stories as the real diff.** Do not scope it down to the few pairs that moved, however tempting that is. `--noise` marks a pair flaky when the noise diff does not show it as unchanged, so every story the noise run leaves out is marked flaky too, including the new screens the reviewer is there to look at. Shooting two narrow runs and diffing those against each other has the same problem. Either shoot the full set twice, or skip `--noise` and explain the few movers by hand.
|
||||
|
||||
**`--noise` ignores how far a pair moved.** A pair is marked flaky when the noise diff does not show it as unchanged, so a 70-pixel flicker in the noise run hides a real 8% change in the real one. Compare the two numbers before trusting the mark. When most of the flaky marks are hiding real changes, run the diff without `--noise` and tag the genuinely noisy pairs by hand instead, so nothing real is filtered out of the reviewer's view.
|
||||
|
||||
That is the trade: a second full capture costs about as long as the first, so on a small diff it is often quicker to crop the two or three movers and say what each one is. Use `--noise` when the diff is large, or when wiring `--fail-over` into CI, where something has to decide without a person looking.
|
||||
|
||||
If a pair is flaky every run and the state it captures matters, fix the story rather than living with it: blur what the `play` clicked, freeze the clock, or assert the state instead of shooting it.
|
||||
|
||||
## Making it readable for a reviewer
|
||||
|
||||
A reviewer cannot open `localhost`, so `sbshot ui` is for you, not for them.
|
||||
|
||||
1. `tag` the pairs that matter, with `--note` saying why. The viewer filters by tag, so the reviewer lands on the real changes instead of scrolling past noise that scored higher.
|
||||
2. `export <diff> --out <dir>`. It writes a static site carrying only the pairs that are not identical, converted to WebP, so a large diff stays small.
|
||||
3. Host it. A Claude artifact works: publish the export's files and give the reviewer the link. `index.html` is reserved for the artifact's own page, so publish sbshot's entry as `viewer.html` and link to `viewer.html#/job/diff/<diff>`.
|
||||
4. Put the link in the PR body under **Screenshots / Screen Recordings**.
|
||||
|
||||
Say the numbers plainly: how many screens changed, how many are new, how many are pixel-identical, how many are flaky. The count of unchanged screens is often the most useful claim in the diff, because it is the one a reviewer cannot check by eye.
|
||||
|
||||
## Operational traps
|
||||
|
||||
- A full capture is hundreds of shots. Scope with `--title "Pages/Alerts"` or `--stories "a,b,c"` (substring match on id or Title/Name, repeatable).
|
||||
- **On macOS a capture can pin itself to one story at a time.** sbshot picks how many to shoot at once by sampling free memory, and off Linux it reads `os.freemem()`, which counts only truly free pages. macOS parks almost everything in "inactive" instead, which is reclaimable on demand, so the sampler can read ~100 MB on a 25 GB machine and shrink to one forever. `--jobs` does not help: it is only an upper cap. Check `parallelism` in `<run>/events.ndjson`; if it sits at 1 while `vm_stat` shows gigabytes inactive, that is this. A 124-shot run takes about 90 minutes pinned and about 6 minutes at four at a time, so it is worth spotting before a long capture.
|
||||
- A fresh worktree cannot build Storybook: `i18n-translations-hash.json` is gitignored and generated. Run `node ./i18-generate-hash.cjs` in `<worktree>/frontend`, and symlink `node_modules` from the main checkout.
|
||||
- `pnpm test:storybook` starts its own server on 6006 and hangs silently if something already holds that port. Free it first.
|
||||
- ImageMagick on macOS may have no default font, so `--mode green-parallel` fails with ``unable to read font `' ``. Map Helvetica to a real file in `~/.config/ImageMagick/type.xml`.
|
||||
- `sbshot link <dir> <name>` adds a run from elsewhere into the workspace, but it symlinks. Keep the source directory alive until you have exported.
|
||||
- The export directory is build output. Delete it when done, or gitignore it.
|
||||
- Running the whole Storybook test suite locally fails broadly with `page.goto: Timeout` from resource starvation. Scope it; CI is the real signal.
|
||||
|
||||
## Done means
|
||||
|
||||
- [ ] every screen and state the change touches has a story, written before capturing
|
||||
- [ ] one capture per ref, each from its own freshly started Storybook
|
||||
- [ ] `--theme dark,light` on every capture
|
||||
- [ ] baseline is the merge-base
|
||||
- [ ] noise either measured with a full second capture, or each mover explained by hand
|
||||
- [ ] every non-flaky pair opened and explained, not inferred from its size
|
||||
- [ ] the pairs that matter tagged, with a note
|
||||
- [ ] exported and hosted, and the link in the PR body
|
||||
@@ -5,35 +5,63 @@ description: Explore a SigNoz page, map the endpoints, states and query params i
|
||||
|
||||
# SigNoz page stories
|
||||
|
||||
A page story renders the real page inside the real app shell against msw, and its controls panel can reach every state the page has. The panel is the deliverable, not the story list.
|
||||
A page story renders the real page inside the real app shell against msw, and its
|
||||
controls panel can reach every state the page has. The panel is the deliverable,
|
||||
not the story list.
|
||||
|
||||
`frontend/src/storybook/README.md` is the API surface (providers, parameters, control builders, module mocks, navigation). Read it first; this skill is the process on top of it.
|
||||
`frontend/src/storybook/README.md` is the API surface (providers, parameters,
|
||||
control builders, module mocks, navigation). Read it first; this skill is the
|
||||
process on top of it.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Map the page**: [references/discovery.md](references/discovery.md). Produce the inventory (endpoints, states, params, permissions, caps) before writing code. No inventory, no story.
|
||||
2. **Skeleton first**: story + empty `defineStoryMocks`, then run it. The console names the endpoints step 1 missed.
|
||||
1. **Map the page**: [references/discovery.md](references/discovery.md). Produce
|
||||
the inventory (endpoints, states, params, permissions, caps) before writing
|
||||
code. No inventory, no story.
|
||||
2. **Skeleton first**: story + empty `defineStoryMocks`, then run it. The console
|
||||
names the endpoints step 1 missed.
|
||||
3. **Inventory to controls**: [references/controls.md](references/controls.md).
|
||||
4. **Mock data and handlers**: builders in `__story_mockdata__`, handlers in the page's mocks module.
|
||||
5. **Verify in the browser**: [references/verify.md](references/verify.md). Never report the story as done without it.
|
||||
4. **Mock data and handlers**: builders in `__story_mockdata__`, handlers in the page's
|
||||
mocks module.
|
||||
5. **Verify in the browser**: [references/verify.md](references/verify.md). Never
|
||||
report the story as done without it.
|
||||
|
||||
## Where it lands in the sidebar
|
||||
|
||||
The sidebar mirrors the app's own side nav (`container/SideNav/menuItems.tsx`), so a page sits where someone would click it in the product. Four things decide that, and all four are part of writing the story, not a follow-up.
|
||||
The sidebar mirrors the app's own side nav (`container/SideNav/menuItems.tsx`), so
|
||||
a page sits where someone would click it in the product. Four things decide that,
|
||||
and all four are part of writing the story, not a follow-up.
|
||||
|
||||
**Title.** `Pages/<Area>/<Page>`, where `<Area>` is the nav section and `<Page>` is the label the nav gives it.
|
||||
**Title.** `Pages/<Area>/<Page>`, where `<Area>` is the nav section and `<Page>`
|
||||
is the label the nav gives it.
|
||||
|
||||
- The leaf is the product's label, never the component's name: `MetricsExplorer` is `Metrics/Explorer`, `MeterExplorer` is `Metering/Cost Meter`, `AIAssistantPage` is `Noz`.
|
||||
- The leaf is the product's label, never the component's name: `MetricsExplorer`
|
||||
is `Metrics/Explorer`, `MeterExplorer` is `Metering/Cost Meter`,
|
||||
`AIAssistantPage` is `Noz`.
|
||||
- Never repeat the area in the leaf: `Alerts/Rules`, not `Alerts/AlertRules`.
|
||||
- A leaf never shares its name with a sibling folder. The folder wins and the page becomes `List`, or `Overview` for a tab strip: `Services/List` beside `Services/Detail`.
|
||||
- A leaf never shares its name with a sibling folder. The folder wins and the
|
||||
page becomes `List`, or `Overview` for a tab strip: `Services/List` beside
|
||||
`Services/Detail`.
|
||||
- Title Case with spaces. No camelCase, no kebab.
|
||||
- Four levels is the floor to stay under: `Pages/Alerts/Channels/New` is as deep as it goes.
|
||||
- Pages nobody navigates to on purpose go under `Pages/System` (`Status`, `Unauthorized`, `Workspace Locked`), and the pre-session pages under `Pages/Auth`.
|
||||
- A page whose permission stories earn their own folder becomes one: `Pages/Settings/Billing/Overview` beside `Pages/Settings/Billing/Authz`. See **Permission stories** below.
|
||||
- Four levels is the floor to stay under: `Pages/Alerts/Channels/New` is as deep
|
||||
as it goes.
|
||||
- Pages nobody navigates to on purpose go under `Pages/System` (`Status`,
|
||||
`Unauthorized`, `Workspace Locked`), and the pre-session pages under
|
||||
`Pages/Auth`.
|
||||
- A page whose permission stories earn their own folder becomes one:
|
||||
`Pages/Settings/Billing/Overview` beside `Pages/Settings/Billing/Authz`. See
|
||||
**Permission stories** below.
|
||||
|
||||
**Order.** The `storySort.order` literal in `.storybook/preview.tsx` carries the order for every level. A new page in an existing area is appended to that area's array, in the order the product lists it; a new area goes where the side nav puts it. Storybook parses the order out of the file statically, so it has to stay an inline literal. Missing entries fall to the end of their level rather than disappearing, so a forgotten edit is a page at the bottom of its area, not a broken sidebar.
|
||||
**Order.** The `storySort.order` literal in `.storybook/preview.tsx` carries the
|
||||
order for every level. A new page in an existing area is appended to that area's
|
||||
array, in the order the product lists it; a new area goes where the side nav
|
||||
puts it. Storybook parses the order out of the file statically, so it has to
|
||||
stay an inline literal. Missing entries fall to the end of their level rather
|
||||
than disappearing, so a forgotten edit is a page at the bottom of its area, not
|
||||
a broken sidebar.
|
||||
|
||||
**Tags.** Declared on the meta, right under `title`, and what the sidebar's tag filter answers questions with. Only these:
|
||||
**Tags.** Declared on the meta, right under `title`, and what the sidebar's tag
|
||||
filter answers questions with. Only these:
|
||||
|
||||
| Tag | When |
|
||||
| --- | --- |
|
||||
@@ -45,7 +73,8 @@ The sidebar mirrors the app's own side nav (`container/SideNav/menuItems.tsx`),
|
||||
|
||||
`autodocs` comes from `preview.tsx` and is never written on a meta.
|
||||
|
||||
**Doc comment on the meta.** What the page is, in the page's own terms, then a blank line, then the route:
|
||||
**Doc comment on the meta.** What the page is, in the page's own terms, then a
|
||||
blank line, then the route:
|
||||
|
||||
```tsx
|
||||
const pageStory = storyMocks(logsExplorerMocks, {
|
||||
@@ -68,56 +97,113 @@ const meta = {
|
||||
} satisfies Meta<LogsExplorerArgs>;
|
||||
```
|
||||
|
||||
The `pageStory` const and the trailing `parameters` line are what make the doc comment safe. The comment compiles to a `parameters` property that the csf plugin appends after the spread, so a meta that spreads `storyMocks(...)` and stops there loses `parameters.signoz` and renders the page against the global handlers alone: every one of the page's endpoints misses. Restating `parameters` as a literal gives the plugin something to merge into. `resolveStory` logs the combination that says it happened, so the console names it rather than leaving it to be found by reading the page.
|
||||
The `pageStory` const and the trailing `parameters` line are what make the doc
|
||||
comment safe. The comment compiles to a `parameters` property that the csf plugin
|
||||
appends after the spread, so a meta that spreads `storyMocks(...)` and stops
|
||||
there loses `parameters.signoz` and renders the page against the global handlers
|
||||
alone: every one of the page's endpoints misses. Restating `parameters` as a
|
||||
literal gives the plugin something to merge into. `resolveStory` logs the
|
||||
combination that says it happened, so the console names it rather than leaving it
|
||||
to be found by reading the page.
|
||||
|
||||
It is the description on the page's Docs page, which is the only place a reader who is not in the code finds out what the page is for. Two or three sentences: what it shows, what drives it, and the gating worth knowing about (`Gated on authz permissions`, `follows the legacy editor role`). A control-driven route says so instead of a path: ``Route: `/metrics-explorer/*`, the tab control picks which``.
|
||||
It is the description on the page's Docs page, which is the only place a reader
|
||||
who is not in the code finds out what the page is for. Two or three sentences:
|
||||
what it shows, what drives it, and the gating worth knowing about (`Gated on
|
||||
authz permissions`, `follows the legacy editor role`). A control-driven route
|
||||
says so instead of a path: ``Route: `/metrics-explorer/*`, the tab control picks
|
||||
which``.
|
||||
|
||||
## Permission stories
|
||||
|
||||
A page that gates UI on `lib/authz` keeps its permission states in a folder of their own, so the page's own file stays about the page and the sidebar answers "what does this permission do" in one place.
|
||||
A page that gates UI on `lib/authz` keeps its permission states in a folder of
|
||||
their own, so the page's own file stays about the page and the sidebar answers
|
||||
"what does this permission do" in one place.
|
||||
|
||||
**Layout.** A second story file at `stories/authz/<Page>.authz.stories.tsx`, titled `Pages/<Area>/<Page>/Authz`, which turns the page into a folder: its own file is retitled `Pages/<Area>/<Page>/Overview`, and `.storybook/preview.tsx` gains the sub-order (`'Billing', ['Overview', 'Authz']`). Both files carry the `authz` tag and share the page's one mocks module, which the authz file imports as `../<Page>.stories.mocks`. It declares no controls and no mock data of its own: a permission story that needs a new response is a control the page's mocks were missing.
|
||||
**Layout.** A second story file at `stories/authz/<Page>.authz.stories.tsx`,
|
||||
titled `Pages/<Area>/<Page>/Authz`, which turns the page into a folder: its own
|
||||
file is retitled `Pages/<Area>/<Page>/Overview`, and `.storybook/preview.tsx`
|
||||
gains the sub-order (`'Billing', ['Overview', 'Authz']`). Both files carry the
|
||||
`authz` tag and share the page's one mocks module, which the authz file imports
|
||||
as `../<Page>.stories.mocks`. It declares no controls and no mock data of its
|
||||
own: a permission story that needs a new response is a control the page's mocks
|
||||
were missing.
|
||||
|
||||
**One story per permission the page reads**, named for what is gone: `NoRead`, `NoList`, `NoUpdate`, `NoCreate`, `NoDelete`. Then the combinations the page itself distinguishes, and only those: `NoManage` where two permissions gate one button, `ReadOnly` where everything but reading is denied, `NoSubscriptionAccess` where none of the resource's permissions are held, and `CheckFailed` for `authzState: 'error'`, which is the page's fail-open path rather than a denial.
|
||||
**One story per permission the page reads**, named for what is gone: `NoRead`,
|
||||
`NoList`, `NoUpdate`, `NoCreate`, `NoDelete`. Then the combinations the page
|
||||
itself distinguishes, and only those: `NoManage` where two permissions gate one
|
||||
button, `ReadOnly` where everything but reading is denied, `NoSubscriptionAccess`
|
||||
where none of the resource's permissions are held, and `CheckFailed` for
|
||||
`authzState: 'error'`, which is the page's fail-open path rather than a denial.
|
||||
|
||||
**Revoke, never allow-list.** Each story is a full grant minus what its name says: `args: { revoked: ['read:subscription'] }`. The `Revoked` control subtracts from the preset, so the story stays "an admin missing one permission" as the catalogue grows, and the diff against the page's `Default` is the one permission. Rebuilding the allow-list by hand drifts the moment a resource is added.
|
||||
**Revoke, never allow-list.** Each story is a full grant minus what its name
|
||||
says: `args: { revoked: ['read:subscription'] }`. The `Revoked` control subtracts
|
||||
from the preset, so the story stays "an admin missing one permission" as the
|
||||
catalogue grows, and the diff against the page's `Default` is the one permission.
|
||||
Rebuilding the allow-list by hand drifts the moment a resource is added.
|
||||
|
||||
**Never a role preset in this folder.** `access: 'viewer'` moves the legacy role, the side nav and every other resource's permissions at the same time, so the story no longer shows what its name claims. A persona is a story on the page's own file, and only when the product has that persona.
|
||||
**Never a role preset in this folder.** `access: 'viewer'` moves the legacy role,
|
||||
the side nav and every other resource's permissions at the same time, so the
|
||||
story no longer shows what its name claims. A persona is a story on the page's
|
||||
own file, and only when the product has that persona.
|
||||
|
||||
**Pair the revocation with the state that renders the gated control.** A button that only exists on a trial needs the plan too: `args: { plan: 'on-trial', revoked: ['create:subscription'] }`. A permission whose denial changes nothing on screen gets no story: say so in the PR.
|
||||
**Pair the revocation with the state that renders the gated control.** A button
|
||||
that only exists on a trial needs the plan too:
|
||||
`args: { plan: 'on-trial', revoked: ['create:subscription'] }`. A permission
|
||||
whose denial changes nothing on screen gets no story: say so in the PR.
|
||||
|
||||
Check these by what is turned off, not by what the page says. The page reads the same either way, so a story that is wrong still looks right. Check that the control the permission guards is off, and that the denial message is there or gone.
|
||||
|
||||
**Do not use `toBeEnabled` or `toBeDisabled`.** They only look at the `disabled` attribute. `@signozhq/ui` turns a button off with `aria-disabled` instead, so these matchers pass on a button nobody can click, and the test goes green while clicking straight through a denial. Check the attribute the component really sets:
|
||||
|
||||
```ts
|
||||
expect(save).toHaveAttribute('aria-disabled', 'true');
|
||||
```
|
||||
|
||||
**Some controls are off with no attribute at all.** The browser turns off everything inside a disabled `<fieldset>` without marking it, and a switch is a `<button>` underneath. For those, an attribute check proves nothing, and so does looking for a parent `fieldset[disabled]`. Click it and check nothing happened:
|
||||
|
||||
```ts
|
||||
const before = toggle.getAttribute('aria-checked');
|
||||
await userEvent.click(toggle);
|
||||
await expect(toggle.getAttribute('aria-checked')).toBe(before);
|
||||
```
|
||||
|
||||
**Check that a new assertion can fail.** Flip it once and run it. If that story goes red and nothing else does, the assertion is real; put it back. A test that cannot fail is worse than no test, because it looks like coverage.
|
||||
|
||||
**Delete a story once its difference disappears.** Say a story checks that a button reads "View" rather than "Edit" when a permission is missing. If the label later becomes the same either way, that story passes no matter which permissions are held, including all of them, and it is testing nothing. When a behaviour stops depending on a permission, delete the story or rewrite it to lock in the new behaviour, and say which one you did in the PR.
|
||||
Verify these by their disabled states, not their text. The page reads the same
|
||||
either way, so a story that is wrong looks right: read `disabled` off the buttons
|
||||
the permission gates, and check the denial callout is there or gone.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Default is the loaded page.** `export const Default: Story = {}` with no args, every widget carrying data. Empty, loading and failed are variants or control values, never the default.
|
||||
- **A control is a knob on a response**, resolved through `handlers`, `config` or `effect`. Never a component prop, never a module mock added for one story.
|
||||
- **Every branch in the inventory is reachable from the panel.** A state that needs a code edit to see is a missing control.
|
||||
- **Never re-declare what every story already has**: banner, side nav, data state (loaded/loading/error), access preset, permissions, check state.
|
||||
- **A variant earns a story only when it is worth linking to**: an empty workspace, a viewer, a page mid-load. Everything else stays a control.
|
||||
- **Endpoints the page owns go through `response.json`**, so the Data control covers loaded, loading and failed in one declaration. Endpoints the page cannot render without (ingestion detection, preferences, feature payloads) take a plain resolver so the shell survives the loading and error states.
|
||||
- **Query-param state starts from `route`** (`/logs?tab=explorer`). In-page param navigation works inside a story; a different pathname is blocked and reported by the overlay. A control for a param is worth it only when the param is a page mode someone would want to flip.
|
||||
- **File layout**: every story file for a page lives under `src/pages/<Page>/stories/`: `<Page>.stories.tsx`, `<Page>.stories.mocks.tsx`, payload builders in `stories/__story_mockdata__/<page>.ts`. Nothing page-specific in `src/storybook/controls/`. A page that is a tab strip over several routes gets one story file per tab, in its own folder under the module page (`LogsModulePage/Pipelines/stories/Pipelines.stories.tsx`), each with its own mocks and `__story_mockdata__/`; the builders more than one tab needs stay in the module page's own `stories/__story_mockdata__/` (`AlertList/stories/__story_mockdata__/alerts.ts`), which a tab reaches as `../../stories/__story_mockdata__/alerts`. Every one of them renders the module page, so the tab strip is there, and the `route` its mocks return decides which tab is open. A page's permission stories go one level further down, in `stories/authz/<Page>.authz.stories.tsx`, on the page's own mocks: see **Permission stories**.
|
||||
- **A state only a click reaches is a story with a `play` function**, not a control: a drawer, a modal, an edit mode the page holds in component state. Drive it with `userEvent` and the queries from `storybook/test`, take the first of a repeated row action, and wait on the state's own text. The page fetches before it renders a row, so the finder needs a timeout past the 1s default. A state the app drops again on its own, such as one keyed on an array identity that a refetch replaces, does not get a story: it would not survive being looked at. A _sequence_ of such states, a wizard's steps or a questionnaire's pages, is still a control: declare the steps in the mocks module and walk them from a `play` on the meta that destructures `mount`, which is what makes Storybook replay it on an arg change. See [references/controls.md](references/controls.md).
|
||||
- **The mocks are AI-owned and say so.** `<Page>.stories.mocks.tsx` and every file under a `__story_mockdata__/` open with this banner, above the imports:
|
||||
- **Default is the loaded page.** `export const Default: Story = {}` with no args,
|
||||
every widget carrying data. Empty, loading and failed are variants or control
|
||||
values, never the default.
|
||||
- **A control is a knob on a response**, resolved through `handlers`, `config` or
|
||||
`effect`. Never a component prop, never a module mock added for one story.
|
||||
- **Every branch in the inventory is reachable from the panel.** A state that
|
||||
needs a code edit to see is a missing control.
|
||||
- **Never re-declare what every story already has**: banner, side nav, data state
|
||||
(loaded/loading/error), access preset, permissions, check state.
|
||||
- **A variant earns a story only when it is worth linking to**: an empty
|
||||
workspace, a viewer, a page mid-load. Everything else stays a control.
|
||||
- **Endpoints the page owns go through `response.json`**, so the Data control
|
||||
covers loaded, loading and failed in one declaration. Endpoints the page cannot
|
||||
render without (ingestion detection, preferences, feature payloads) take a
|
||||
plain resolver so the shell survives the loading and error states.
|
||||
- **Query-param state starts from `route`** (`/logs?tab=explorer`). In-page param
|
||||
navigation works inside a story; a different pathname is blocked and reported
|
||||
by the overlay. A control for a param is worth it only when the param is a page
|
||||
mode someone would want to flip.
|
||||
- **File layout**: every story file for a page lives under
|
||||
`src/pages/<Page>/stories/`: `<Page>.stories.tsx`, `<Page>.stories.mocks.tsx`,
|
||||
payload builders in `stories/__story_mockdata__/<page>.ts`. Nothing
|
||||
page-specific in `src/storybook/controls/`. A page that is a tab strip over
|
||||
several routes gets one story file per tab, in its own folder under the module
|
||||
page (`LogsModulePage/Pipelines/stories/Pipelines.stories.tsx`), each with its
|
||||
own mocks and `__story_mockdata__/`; the builders more than one tab needs stay
|
||||
in the module page's own `stories/__story_mockdata__/`
|
||||
(`AlertList/stories/__story_mockdata__/alerts.ts`), which a tab reaches as
|
||||
`../../stories/__story_mockdata__/alerts`. Every one of them renders the module page, so the tab
|
||||
strip is there, and the `route` its mocks return decides which tab is open.
|
||||
A page's permission stories go one level further down, in
|
||||
`stories/authz/<Page>.authz.stories.tsx`, on the page's own mocks: see
|
||||
**Permission stories**.
|
||||
- **A state only a click reaches is a story with a `play` function**, not a
|
||||
control: a drawer, a modal, an edit mode the page holds in component state.
|
||||
Drive it with `userEvent` and the queries from `storybook/test`, take the first
|
||||
of a repeated row action, and wait on the state's own text. The page fetches
|
||||
before it renders a row, so the finder needs a timeout past the 1s default. A
|
||||
state the app drops again on its own, such as one keyed on an array identity
|
||||
that a refetch replaces, does not get a story: it would not survive being
|
||||
looked at. A *sequence* of such states, a wizard's steps or a
|
||||
questionnaire's pages, is still a control: declare the steps in the mocks
|
||||
module and walk them from a `play` on the meta that destructures `mount`, which
|
||||
is what makes Storybook replay it on an arg change. See
|
||||
[references/controls.md](references/controls.md).
|
||||
- **The mocks are AI-owned and say so.** `<Page>.stories.mocks.tsx` and every file
|
||||
under a `__story_mockdata__/` open with this banner, above the imports:
|
||||
|
||||
```ts
|
||||
/**
|
||||
@@ -126,23 +212,52 @@ await expect(toggle.getAttribute('aria-checked')).toBe(before);
|
||||
*/
|
||||
```
|
||||
|
||||
The root `.gitattributes` marks both paths `linguist-generated=true`, so the reviewer gets them collapsed and spends the attention on the rendered page. The story file is the human surface and never carries the banner. A file with the banner has to stay regenerable from the page alone: no page knowledge in `src/storybook/`, and builders typed from `src/api/generated` where the endpoint has types, so a contract change is a compile error instead of a mock that lies.
|
||||
|
||||
- **Reuse fixtures** from `src/mocks-server/` and `src/tests/fixtures/` where they exist. An endpoint jest needs too belongs in `src/mocks-server/handlers.ts`.
|
||||
- **Shared response builders live in `src/storybook/msw/__story_mockdata__/`**: typed helpers like `queryRangeV5ScalarResponse` that multiple pages need. Before writing a response shape inline, check if a builder exists; if not and the shape will repeat, add it there. Page-specific builders stay in the page's `__story_mockdata__/`.
|
||||
- **The story's own doc comment is per state.** Every `export const` gets one: what that state shows, not how it is built. It renders in the States list on the page's Docs page, so `Undocumented.` there is a story nobody described.
|
||||
- **Story names come from a fixed vocabulary** where one fits: `Default`, `Viewer`, `Empty`, `Loading`, `Error`. Page-specific states get page-specific names (`NoIngestion`, `Unlicensed`), never a second spelling of one of those (`ViewerAccess`, `NonAdmin`).
|
||||
- **No comment is the default.** Write one only for what the code cannot show: a shape the backend dictates, an app bug the mock reproduces, an ordering or cap the page depends on, a workaround and the reason for it. Never restate a name, a type, or what a builder plainly builds; if the sentence reads as the signature in prose, delete it. Nothing addressed to a reviewer. The one comment a story always gets is its own doc comment: what it shows, in the page's own terms.
|
||||
The root `.gitattributes` marks both paths `linguist-generated=true`, so the
|
||||
reviewer gets them collapsed and spends the attention on the rendered page. The
|
||||
story file is the human surface and never carries the banner. A file with the
|
||||
banner has to stay regenerable from the page alone: no page knowledge in
|
||||
`src/storybook/`, and builders typed from `src/api/generated` where the endpoint
|
||||
has types, so a contract change is a compile error instead of a mock that lies.
|
||||
- **Reuse fixtures** from `src/mocks-server/` and `src/tests/fixtures/` where they
|
||||
exist. An endpoint jest needs too belongs in `src/mocks-server/handlers.ts`.
|
||||
- **Shared response builders live in `src/storybook/msw/__story_mockdata__/`**: typed
|
||||
helpers like `queryRangeV5ScalarResponse` that multiple pages need. Before
|
||||
writing a response shape inline, check if a builder exists; if not and the
|
||||
shape will repeat, add it there. Page-specific builders stay in the page's
|
||||
`__story_mockdata__/`.
|
||||
- **The story's own doc comment is per state.** Every `export const` gets one:
|
||||
what that state shows, not how it is built. It renders in the States list on
|
||||
the page's Docs page, so `Undocumented.` there is a story nobody described.
|
||||
- **Story names come from a fixed vocabulary** where one fits: `Default`,
|
||||
`Viewer`, `Empty`, `Loading`, `Error`. Page-specific states get page-specific
|
||||
names (`NoIngestion`, `Unlicensed`), never a second spelling of one of those
|
||||
(`ViewerAccess`, `NonAdmin`).
|
||||
- **No comment is the default.** Write one only for what the code cannot show:
|
||||
a shape the backend dictates, an app bug the mock reproduces, an ordering or
|
||||
cap the page depends on, a workaround and the reason for it. Never restate a
|
||||
name, a type, or what a builder plainly builds; if the sentence reads as the
|
||||
signature in prose, delete it. Nothing addressed to a reviewer. The one
|
||||
comment a story always gets is its own doc comment: what it shows, in the
|
||||
page's own terms.
|
||||
|
||||
## Done means
|
||||
|
||||
- [ ] `Default` shows the page with data, checked in dark and light
|
||||
- [ ] title follows the sidebar rules, tags declared, and the page's entry added to the `storySort.order` literal in `.storybook/preview.tsx`
|
||||
- [ ] the meta carries its doc comment with the `Route:` line, the meta restates `parameters: { ...pageStory.parameters }` after the spread, and every story export carries its own doc comment
|
||||
- [ ] the page's Docs page renders: description, controls table, and one row per state with no `Undocumented.`
|
||||
- [ ] a page tagged `authz` has its `Authz` folder: one story per permission it reads, each reached by `revoked`, none of them a role preset, and each one checked by the `disabled` state of what the permission gates
|
||||
- [ ] title follows the sidebar rules, tags declared, and the page's entry added
|
||||
to the `storySort.order` literal in `.storybook/preview.tsx`
|
||||
- [ ] the meta carries its doc comment with the `Route:` line, the meta restates
|
||||
`parameters: { ...pageStory.parameters }` after the spread, and every story
|
||||
export carries its own doc comment
|
||||
- [ ] the page's Docs page renders: description, controls table, and one row per
|
||||
state with no `Undocumented.`
|
||||
- [ ] a page tagged `authz` has its `Authz` folder: one story per permission it
|
||||
reads, each reached by `revoked`, none of them a role preset, and each one
|
||||
checked by the `disabled` state of what the permission gates
|
||||
- [ ] the mocks module and every `__story_mockdata__` file carry the AI-owned banner
|
||||
- [ ] every control flipped once, its effect seen on screen
|
||||
- [ ] console clean: no `[storybook] no msw handler`, no 501, no msw unhandled request, no React warning
|
||||
- [ ] console clean: no `[storybook] no msw handler`, no 501, no msw unhandled
|
||||
request, no React warning
|
||||
- [ ] no navigation overlay on mount
|
||||
- [ ] `pnpm tsgo --noEmit`, `pnpm exec oxlint <files>`, `pnpm exec oxfmt --check <files>` all clean. The repo has no prettier: `pnpm exec prettier` prints a pass while exiting 254
|
||||
- [ ] `pnpm tsgo --noEmit`, `pnpm exec oxlint <files>`,
|
||||
`pnpm exec oxfmt --check <files>` all clean. The repo has no
|
||||
prettier: `pnpm exec prettier` prints a pass while exiting 254
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Turning the inventory into controls
|
||||
|
||||
Every row of the inventory becomes a control, a global control that already exists, or a documented reason it cannot be one.
|
||||
Every row of the inventory becomes a control, a global control that already
|
||||
exists, or a documented reason it cannot be one.
|
||||
|
||||
## Imports
|
||||
|
||||
Paths written as `src/storybook/...` in prose are repo paths, not import specifiers. Stories import through the `@/` alias (`@/*` → `./src/*`); modules inside `src/storybook/` import each other relatively.
|
||||
Paths written as `src/storybook/...` in prose are repo paths, not import
|
||||
specifiers. Stories import through the `@/` alias (`@/*` → `./src/*`); modules
|
||||
inside `src/storybook/` import each other relatively.
|
||||
|
||||
| Import | From |
|
||||
| --- | --- |
|
||||
@@ -28,21 +31,38 @@ Paths written as `src/storybook/...` in prose are repo paths, not import specifi
|
||||
|
||||
Rules that come with them:
|
||||
|
||||
- `countControl` `max` goes past what the page renders, so a story can show the cap being hit. `0` is the empty state, which is why an empty list rarely needs its own story. When the cap is in the _request_ (`?limit=5`) rather than the renderer, stop `max` at the limit: a longer response is a body the backend cannot send.
|
||||
- `choiceControl` options come from a `const` array typed with `(typeof X)[number]`, not from string literals scattered in the handlers.
|
||||
- Defaults describe the fully-populated page. The panel starts where `Default` starts.
|
||||
- `group` is `'<Page> · <facet>'`, such as `'Services · lists'` or `'Alerts · rules'`. Keep a page's knobs in two or three groups, not one per control.
|
||||
- `description` only when the name does not carry the effect (what dismissing does, what the cap is, which widget it feeds).
|
||||
- `countControl` `max` goes past what the page renders, so a story can show the
|
||||
cap being hit. `0` is the empty state, which is why an empty list rarely needs
|
||||
its own story. When the cap is in the *request* (`?limit=5`) rather than the
|
||||
renderer, stop `max` at the limit: a longer response is a body the backend
|
||||
cannot send.
|
||||
- `choiceControl` options come from a `const` array typed with
|
||||
`(typeof X)[number]`, not from string literals scattered in the handlers.
|
||||
- Defaults describe the fully-populated page. The panel starts where `Default`
|
||||
starts.
|
||||
- `group` is `'<Page> · <facet>'`, such as `'Services · lists'` or
|
||||
`'Alerts · rules'`. Keep a page's knobs in two or three groups, not one per
|
||||
control.
|
||||
- `description` only when the name does not carry the effect (what dismissing
|
||||
does, what the cap is, which widget it feeds).
|
||||
|
||||
## Which hook
|
||||
|
||||
`defineStoryMocks` takes three, all optional:
|
||||
|
||||
- `handlers(values, response)`: the page's endpoints. Everything the page owns goes through `response.json`, so the global Data control turns the whole page into loading or failed without a second declaration. An endpoint the page cannot render at all without (ingestion detection, preferences, license payloads) takes a plain `rest.get(...)` resolver instead, so the shell stays visible while the rest hangs or fails.
|
||||
- `config(values)`: `SignozStoryConfig` for knobs no endpoint covers: `route`, `appContext`, `reduxState`, `queryBuilder`, `theme`.
|
||||
- `handlers(values, response)`: the page's endpoints. Everything the page owns
|
||||
goes through `response.json`, so the global Data control turns the whole page
|
||||
into loading or failed without a second declaration. An endpoint the page
|
||||
cannot render at all without (ingestion detection, preferences, license
|
||||
payloads) takes a plain `rest.get(...)` resolver instead, so the shell stays
|
||||
visible while the rest hangs or fails.
|
||||
- `config(values)`: `SignozStoryConfig` for knobs no endpoint covers: `route`,
|
||||
`appContext`, `reduxState`, `queryBuilder`, `theme`.
|
||||
- `effect(values)`: module-level state no provider exposes.
|
||||
|
||||
One endpoint feeding several widgets stays one handler that reads the request. `response.json` hands the request to the builder and awaits it, so reading a query param, or a POST body, does not cost the Data control:
|
||||
One endpoint feeding several widgets stays one handler that reads the request.
|
||||
`response.json` hands the request to the builder and awaits it, so reading a
|
||||
query param, or a POST body, does not cost the Data control:
|
||||
|
||||
```ts
|
||||
rest.get(
|
||||
@@ -65,11 +85,18 @@ rest.post(
|
||||
),
|
||||
```
|
||||
|
||||
Reach for a plain `rest.post(url, async (req, res, ctx) => …)` only when the endpoint has to keep answering while the Data control is on `loading` or `error`: detection calls the page cannot render without.
|
||||
Reach for a plain `rest.post(url, async (req, res, ctx) => …)` only when the
|
||||
endpoint has to keep answering while the Data control is on `loading` or
|
||||
`error`: detection calls the page cannot render without.
|
||||
|
||||
## Mutations
|
||||
|
||||
A control drives the response, so a write the page makes against state a control owns does not stick: the refetch answers with the control's value and the button appears to do nothing. Two honest options: leave it declarative and say so in the PR, or move the state into `effect` so the handler can read what the page wrote. Never fake the write by mutating a builder's module state without saying where the state lives.
|
||||
A control drives the response, so a write the page makes against state a control
|
||||
owns does not stick: the refetch answers with the control's value and the button
|
||||
appears to do nothing. Two honest options: leave it declarative and say so in
|
||||
the PR, or move the state into `effect` so the handler can read what the page
|
||||
wrote. Never fake the write by mutating a builder's module state without saying
|
||||
where the state lives.
|
||||
|
||||
## Wiring it up
|
||||
|
||||
@@ -119,11 +146,15 @@ const meta = {
|
||||
} satisfies Meta<ServicesArgs>;
|
||||
```
|
||||
|
||||
`PageStoryArgs` folds in the global controls, so a story's `args` can set `access`, `dataState` or `banner` next to the page's own knobs and stay typed.
|
||||
`PageStoryArgs` folds in the global controls, so a story's `args` can set
|
||||
`access`, `dataState` or `banner` next to the page's own knobs and stay typed.
|
||||
|
||||
## A step the page keeps in component state
|
||||
|
||||
A wizard's step, a questionnaire's page, a picker's next question: the page holds it in `useState` and nothing in the URL says which one is open. It is still a control. Declare the steps in the mocks module and drive them from a `play` on the **meta**, so every story of the page inherits the walk and only sets `args`:
|
||||
A wizard's step, a questionnaire's page, a picker's next question: the page holds
|
||||
it in `useState` and nothing in the URL says which one is open. It is still a
|
||||
control. Declare the steps in the mocks module and drive them from a `play` on
|
||||
the **meta**, so every story of the page inherits the walk and only sets `args`:
|
||||
|
||||
```tsx
|
||||
// <Page>.stories.mocks.tsx
|
||||
@@ -148,23 +179,39 @@ const meta = {
|
||||
export const Configure: Story = { args: { step: 'configure' } };
|
||||
```
|
||||
|
||||
**Destructuring `mount` is what makes it a control.** Storybook re-runs a play function on an arg change only for a story whose play asks to be remounted (`usesMount`); otherwise it re-renders the tree the previous walk left behind and the panel looks broken. With `mount` destructured, the story renders when `play` calls it, and every arg change replays the walk from a fresh mount.
|
||||
**Destructuring `mount` is what makes it a control.** Storybook re-runs a play
|
||||
function on an arg change only for a story whose play asks to be remounted
|
||||
(`usesMount`); otherwise it re-renders the tree the previous walk left behind and
|
||||
the panel looks broken. With `mount` destructured, the story renders when `play`
|
||||
calls it, and every arg change replays the walk from a fresh mount.
|
||||
|
||||
The walk itself:
|
||||
|
||||
- one `answer` function per step, in an array indexed the same as the step list, so reaching step _n_ is `answers.slice(0, STEPS.indexOf(step))`;
|
||||
- answer each step with the least its Next button accepts, and prefer a "do this later" over filling a slider;
|
||||
- run them sequentially (`reduce` over a promise), since each answer is what renders the step the next one reads;
|
||||
- bail out when the page did not start where the walk expects, such as a source deep-linked past the questions. Check for the first step's own text rather than reading another control's value.
|
||||
- one `answer` function per step, in an array indexed the same as the step list,
|
||||
so reaching step *n* is `answers.slice(0, STEPS.indexOf(step))`;
|
||||
- answer each step with the least its Next button accepts, and prefer a "do this
|
||||
later" over filling a slider;
|
||||
- run them sequentially (`reduce` over a promise), since each answer is what
|
||||
renders the step the next one reads;
|
||||
- bail out when the page did not start where the walk expects, such as a source
|
||||
deep-linked past the questions. Check for the first step's own text rather than
|
||||
reading another control's value.
|
||||
|
||||
An endpoint that only settles the transition between two steps (the profile a questionnaire saves before its last page) takes a plain resolver, or the Data control on `loading` strands the walk halfway.
|
||||
An endpoint that only settles the transition between two steps (the profile a
|
||||
questionnaire saves before its last page) takes a plain resolver, or the Data
|
||||
control on `loading` strands the walk halfway.
|
||||
|
||||
## Not a control
|
||||
|
||||
- Anything the global controls already cover: banner, side nav, data state, access preset, granted permissions, revoked permissions, check state.
|
||||
- A knob whose effect nobody can see on the page. Delete it or find the widget it was supposed to drive.
|
||||
- A raw payload as an object control. Controls carry intent (`5 dashboards`, `viewer`), and the builder turns intent into the payload.
|
||||
- Anything that needs a module mock or a component prop to work. If the state cannot be produced from a response, config or module state, say so in the PR instead of faking it.
|
||||
- Anything the global controls already cover: banner, side nav, data state,
|
||||
access preset, granted permissions, revoked permissions, check state.
|
||||
- A knob whose effect nobody can see on the page. Delete it or find the widget it
|
||||
was supposed to drive.
|
||||
- A raw payload as an object control. Controls carry intent (`5 dashboards`,
|
||||
`viewer`), and the builder turns intent into the payload.
|
||||
- Anything that needs a module mock or a component prop to work. If the state
|
||||
cannot be produced from a response, config or module state, say so in the PR
|
||||
instead of faking it.
|
||||
|
||||
## Control or story
|
||||
|
||||
@@ -173,8 +220,12 @@ Default to a control. Write a story when the state is worth a link:
|
||||
- the fresh workspace, because that is what a new user sees
|
||||
- a page-defining mode (a tab, a category) that has its own layout
|
||||
|
||||
A permission that visibly changes the page is a story too, but it goes in the page's `Authz` folder, one per permission, turned with the `Revoked` control. See **Permission stories** in SKILL.md.
|
||||
A permission that visibly changes the page is a story too, but it goes in the
|
||||
page's `Authz` folder, one per permission, turned with the `Revoked` control.
|
||||
See **Permission stories** in SKILL.md.
|
||||
|
||||
Combinations of controls do not need stories, which is what the panel is for.
|
||||
|
||||
Each story gets one prose doc comment: what it shows, in the page's own terms. Everywhere else the comment rule in SKILL.md applies: write one only for what the code cannot show.
|
||||
Each story gets one prose doc comment: what it shows, in the page's own terms.
|
||||
Everywhere else the comment rule in SKILL.md applies: write one only for what
|
||||
the code cannot show.
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# Mapping a page
|
||||
|
||||
Two passes: read the code, then let the running story correct you. Write the inventory down: it is what the controls are derived from, and the only protection against a story that renders one state and calls it a page.
|
||||
Two passes: read the code, then let the running story correct you. Write the
|
||||
inventory down: it is what the controls are derived from, and the only
|
||||
protection against a story that renders one state and calls it a page.
|
||||
|
||||
## Pass 1: read the page
|
||||
|
||||
Start at `src/pages/<Page>/` and follow it outward: the containers it mounts (`src/container/<Feature>/`), the hooks those use, the components with their own fetches. Stop at leaf components that take props only.
|
||||
Start at `src/pages/<Page>/` and follow it outward: the containers it mounts
|
||||
(`src/container/<Feature>/`), the hooks those use, the components with their own
|
||||
fetches. Stop at leaf components that take props only.
|
||||
|
||||
Grep recipes, run against the page's directories:
|
||||
|
||||
@@ -20,32 +24,41 @@ Grep recipes, run against the page's directories:
|
||||
| empty and error branches | `isLoading\|isError\|isFetching\|length === 0\|!data` |
|
||||
| render caps | `slice(0,\|PAGE_SIZE\|pageSize\|limit` |
|
||||
|
||||
`src/constants/routes.ts` has the route, `src/constants/query.ts` the param names, `src/lib/authz/README.md` how a permission check resolves.
|
||||
`src/constants/routes.ts` has the route, `src/constants/query.ts` the param names,
|
||||
`src/lib/authz/README.md` how a permission check resolves.
|
||||
|
||||
## The inventory
|
||||
|
||||
One table, in the story's PR or scratch notes:
|
||||
|
||||
| Endpoint | Feeds | States it can be in |
|
||||
| --------------- | ---------------- | ---------------------- |
|
||||
| Endpoint | Feeds | States it can be in |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/v1/x` | the header count | populated, zero, error |
|
||||
|
||||
Plus four short lists:
|
||||
|
||||
- **Query params** the page reads, and what each one switches.
|
||||
- **Permission checks** the page makes, and what disappears when each is denied.
|
||||
- **Preferences and flags** that change layout (dismissed banners, onboarding checklists, opt-in views).
|
||||
- **Preferences and flags** that change layout (dismissed banners, onboarding
|
||||
checklists, opt-in views).
|
||||
- **Caps**: how many rows each list renders before it truncates or paginates.
|
||||
|
||||
A state that appears in this inventory and not in the controls panel is a bug in the story.
|
||||
A state that appears in this inventory and not in the controls panel is a bug in
|
||||
the story.
|
||||
|
||||
## Pass 2: let it run
|
||||
|
||||
Write the story and an empty `defineStoryMocks({ controls: {} })`, point it at the route with `layout: 'app'`, then open it (see verify.md). The console is the oracle:
|
||||
Write the story and an empty `defineStoryMocks({ controls: {} })`, point it at the
|
||||
route with `layout: 'app'`, then open it (see verify.md). The console is the
|
||||
oracle:
|
||||
|
||||
- `[storybook] no msw handler` or a 501 from the catch-all: an endpoint pass 1 missed. Add it to the inventory.
|
||||
- an msw unhandled-request warning: a request going to an origin the handlers do not answer on. handlers are declared against `http://localhost`.
|
||||
- a spinner that never resolves with the Data control on `loaded`: a handler whose URL does not match what the page calls.
|
||||
- the navigation overlay on mount: the page redirects, usually because `route` is wrong or a guard is failing on a permission the controls have not granted.
|
||||
- `[storybook] no msw handler` or a 501 from the catch-all: an endpoint pass 1
|
||||
missed. Add it to the inventory.
|
||||
- an msw unhandled-request warning: a request going to an origin the handlers do
|
||||
not answer on. handlers are declared against `http://localhost`.
|
||||
- a spinner that never resolves with the Data control on `loaded`: a handler
|
||||
whose URL does not match what the page calls.
|
||||
- the navigation overlay on mount: the page redirects, usually because `route`
|
||||
is wrong or a guard is failing on a permission the controls have not granted.
|
||||
|
||||
Repeat until the console is silent. Only then start declaring controls.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Verifying a page story
|
||||
|
||||
A story is not done because it compiles. It is done when each control has been seen changing the page and the console is silent.
|
||||
A story is not done because it compiles. It is done when each control has been
|
||||
seen changing the page and the console is silent.
|
||||
|
||||
## Run it
|
||||
|
||||
@@ -8,9 +9,12 @@ A story is not done because it compiles. It is done when each control has been s
|
||||
cd frontend && pnpm storybook --ci --quiet # :6006, background it
|
||||
```
|
||||
|
||||
A newly added `.stories.tsx` takes a few seconds to appear in `index.json` on an already-running server; an empty first poll is not a broken `stories` glob.
|
||||
A newly added `.stories.tsx` takes a few seconds to appear in `index.json` on an
|
||||
already-running server; an empty first poll is not a broken `stories` glob.
|
||||
|
||||
Story ids come from the meta title: `Pages/Services/List` → `pages-services-list`, plus the story export in kebab-case. Render one story on its own:
|
||||
Story ids come from the meta title: `Pages/Services/List` →
|
||||
`pages-services-list`, plus the story export in kebab-case. Render one story on
|
||||
its own:
|
||||
|
||||
```
|
||||
http://localhost:6006/iframe.html?id=pages-services-list--default&viewMode=story
|
||||
@@ -18,7 +22,9 @@ http://localhost:6006/iframe.html?id=pages-services-list--default&viewMode=story
|
||||
|
||||
## Flip controls from the URL
|
||||
|
||||
Args are settable in the iframe URL, so a whole sweep runs headless without touching the panel. Booleans go as `!true` / `!false`, numbers bare, arrays indexed, several separated by `;`, and the theme through `globals`:
|
||||
Args are settable in the iframe URL, so a whole sweep runs headless without
|
||||
touching the panel. Booleans go as `!true` / `!false`, numbers bare, arrays
|
||||
indexed, several separated by `;`, and the theme through `globals`:
|
||||
|
||||
```
|
||||
&args=services:0;apdex:poor;access:viewer;dataState:loading
|
||||
@@ -26,11 +32,13 @@ Args are settable in the iframe URL, so a whole sweep runs headless without touc
|
||||
&globals=theme:light
|
||||
```
|
||||
|
||||
That is the cheap way to check a control does something: load with and without it, diff the page text.
|
||||
That is the cheap way to check a control does something: load with and without
|
||||
it, diff the page text.
|
||||
|
||||
## Drive it
|
||||
|
||||
Playwright lives in the repo's e2e workspace, so a scratch script can use it directly:
|
||||
Playwright lives in the repo's e2e workspace, so a scratch script can use it
|
||||
directly:
|
||||
|
||||
```js
|
||||
import pw from '<repo>/tests/e2e/node_modules/playwright/index.js';
|
||||
@@ -50,18 +58,36 @@ console.log((await page.locator('body').innerText()).slice(0, 1500), problems);
|
||||
await browser.close();
|
||||
```
|
||||
|
||||
Screenshots are worth taking for `Default` in both themes: text extraction does not catch an unstyled page. Take them with [sbshot](https://github.com/SigNoz/sbshot), e.g. `sbshot capture http://localhost:6006 --title <story title> --theme dark,light`.
|
||||
|
||||
That is one run, for checking the story you just wrote. Comparing two branches is a different job: see the `sbshot-visual-diff` skill, which covers the baseline to pick, measuring the noise and exporting something a reviewer can open.
|
||||
Screenshots are worth taking for `Default` in both themes: text extraction does
|
||||
not catch an unstyled page. Take them with
|
||||
[sbshot](https://github.com/SigNoz/sbshot), e.g.
|
||||
`sbshot capture http://localhost:6006 --title <story title> --theme dark,light`.
|
||||
|
||||
## Gates
|
||||
|
||||
- **Console silent.** `[storybook] no msw handler`, a 501 from the catch-all, an msw unhandled-request warning, a React key or state warning: assume the story is wrong first. A warning that survives is sometimes the app's. Prove it by turning off the control that renders the widget and watching the warning go with it, and by finding the same component elsewhere doing it right. Then report the app bug in the PR. Never invent a field the API does not return to silence a warning.
|
||||
- **No navigation overlay on mount.** "Navigation blocked in Storybook" on load means the page is trying to leave: wrong `route`, or a guard denying on a permission the controls did not grant.
|
||||
- **Every control moves something.** Sweep them one at a time from the URL and diff the page text. A control with no diff is either wired to nothing or aimed at a widget that is not rendering. Some only show their effect after an interaction, such as a tab that has to be clicked or a select that has to be opened. Drive that interaction rather than calling the control unobservable.
|
||||
- **Both themes render styled.** An unstyled page means the story is not inside the provider decorator, or `<body data-theme>` was lost.
|
||||
- **Roles agree.** `<body data-signoz-story-role>` and `<body data-signoz-context-role>` disagreeing means the page reads a different `AppContext` than the story config fills.
|
||||
- **The page's own navigation works.** Tabs, filters and pagination that write query params should re-render the page in place; only leaving the page belongs in the overlay.
|
||||
- **Console silent.** `[storybook] no msw handler`, a 501 from the catch-all, an
|
||||
msw unhandled-request warning, a React key or state warning: assume the story
|
||||
is wrong first. A warning that survives is sometimes the app's. Prove it by
|
||||
turning off the control that renders the widget and watching the warning go
|
||||
with it, and by finding the same component elsewhere doing it right. Then
|
||||
report the app bug in the PR. Never invent a field the API does not return to
|
||||
silence a warning.
|
||||
- **No navigation overlay on mount.** "Navigation blocked in Storybook" on load
|
||||
means the page is trying to leave: wrong `route`, or a guard denying on a
|
||||
permission the controls did not grant.
|
||||
- **Every control moves something.** Sweep them one at a time from the URL and
|
||||
diff the page text. A control with no diff is either wired to nothing or aimed
|
||||
at a widget that is not rendering. Some only show their effect after an
|
||||
interaction, such as a tab that has to be clicked or a select that has to be
|
||||
opened. Drive that interaction rather than calling the control unobservable.
|
||||
- **Both themes render styled.** An unstyled page means the story is not inside
|
||||
the provider decorator, or `<body data-theme>` was lost.
|
||||
- **Roles agree.** `<body data-signoz-story-role>` and
|
||||
`<body data-signoz-context-role>` disagreeing means the page reads a different
|
||||
`AppContext` than the story config fills.
|
||||
- **The page's own navigation works.** Tabs, filters and pagination that write
|
||||
query params should re-render the page in place; only leaving the page belongs
|
||||
in the overlay.
|
||||
|
||||
## Then the usual
|
||||
|
||||
@@ -71,7 +97,9 @@ pnpm exec oxlint <changed files>
|
||||
pnpm exec oxfmt --check <changed files>
|
||||
```
|
||||
|
||||
There is no prettier in this repo. `pnpm exec prettier --check` fetches something else, prints `Prettier: All files formatted correctly` and exits 254: a pass that is not one.
|
||||
There is no prettier in this repo. `pnpm exec prettier --check` fetches something
|
||||
else, prints `Prettier: All files formatted correctly` and exits 254: a pass that is
|
||||
not one.
|
||||
|
||||
## Common failures
|
||||
|
||||
|
||||
10
.github/CODEOWNERS
vendored
@@ -213,7 +213,9 @@ go.mod @therealpandey
|
||||
## Notification Channels
|
||||
/frontend/src/pages/ChannelsEdit/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/ChannelsNew/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/AllAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/CreateAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/EditAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/FormAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/hooks/notificationChannels/ @SigNoz/pulse-frontend
|
||||
|
||||
@@ -290,14 +292,6 @@ go.mod @therealpandey
|
||||
/frontend/src/components/MessagingQueueHealthCheck/ @SigNoz/events-frontend
|
||||
/frontend/src/hooks/messagingQueue/ @SigNoz/events-frontend
|
||||
|
||||
## Saved Views
|
||||
/frontend/src/container/SavedViews/ @aks07
|
||||
/frontend/src/container/Home/SavedViews/ @aks07
|
||||
/frontend/src/hooks/saveViews/ @aks07
|
||||
|
||||
## Bottom Strip
|
||||
/frontend/src/container/BottomStrip/ @aks07
|
||||
|
||||
## Storybook
|
||||
/frontend/.storybook/ @H4ad
|
||||
/frontend/src/storybook/ @H4ad
|
||||
|
||||
43
.github/workflows/e2eci.yaml
vendored
@@ -2,13 +2,25 @@ name: e2eci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- labeled
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
merge_group:
|
||||
types:
|
||||
- checks_requested
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
group: e2eci-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
if: github.event_name != 'merge_group'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
e2e: ${{ steps.changes.outputs.e2e }}
|
||||
@@ -18,21 +30,17 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
FORCE: ${{ contains(github.event.pull_request.labels.*.name, 'safe-to-e2e') }}
|
||||
LABELED: ${{ contains(github.event.pull_request.labels.*.name, 'safe-to-e2e') }}
|
||||
run: |
|
||||
CHANGED_FILES=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename | select(startswith("tests/integration/") | not)')
|
||||
PATTERN='^(frontend/|tests/|\.github/workflows/e2eci\.yaml$|cmd/enterprise/Dockerfile\.with-web\.integration$)'
|
||||
|
||||
if [ "$FORCE" = "true" ]; then
|
||||
files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename')
|
||||
if [ "$LABELED" = true ] || grep -qE '^(frontend|tests/e2e)/' <<< "$files"; then
|
||||
echo "e2e=true" >> "$GITHUB_OUTPUT"
|
||||
elif printf '%s\n' "$CHANGED_FILES" | grep -qE "$PATTERN"; then
|
||||
echo "e2e=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "e2e=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fmtlint:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.e2e == 'true'
|
||||
if: |
|
||||
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && needs.changes.outputs.e2e == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
@@ -62,7 +70,9 @@ jobs:
|
||||
- chromium
|
||||
shard: [1, 2, 3, 4]
|
||||
needs: changes
|
||||
if: needs.changes.outputs.e2e == 'true'
|
||||
if: |
|
||||
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && needs.changes.outputs.e2e == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
@@ -171,14 +181,17 @@ jobs:
|
||||
name: playwright-artifacts-${{ matrix.project }}-${{ matrix.shard }}
|
||||
path: tests/e2e/artifacts/
|
||||
retention-days: 5
|
||||
# Required status check. Fails when e2e was needed but did not pass.
|
||||
# Required status check. Fails when e2e was needed but did not pass,
|
||||
# including fork and dependabot PRs still waiting on safe-to-test.
|
||||
e2e:
|
||||
if: always()
|
||||
needs: [changes, fmtlint, test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: verdict
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||
if: |
|
||||
github.event_name != 'merge_group' && (needs.changes.result != 'success' ||
|
||||
(needs.changes.outputs.e2e == 'true' && (needs.fmtlint.result != 'success' || needs.test.result != 'success')))
|
||||
run: |
|
||||
echo "::error::e2e did not pass."
|
||||
echo "::error::e2e did not pass. Fork and dependabot PRs need the safe-to-test label."
|
||||
exit 1
|
||||
|
||||
1
.github/workflows/integrationci.yaml
vendored
@@ -75,7 +75,6 @@ jobs:
|
||||
- ttl
|
||||
- clickhousecluster
|
||||
- metricreduction
|
||||
- metricsexplorer
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
|
||||
@@ -41,6 +41,7 @@ linters:
|
||||
no-global: all
|
||||
context: all
|
||||
static-msg: true
|
||||
key-naming-case: snake
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
|
||||
25
cmd/authz.go
@@ -93,19 +93,18 @@ func runGenerateAuthz(_ context.Context) error {
|
||||
registry := coretypes.NewRegistry()
|
||||
|
||||
allowedResources := map[string]bool{
|
||||
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceDashboard).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceLicense).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceSubscription).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceLogs).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceTraces).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMetrics).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMeterMetrics).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceQuickFilter).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceSavedView).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceNotificationChannel).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceDashboard).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceLicense).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceSubscription).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceLogs).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceTraces).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMetrics).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMeterMetrics).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceQuickFilter).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceSavedView).String(): true,
|
||||
}
|
||||
|
||||
allowedTypes := map[string]bool{}
|
||||
|
||||
@@ -251,7 +251,6 @@ components:
|
||||
opsgenie: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelOpsgenieConfig'
|
||||
pagerduty: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelPagerdutyConfig'
|
||||
slack: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfig'
|
||||
telegram: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfig'
|
||||
webhook: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfig'
|
||||
propertyName: kind
|
||||
oneOf:
|
||||
@@ -265,7 +264,6 @@ components:
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfig'
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfig:
|
||||
properties:
|
||||
@@ -375,18 +373,6 @@ components:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- telegram
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelTelegramConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfig:
|
||||
properties:
|
||||
kind:
|
||||
@@ -539,7 +525,6 @@ components:
|
||||
- jira
|
||||
- jsmops
|
||||
- incidentio
|
||||
- telegram
|
||||
type: string
|
||||
AlertmanagertypesChannelListOrder:
|
||||
enum:
|
||||
@@ -744,31 +729,6 @@ components:
|
||||
- title
|
||||
- value
|
||||
type: object
|
||||
AlertmanagertypesChannelTelegramConfig:
|
||||
properties:
|
||||
apiUrl:
|
||||
type: string
|
||||
botToken:
|
||||
format: password
|
||||
type: string
|
||||
chatId:
|
||||
format: int64
|
||||
type: integer
|
||||
disableNotifications:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
messageThreadId:
|
||||
type: integer
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
title:
|
||||
type: string
|
||||
required:
|
||||
- botToken
|
||||
- chatId
|
||||
type: object
|
||||
AlertmanagertypesChannelWebhookConfig:
|
||||
properties:
|
||||
bearerToken:
|
||||
@@ -1062,8 +1022,6 @@ components:
|
||||
- jsmops_configs
|
||||
- required:
|
||||
- incidentio_configs
|
||||
- required:
|
||||
- telegram_configs
|
||||
- required:
|
||||
- discord_configs
|
||||
- required:
|
||||
@@ -1084,6 +1042,8 @@ components:
|
||||
- victorops_configs
|
||||
- required:
|
||||
- sns_configs
|
||||
- required:
|
||||
- telegram_configs
|
||||
- required:
|
||||
- webex_configs
|
||||
- required:
|
||||
@@ -1159,7 +1119,7 @@ components:
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesTelegramReceiverConfig'
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
@@ -1303,7 +1263,7 @@ components:
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesTelegramReceiverConfig'
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
@@ -1374,34 +1334,6 @@ components:
|
||||
- timezone
|
||||
- startTime
|
||||
type: object
|
||||
AlertmanagertypesTelegramReceiverConfig:
|
||||
properties:
|
||||
api_url:
|
||||
$ref: '#/components/schemas/ConfigURLType2'
|
||||
chat:
|
||||
format: int64
|
||||
type: integer
|
||||
chat_file:
|
||||
type: string
|
||||
disable_notifications:
|
||||
type: boolean
|
||||
http_config:
|
||||
$ref: '#/components/schemas/ConfigHTTPClientConfig'
|
||||
message:
|
||||
type: string
|
||||
message_thread_id:
|
||||
type: integer
|
||||
parse_mode:
|
||||
type: string
|
||||
send_resolved:
|
||||
type: boolean
|
||||
title:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
token_file:
|
||||
type: string
|
||||
type: object
|
||||
AlertmanagertypesTestableNotificationChannel:
|
||||
properties:
|
||||
config:
|
||||
@@ -1442,13 +1374,9 @@ components:
|
||||
AuthtypesAuthDomainConfigGoogle:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
description: The kind of authn provider.
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
description: The google auth configuration.
|
||||
$ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -1456,13 +1384,9 @@ components:
|
||||
AuthtypesAuthDomainConfigOIDC:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
description: The kind of authn provider.
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
description: The oidc configuration.
|
||||
$ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -1470,13 +1394,9 @@ components:
|
||||
AuthtypesAuthDomainConfigSAML:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
description: The kind of authn provider.
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
description: The saml configuration.
|
||||
$ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -2239,8 +2159,6 @@ components:
|
||||
type: object
|
||||
CloudintegrationtypesGCPAccountConfig:
|
||||
properties:
|
||||
connectionMethod:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesGCPConnectionMethod'
|
||||
deploymentProjectId:
|
||||
type: string
|
||||
deploymentRegion:
|
||||
@@ -2250,79 +2168,13 @@ components:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- connectionMethod
|
||||
- deploymentProjectId
|
||||
- deploymentRegion
|
||||
- projectIds
|
||||
type: object
|
||||
CloudintegrationtypesGCPConnectionArtifact:
|
||||
properties:
|
||||
cliCommand:
|
||||
type: string
|
||||
required:
|
||||
- cliCommand
|
||||
type: object
|
||||
CloudintegrationtypesGCPConnectionMethod:
|
||||
enum:
|
||||
- manual
|
||||
- agent
|
||||
type: string
|
||||
CloudintegrationtypesGCPIntegrationConfig:
|
||||
properties:
|
||||
connectionMethod:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesGCPConnectionMethod'
|
||||
deploymentProjectId:
|
||||
type: string
|
||||
deploymentRegion:
|
||||
type: string
|
||||
projectIds:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
telemetryCollectionStrategy:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesGCPTelemetryCollectionStrategy'
|
||||
required:
|
||||
- connectionMethod
|
||||
- deploymentProjectId
|
||||
- deploymentRegion
|
||||
- projectIds
|
||||
- telemetryCollectionStrategy
|
||||
type: object
|
||||
CloudintegrationtypesGCPLogsCollectionStrategy:
|
||||
nullable: true
|
||||
properties:
|
||||
sinkFilter:
|
||||
type: string
|
||||
required:
|
||||
- sinkFilter
|
||||
type: object
|
||||
CloudintegrationtypesGCPMetricsCollectionStrategy:
|
||||
nullable: true
|
||||
properties:
|
||||
metricNames:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- metricNames
|
||||
type: object
|
||||
CloudintegrationtypesGCPPostableAccountConfig:
|
||||
properties:
|
||||
connectionMethod:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesGCPConnectionMethod'
|
||||
deploymentProjectId:
|
||||
type: string
|
||||
deploymentRegion:
|
||||
type: string
|
||||
projectIds:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- connectionMethod
|
||||
- deploymentProjectId
|
||||
- deploymentRegion
|
||||
- projectIds
|
||||
type: object
|
||||
CloudintegrationtypesGCPServiceConfig:
|
||||
properties:
|
||||
@@ -2345,16 +2197,6 @@ components:
|
||||
required:
|
||||
- enabled
|
||||
type: object
|
||||
CloudintegrationtypesGCPTelemetryCollectionStrategy:
|
||||
properties:
|
||||
logs:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesGCPLogsCollectionStrategy'
|
||||
metrics:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesGCPMetricsCollectionStrategy'
|
||||
required:
|
||||
- metrics
|
||||
- logs
|
||||
type: object
|
||||
CloudintegrationtypesGettableAccountWithConnectionArtifact:
|
||||
properties:
|
||||
connectionArtifact:
|
||||
@@ -2491,7 +2333,7 @@ components:
|
||||
azure:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAzureAccountConfig'
|
||||
gcp:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesGCPPostableAccountConfig'
|
||||
$ref: '#/components/schemas/CloudintegrationtypesGCPAccountConfig'
|
||||
type: object
|
||||
CloudintegrationtypesPostableAgentCheckIn:
|
||||
properties:
|
||||
@@ -2726,6 +2568,7 @@ components:
|
||||
projectIds:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- deploymentProjectId
|
||||
@@ -3658,7 +3501,6 @@ components:
|
||||
- meter-metrics
|
||||
- logs-field
|
||||
- traces-field
|
||||
- metrics-field
|
||||
type: string
|
||||
CoretypesObject:
|
||||
properties:
|
||||
@@ -8310,13 +8152,9 @@ components:
|
||||
Querybuildertypesv5BucketOptionsLinear:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5BucketsKind'
|
||||
description: How the upper bounds are spaced.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5BucketsKind'
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5LinearBucketsSpec'
|
||||
description: The evenly spaced bucket specification.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5LinearBucketsSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -8324,13 +8162,9 @@ components:
|
||||
Querybuildertypesv5BucketOptionsLog:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5BucketsKind'
|
||||
description: How the upper bounds are spaced.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5BucketsKind'
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5LogBucketsSpec'
|
||||
description: The logarithmic bucket specification.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5LogBucketsSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -8616,8 +8450,6 @@ components:
|
||||
type: string
|
||||
query:
|
||||
type: string
|
||||
skipStepAlignment:
|
||||
type: boolean
|
||||
stats:
|
||||
type: boolean
|
||||
step:
|
||||
@@ -8921,78 +8753,54 @@ components:
|
||||
Querybuildertypesv5QueryEnvelopeBuilder:
|
||||
properties:
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5BuilderQuerySpec'
|
||||
description: The builder query specification.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5BuilderQuerySpec'
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeBuilderAI:
|
||||
properties:
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
|
||||
description: The AI builder query specification.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeClickHouseSQL:
|
||||
properties:
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5ClickHouseQuery'
|
||||
description: The ClickHouse SQL query specification.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5ClickHouseQuery'
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeFormula:
|
||||
properties:
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderFormula'
|
||||
description: The formula specification.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderFormula'
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopePromQL:
|
||||
properties:
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5PromQuery'
|
||||
description: The PromQL query specification.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5PromQuery'
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeTraceOperator:
|
||||
properties:
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderTraceOperator'
|
||||
description: The trace operator specification.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderTraceOperator'
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
@@ -9552,13 +9360,9 @@ components:
|
||||
RuletypesEvaluationCumulative:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
description: The kind of evaluation.
|
||||
$ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesCumulativeWindow'
|
||||
description: The cumulative window evaluation specification.
|
||||
$ref: '#/components/schemas/RuletypesCumulativeWindow'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -9581,13 +9385,9 @@ components:
|
||||
RuletypesEvaluationRolling:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
description: The kind of evaluation.
|
||||
$ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesRollingWindow'
|
||||
description: The rolling window evaluation specification.
|
||||
$ref: '#/components/schemas/RuletypesRollingWindow'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -9968,13 +9768,9 @@ components:
|
||||
RuletypesThresholdBasic:
|
||||
properties:
|
||||
kind:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesThresholdKind'
|
||||
description: The kind of threshold.
|
||||
$ref: '#/components/schemas/RuletypesThresholdKind'
|
||||
spec:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesBasicRuleThresholds'
|
||||
description: The basic threshold specification (array of thresholds).
|
||||
$ref: '#/components/schemas/RuletypesBasicRuleThresholds'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -20717,9 +20513,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- metrics-field:list
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- metrics-field:list
|
||||
- VIEWER
|
||||
summary: List metric names
|
||||
tags:
|
||||
- metrics
|
||||
@@ -20786,9 +20582,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- rule:list
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- rule:list
|
||||
- VIEWER
|
||||
summary: Get metric alerts
|
||||
tags:
|
||||
- metrics
|
||||
@@ -20870,9 +20666,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- metrics-field:read
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- metrics-field:read
|
||||
- VIEWER
|
||||
summary: Get metric attributes
|
||||
tags:
|
||||
- metrics
|
||||
@@ -20940,9 +20736,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
summary: Get metric highlights
|
||||
tags:
|
||||
- metrics
|
||||
@@ -20999,9 +20795,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
summary: Inspect raw metric data points
|
||||
tags:
|
||||
- metrics
|
||||
@@ -21069,9 +20865,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- metrics-field:read
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- metrics-field:read
|
||||
- VIEWER
|
||||
summary: Get metric metadata
|
||||
tags:
|
||||
- metrics
|
||||
@@ -21115,9 +20911,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- metrics-field:update
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- metrics-field:update
|
||||
- EDITOR
|
||||
summary: Update metric metadata
|
||||
tags:
|
||||
- metrics
|
||||
@@ -21163,9 +20959,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
summary: Check if non-SigNoz metrics have been received
|
||||
tags:
|
||||
- metrics
|
||||
@@ -21222,9 +21018,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
summary: Get metrics statistics
|
||||
tags:
|
||||
- metrics
|
||||
@@ -21281,9 +21077,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- metrics:read
|
||||
- VIEWER
|
||||
summary: Get metrics treemap
|
||||
tags:
|
||||
- metrics
|
||||
@@ -28422,9 +28218,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- dashboard:list
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- dashboard:list
|
||||
- VIEWER
|
||||
summary: Get metric dashboards (v2)
|
||||
tags:
|
||||
- metrics
|
||||
|
||||
@@ -43,25 +43,6 @@ type Resource interface {
|
||||
|
||||
All resources are registered in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) using constructors like `NewResourceMetaResource(KindDashboard)` or `NewResourceTelemetryResource(KindLogs)`.
|
||||
|
||||
### Telemetry resources and metaresources
|
||||
|
||||
Almost every feature resource is one of these two types, and they answer different questions.
|
||||
|
||||
- A **telemetry resource** (`logs`, `traces`, `metrics`, `meter-metrics`, `audit-logs`) is the data itself. It only supports `read`. Its selector is not an ID but the query: the route wires a `TelemetryResourceDef` whose extractor turns the caller's filter into `<query_type>/<key>/<value>` IDs, and the check ladders each one through `<query_type>/<key>/*`, `<query_type>/*` and `*`. A grant such as `builder_query/signoz.workspace.key.id/key-a` therefore decides which rows a role may read; a query that does not pin a granted key resolves to `<query_type>/*` and is denied for key-scoped roles. Managed roles hold `*`.
|
||||
- A **metaresource** (`dashboard`, `rule`, `logs-field`, ...) is an object that describes or configures the system. It supports whichever of `create`, `read`, `update`, `delete` and `list` the resource declares, its selectors are object IDs or `*`, and routes wire a `BasicResourceDef`. It says nothing about rows: `dashboard:read` on `*` does not grant the telemetry a dashboard's panels query.
|
||||
|
||||
The metrics explorer uses both on one surface:
|
||||
|
||||
| Route | Resource | Why |
|
||||
|---|---|---|
|
||||
| `POST /api/v2/metrics/stats` | `metrics:read` (telemetry) | Returns sample and series counts, i.e. the data. The selector comes from `filter.expression`, so a role scoped to a workspace key sees only its own samples. |
|
||||
| `GET /api/v2/metrics` | `metrics-field:list` (meta) | Lists metric names, types and units: the catalogue, not the samples. |
|
||||
| `POST /api/v2/metrics/metadata` | `metrics-field:update` (meta) | Edits a metric's type, unit and description. |
|
||||
|
||||
The two are independent: a role holding `metrics-field` `list` and `read` can browse the catalogue but cannot read a sample, and a role scoped to `builder_query/signoz.workspace.key.id/key-a` on `metrics` can query its samples but cannot list the catalogue.
|
||||
|
||||
For a new route, ask what it returns. Rows or aggregates over them go on the signal's telemetry resource with an extractor; anything that names, describes or configures telemetry goes on a metaresource.
|
||||
|
||||
### Selector
|
||||
|
||||
A `Selector` identifies *which* instance(s) of a resource a check is about — a UUID, a role name, or the wildcard `*`. A `SelectorFunc` maps the extracted resource ID to selectors at request time. Two prebuilt ones cover most routes ([pkg/types/coretypes/selector.go](/pkg/types/coretypes/selector.go)):
|
||||
@@ -161,7 +142,6 @@ Because both paths go through the same middleware and the same `ResourceDef` dec
|
||||
## What should I remember?
|
||||
|
||||
- Declare authz in the registries ([pkg/types/coretypes](/pkg/types/coretypes)), not in migrations — tuples for new organizations are derived from code at bootstrap.
|
||||
- Telemetry resources are the data: `read` only, selector derived from the query. Metaresources are objects about the data: CRUD and list, ID or wildcard selectors. A route that returns rows or aggregates is telemetry; one that names or configures them is meta, as `metrics` versus `metrics-field` shows.
|
||||
- A new kind never needs an OpenFGA schema change; only a new type does, and then **both** [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) must be updated together.
|
||||
- Prefer `CheckResources` + `ResourceDef` over the coarse `ViewAccess`/`EditAccess`/`AdminAccess` gates for new routes.
|
||||
- Use `WildcardSelector` for `create`/`list`, `IDSelector` for instance operations, and a custom `SelectorFunc` when the request ID is not the FGA selector.
|
||||
|
||||
@@ -7,66 +7,24 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
)
|
||||
|
||||
type gcpcloudprovider struct {
|
||||
serviceDefinitions cloudintegrationtypes.ServiceDefinitionStore
|
||||
}
|
||||
|
||||
func NewGCPCloudProvider(defStore cloudintegrationtypes.ServiceDefinitionStore) cloudintegration.CloudProviderModule {
|
||||
return &gcpcloudprovider{
|
||||
serviceDefinitions: defStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *gcpcloudprovider) BuildIntegrationConfig(
|
||||
ctx context.Context,
|
||||
account *cloudintegrationtypes.Account,
|
||||
services []*cloudintegrationtypes.StorableCloudIntegrationService,
|
||||
) (*cloudintegrationtypes.ProviderIntegrationConfig, error) {
|
||||
if account.Config.GCP.ConnectionMethod != cloudintegrationtypes.GCPConnectionMethodAgent {
|
||||
return &cloudintegrationtypes.ProviderIntegrationConfig{}, nil
|
||||
}
|
||||
|
||||
var metricNames []string
|
||||
var sinkFilters []string
|
||||
|
||||
for _, storedSvc := range services {
|
||||
svcCfg, err := cloudintegrationtypes.NewServiceConfigFromJSON(cloudintegrationtypes.CloudProviderTypeGCP, storedSvc.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svcDef, err := g.GetServiceDefinition(ctx, storedSvc.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if svcDef.TelemetryCollectionStrategy.GCP == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
strategy := svcDef.TelemetryCollectionStrategy.GCP
|
||||
if svcCfg.IsMetricsEnabled(cloudintegrationtypes.CloudProviderTypeGCP) && strategy.Metrics != nil {
|
||||
metricNames = append(metricNames, strategy.Metrics.MetricNames...)
|
||||
}
|
||||
|
||||
if svcCfg.IsLogsEnabled(cloudintegrationtypes.CloudProviderTypeGCP) && strategy.Logs != nil {
|
||||
sinkFilters = append(sinkFilters, strategy.Logs.SinkFilter)
|
||||
}
|
||||
}
|
||||
|
||||
return &cloudintegrationtypes.ProviderIntegrationConfig{
|
||||
GCP: cloudintegrationtypes.NewGCPIntegrationConfig(account.Config.GCP, metricNames, sinkFilters),
|
||||
}, nil
|
||||
func (g *gcpcloudprovider) BuildIntegrationConfig(ctx context.Context, account *cloudintegrationtypes.Account, services []*cloudintegrationtypes.StorableCloudIntegrationService) (*cloudintegrationtypes.ProviderIntegrationConfig, error) {
|
||||
// for manual flow we don't have any integration config to return, so returning empty config for now.
|
||||
return &cloudintegrationtypes.ProviderIntegrationConfig{}, nil
|
||||
}
|
||||
|
||||
func (g *gcpcloudprovider) GetConnectionArtifact(ctx context.Context, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.GetConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
||||
if account.Config.GCP.ConnectionMethod != cloudintegrationtypes.GCPConnectionMethodAgent {
|
||||
return &cloudintegrationtypes.ConnectionArtifact{}, nil
|
||||
}
|
||||
|
||||
connectionArtifact, err := cloudintegrationtypes.NewGCPConnectionArtifact(account.ID, req.Config.AgentVersion, req.Credentials, account.Config.GCP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cloudintegrationtypes.ConnectionArtifact{
|
||||
GCP: connectionArtifact,
|
||||
}, nil
|
||||
// for manual flow we don't have any connection artifact to return, so returning empty artifact for now.
|
||||
return &cloudintegrationtypes.ConnectionArtifact{}, nil
|
||||
}
|
||||
|
||||
func (g *gcpcloudprovider) GetServiceDefinition(ctx context.Context, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
|
||||
@@ -76,7 +34,3 @@ func (g *gcpcloudprovider) GetServiceDefinition(ctx context.Context, serviceID c
|
||||
func (g *gcpcloudprovider) ListServiceDefinitions(ctx context.Context) ([]*cloudintegrationtypes.ServiceDefinition, error) {
|
||||
return g.serviceDefinitions.List(ctx, cloudintegrationtypes.CloudProviderTypeGCP)
|
||||
}
|
||||
|
||||
type gcpcloudprovider struct {
|
||||
serviceDefinitions cloudintegrationtypes.ServiceDefinitionStore
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package implcloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -170,6 +171,18 @@ func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provi
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
connectedAccount, err := module.store.GetConnectedAccountByProviderAccountID(ctx, orgID, req.ProviderAccountID, provider)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If a different integration is already connected to this provider account ID, reject the check-in.
|
||||
// Allow re-check-in from the same integration (e.g. agent restarting).
|
||||
if connectedAccount != nil && connectedAccount.ID != req.CloudIntegrationID {
|
||||
errMessage := fmt.Sprintf("provider account id %s is already connected to cloud integration id %s", req.ProviderAccountID, connectedAccount.ID)
|
||||
return nil, errors.New(errors.TypeAlreadyExists, cloudintegrationtypes.ErrCodeCloudIntegrationAlreadyConnected, errMessage)
|
||||
}
|
||||
|
||||
storableAccount, err := module.store.GetAccountByID(ctx, orgID, req.CloudIntegrationID, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -182,36 +195,41 @@ func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provi
|
||||
|
||||
syncState := account.NextSyncState(req.SyncedVersion)
|
||||
|
||||
// If account has been removed (disconnected), return a minimal response with empty integration config.
|
||||
if account.RemovedAt != nil {
|
||||
return module.checkInRemovedAccount(ctx, account, req.ProviderAccountID, syncState)
|
||||
}
|
||||
// Heartbeat stays frozen after removal, only the sync state is updated.
|
||||
if account.AgentReport != nil && syncState != nil {
|
||||
account.UpdateSyncState(syncState)
|
||||
|
||||
if err := account.ErrIfInvalidProviderAccountID(req.ProviderAccountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storableAccount, err = cloudintegrationtypes.NewStorableCloudIntegration(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connectedAccount, err := module.store.GetConnectedAccountByProviderAccountID(ctx, orgID, req.ProviderAccountID, provider)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
err = module.store.UpdateAgentReport(ctx, storableAccount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If a different integration is already connected to this provider account ID, reject the check-in.
|
||||
// Allow re-check-in from the same integration (e.g. agent restarting).
|
||||
if connectedAccount != nil && connectedAccount.ID != req.CloudIntegrationID {
|
||||
return nil, errors.Newf(
|
||||
errors.TypeAlreadyExists,
|
||||
cloudintegrationtypes.ErrCodeCloudIntegrationAlreadyConnected,
|
||||
"provider account id %s is already connected to cloud integration id %s", req.ProviderAccountID, connectedAccount.ID,
|
||||
)
|
||||
return cloudintegrationtypes.NewAgentCheckInResponse(
|
||||
req.ProviderAccountID,
|
||||
account.ID.StringValue(),
|
||||
new(cloudintegrationtypes.ProviderIntegrationConfig),
|
||||
account.RemovedAt,
|
||||
syncState,
|
||||
), nil
|
||||
}
|
||||
|
||||
account.UpdateAgentReport(&req.ProviderAccountID, cloudintegrationtypes.NewAgentReport(req.Data, syncState))
|
||||
|
||||
storableAccount, err = cloudintegrationtypes.NewStorableCloudIntegration(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := module.store.UpdateAgentReport(ctx, storableAccount); err != nil {
|
||||
err = module.store.UpdateAgentReport(ctx, storableAccount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -480,45 +498,11 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
|
||||
stats["cloudintegration.azure.connectedaccounts.count"] = azureAccountsCount
|
||||
}
|
||||
|
||||
gcpAccountsCount, err := module.store.CountConnectedAccounts(ctx, orgID, cloudintegrationtypes.CloudProviderTypeGCP)
|
||||
if err == nil {
|
||||
stats["cloudintegration.gcp.connectedaccounts.count"] = gcpAccountsCount
|
||||
}
|
||||
// NOTE: not adding stats for services for now.
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// checkInRemovedAccount returns an empty integration config and saves cleanup acknowledgments without refreshing the heartbeat.
|
||||
func (module *module) checkInRemovedAccount(ctx context.Context, account *cloudintegrationtypes.Account, providerAccountID string, syncState *cloudintegrationtypes.SyncState) (*cloudintegrationtypes.AgentCheckInResponse, error) {
|
||||
response := cloudintegrationtypes.NewAgentCheckInResponse(
|
||||
providerAccountID,
|
||||
account.ID.StringValue(),
|
||||
new(cloudintegrationtypes.ProviderIntegrationConfig),
|
||||
account.RemovedAt,
|
||||
syncState,
|
||||
)
|
||||
|
||||
if account.AgentReport == nil {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
if syncState == nil {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
account.UpdateSyncState(syncState)
|
||||
storableAccount, err := cloudintegrationtypes.NewStorableCloudIntegration(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := module.store.UpdateAgentReport(ctx, storableAccount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (module *module) getCloudProvider(provider cloudintegrationtypes.CloudProviderType) (cloudintegration.CloudProviderModule, error) {
|
||||
if cloudProviderModule, ok := module.cloudProvidersMap[provider]; ok {
|
||||
return cloudProviderModule, nil
|
||||
|
||||
@@ -116,7 +116,7 @@ func (c *clickhouse) EstimateCardinality(ctx context.Context, metricName string,
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
reducedExpr := "toUInt64(1)"
|
||||
reducedExpr := "1"
|
||||
if len(keptLabels) > 0 {
|
||||
reducedExpr = "uniq(("
|
||||
for i, label := range keptLabels {
|
||||
|
||||
@@ -277,7 +277,10 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
lb.Set(ruletypes.AlertRuleIDLabel, r.ID())
|
||||
lb.Set(ruletypes.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := r.ExpandAnnotations(expand)
|
||||
annotations := make(ruletypes.Labels, 0, len(r.Annotations().Map()))
|
||||
for name, value := range r.Annotations().Map() {
|
||||
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
if smpl.IsMissing {
|
||||
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(ruletypes.NoDataLabel, "true")
|
||||
|
||||
@@ -262,20 +262,23 @@
|
||||
// React Hooks rules - Enforces Rules of Hooks (only call at top level)
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
// Warns about missing dependencies in useEffect/useMemo/useCallback
|
||||
// React Compiler rules, off because we don't have React Compiler enabled
|
||||
"react/error-boundaries": "off",
|
||||
"react/globals": "off",
|
||||
"react/immutability": "off",
|
||||
"react/incompatible-library": "off",
|
||||
"react/preserve-manual-memoization": "off",
|
||||
"react/purity": "off",
|
||||
"react/refs": "off",
|
||||
"react/set-state-in-effect": "off",
|
||||
"react/set-state-in-render": "off",
|
||||
"react/static-components": "off",
|
||||
"react/unsupported-syntax": "off",
|
||||
"react/use-memo": "off",
|
||||
"react/void-use-memo": "off",
|
||||
// NOTE: The following react-hooks rules are not supported right know, follow the progress at https://github.com/oxc-project/oxc/issues/1022
|
||||
// Most of them are for React Compiler, which we don't have enabled right know
|
||||
// "react-hooks/config": "error",
|
||||
// "react-hooks/error-boundaries": "error",
|
||||
// "react-hooks/component-hook-factories": "error",
|
||||
// "react-hooks/gating": "error",
|
||||
// "react-hooks/globals": "error",
|
||||
// "react-hooks/immutability": "error",
|
||||
// "react-hooks/preserve-manual-memoization": "error",
|
||||
// "react-hooks/purity": "error",
|
||||
// "react-hooks/refs": "error",
|
||||
// "react-hooks/set-state-in-effect": "error",
|
||||
// "react-hooks/set-state-in-render": "error",
|
||||
// "react-hooks/static-components": "error",
|
||||
// "react-hooks/unsupported-syntax": "warn",
|
||||
// "react-hooks/use-memo": "error",
|
||||
// "react-hooks/incompatible-library": "warn",
|
||||
"signoz/no-unsupported-asset-pattern": "error",
|
||||
// Prevents the wrong usage of assets to break custom base path installations
|
||||
"signoz/no-zustand-getstate-in-hooks": "error",
|
||||
@@ -579,16 +582,6 @@
|
||||
"rules": {
|
||||
"signoz/no-dashboard-fetch-outside-root": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
// TypeScript reports real redeclarations; this rule also flags `const X` + `type X` pairs.
|
||||
"files": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"no-redeclare": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ const preview: Preview = {
|
||||
'Planned Downtime',
|
||||
'Routing Policies',
|
||||
'Channels',
|
||||
['List', 'List Authz', 'New', 'New Authz', 'Edit', 'Edit Authz'],
|
||||
['List', 'New', 'Edit'],
|
||||
],
|
||||
'Dashboards',
|
||||
[
|
||||
|
||||
@@ -163,9 +163,9 @@
|
||||
"@commitlint/config-conventional": "20.4.4",
|
||||
"@jest/globals": "30.4.1",
|
||||
"@jest/types": "30.2.0",
|
||||
"@storybook/addon-a11y": "10.6.3",
|
||||
"@storybook/addon-docs": "10.6.3",
|
||||
"@storybook/react-vite": "10.6.3",
|
||||
"@storybook/addon-a11y": "10.5.9",
|
||||
"@storybook/addon-docs": "10.5.9",
|
||||
"@storybook/react-vite": "10.5.9",
|
||||
"@storybook/test-runner": "0.24.5",
|
||||
"@testing-library/dom": "8.20.0",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
@@ -204,19 +204,19 @@
|
||||
"lint-staged": "^17.0.4",
|
||||
"msw": "1.3.2",
|
||||
"orval": "8.22.0",
|
||||
"oxfmt": "0.73.0",
|
||||
"oxlint": "1.88.0",
|
||||
"oxlint-tsgolint": "7.0.2004",
|
||||
"oxfmt": "0.54.0",
|
||||
"oxlint": "1.69.0",
|
||||
"oxlint-tsgolint": "0.23.0",
|
||||
"postcss": "8.5.26",
|
||||
"postcss-scss": "4.0.9",
|
||||
"react-resizable": "3.0.4",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"sass": "1.97.3",
|
||||
"sharp": "0.35.4",
|
||||
"storybook": "10.6.3",
|
||||
"storybook": "10.5.9",
|
||||
"stylelint": "17.15.0",
|
||||
"svgo": "4.1.0",
|
||||
"ts-jest": "29.4.15",
|
||||
"ts-jest": "29.4.9",
|
||||
"typescript-plugin-css-modules": "5.2.0",
|
||||
"use-sync-external-store": "1.6.0",
|
||||
"vite-plugin-checker": "0.12.0",
|
||||
|
||||
788
frontend/pnpm-lock.yaml
generated
@@ -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.21: '>=1.1.21 <2'
|
||||
brace-expansion@<1.1.18: '>=1.1.18 <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.7': '>=2.1.7 <3'
|
||||
'brace-expansion@>=2.0.0 <2.1.4': '>=2.1.4 <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.12': '>=5.0.12 <6'
|
||||
'brace-expansion@>=5.0.0 <5.0.9': '>=5.0.9 <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
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
{
|
||||
"channel_delete_unexp_error": "Something went wrong",
|
||||
"channel_delete_success": "Channel Deleted Successfully",
|
||||
"column_channel_name": "Name",
|
||||
"column_channel_type": "Type",
|
||||
"column_channel_action": "Action",
|
||||
"column_channel_edit": "Edit",
|
||||
"button_new_channel": "New Alert Channel",
|
||||
"tooltip_notification_channels": "More details on how to setting notification channels",
|
||||
"sending_channels_note": "The alerts will be sent to all the configured channels.",
|
||||
"loading_channels_message": "Loading Channels..",
|
||||
"page_title_create": "New Notification Channel",
|
||||
"page_title_edit": "Edit Notification Channel",
|
||||
"page_title_view": "View Notification Channel",
|
||||
"button_save_channel": "Save",
|
||||
"button_test_channel": "Test",
|
||||
"button_return": "Back",
|
||||
@@ -116,9 +123,12 @@
|
||||
"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_creation_failed": "An unexpected error occurred while creating this channel",
|
||||
"channel_edit_done": "Channels Edited Successfully",
|
||||
"channel_edit_failed": "An unexpected error occurred while updating this channel",
|
||||
"selected_channel_invalid": "Channel type selected is invalid",
|
||||
"username_no_password": "A Password must be provided with user name",
|
||||
"test_unsupported": "Sorry, this channel type does not support test yet",
|
||||
"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",
|
||||
@@ -127,4 +137,4 @@
|
||||
"api_key_required": "API Key is mandatory",
|
||||
"to_required": "To field is mandatory",
|
||||
"channel_name_required": "Channel name is mandatory"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
{
|
||||
"channel_delete_unexp_error": "Something went wrong",
|
||||
"channel_load_failed": "This channel could not be opened",
|
||||
"channel_delete_success": "Channel Deleted Successfully",
|
||||
"column_channel_name": "Name",
|
||||
"column_channel_type": "Type",
|
||||
"column_channel_action": "Action",
|
||||
"column_channel_edit": "Edit",
|
||||
"button_new_channel": "New Alert Channel",
|
||||
"tooltip_notification_channels": "More details on how to setting notification channels",
|
||||
"sending_channels_note": "The alerts will be sent to all the configured channels.",
|
||||
"loading_channels_message": "Loading Channels..",
|
||||
"page_title_create": "New Notification Channel",
|
||||
"page_title_edit": "Edit Notification Channel",
|
||||
"page_title_view": "View Notification Channel",
|
||||
"button_save_channel": "Save",
|
||||
"button_test_channel": "Test",
|
||||
"button_return": "Back",
|
||||
"section_general": "General",
|
||||
"section_connection": "Connection",
|
||||
"section_message": "Message",
|
||||
"section_advanced": "Advanced",
|
||||
"description_slack_advanced": "The rest of the Slack attachment. Every box here is optional.",
|
||||
"section_recipients": "Recipients",
|
||||
"field_channel_name": "Name",
|
||||
"help_channel_name_immutable": "Renaming is blocked while alert rules and routing policies still reference a channel by name.",
|
||||
"field_send_resolved": "Send resolved alerts",
|
||||
"field_channel_type": "Type",
|
||||
"field_webhook_url": "Webhook URL",
|
||||
@@ -96,44 +95,9 @@
|
||||
"placeholder_incidentio_metadata_key": "Key",
|
||||
"placeholder_incidentio_metadata_value": "Value",
|
||||
"button_incidentio_add_metadata": "Add metadata",
|
||||
"button_incidentio_remove_metadata": "Remove metadata",
|
||||
"field_slack_recipient": "Recipient",
|
||||
"field_slack_title": "Title",
|
||||
"field_slack_description": "Description",
|
||||
"field_slack_title_link": "Title link",
|
||||
"field_slack_color": "Color",
|
||||
"help_slack_color": "good, warning, danger, or a hex value like #439FE0. Templates are allowed.",
|
||||
"placeholder_slack_color": "{{ if eq .Status \"firing\" }}danger{{ else }}good{{ end }}",
|
||||
"field_slack_pretext": "Pretext",
|
||||
"help_slack_pretext": "Shown above the attachment block",
|
||||
"field_slack_fallback": "Fallback text",
|
||||
"help_slack_fallback": "Plain text shown where the attachment cannot render, such as push notifications",
|
||||
"field_slack_footer": "Footer",
|
||||
"field_slack_fields": "Fields",
|
||||
"help_slack_fields": "Extra entries rendered as a table inside the attachment",
|
||||
"placeholder_slack_field_title": "Title",
|
||||
"placeholder_slack_field_value": "Value",
|
||||
"field_slack_field_short": "Short",
|
||||
"add_slack_field": "Add field",
|
||||
"remove_slack_field": "Remove 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",
|
||||
"label_slack_action_url": "URL",
|
||||
"label_slack_action_type": "Type",
|
||||
"label_slack_action_name": "Name",
|
||||
"label_slack_action_value": "Value",
|
||||
"label_slack_action_style": "Style",
|
||||
"label_slack_action_confirm": "Confirm",
|
||||
"placeholder_slack_action_text": "Button text",
|
||||
"placeholder_slack_action_url": "https://runbook.example.com",
|
||||
"placeholder_slack_action_type": "button",
|
||||
"placeholder_slack_action_name": "Name (Slack app callbacks)",
|
||||
"placeholder_slack_action_value": "Value (Slack app callbacks)",
|
||||
"placeholder_slack_action_style": "Style: default, primary or danger",
|
||||
"placeholder_slack_action_confirm": "Confirmation prompt (optional)",
|
||||
"add_slack_action": "Add action",
|
||||
"remove_slack_action": "Remove action",
|
||||
"field_opsgenie_api_key": "API Key",
|
||||
"field_opsgenie_description": "Description",
|
||||
"placeholder_opsgenie_description": "Description",
|
||||
@@ -145,7 +109,6 @@
|
||||
"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_pager_routing_key": "Routing Key",
|
||||
"field_pager_description": "Description",
|
||||
"field_pager_severity": "Severity",
|
||||
@@ -170,66 +133,23 @@
|
||||
"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_pager_description": "Shows up as description in pagerduty",
|
||||
"help_opsgenie_message": "Shows up as message in opsgenie",
|
||||
"help_opsgenie_priority": "Priority of the incident",
|
||||
"help_opsgenie_description": "Shows up as description in opsgenie",
|
||||
"channel_creation_done": "Successfully created the channel",
|
||||
"channel_creation_failed": "An unexpected error occurred while creating this channel",
|
||||
"channel_edit_done": "Channels Edited Successfully",
|
||||
"channel_edit_failed": "An unexpected error occurred while updating this channel",
|
||||
"selected_channel_invalid": "Channel type selected is invalid",
|
||||
"username_no_password": "A Password must be provided with user name",
|
||||
"test_unsupported": "Sorry, this channel type does not support test yet",
|
||||
"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",
|
||||
"webhook_url_required": "Webhook URL is mandatory",
|
||||
"routing_key_required": "Routing Key is mandatory for creating pagerduty channel",
|
||||
"pager_details_invalid_json": "Enter valid JSON, or clear the box to send no details",
|
||||
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
|
||||
"api_key_required": "API Key is mandatory",
|
||||
"to_required": "To field is mandatory",
|
||||
"channel_name_required": "Channel name is mandatory",
|
||||
"tooltip_send_resolved": "Also notify this channel when an alert stops firing.",
|
||||
"tooltip_channel_type": "Where the alert is delivered. It cannot be changed once the channel is saved.",
|
||||
"button_discard_channel": "Discard",
|
||||
"button_discard_confirm": "Discard",
|
||||
"discard_changes_title": "Discard your changes?",
|
||||
"discard_changes_body": "This form has unsaved changes. If you leave now, they are gone and the channel stays as it is.",
|
||||
"tooltip_save_no_changes": "Nothing has changed yet.",
|
||||
"entry_slack_field": "Field",
|
||||
"entry_slack_action": "Action",
|
||||
"entry_incidentio_metadata": "Entry",
|
||||
"label_slack_field_title": "Title",
|
||||
"tooltip_slack_field_title": "The bold label Slack shows above the value, for example Severity.",
|
||||
"label_slack_field_value": "Value",
|
||||
"tooltip_slack_field_value": "What Slack shows under the title. Templates are allowed, so `{{ .CommonLabels.severity }}` fills in from the alert.",
|
||||
"label_incidentio_metadata_key": "Key",
|
||||
"label_incidentio_metadata_value": "Value",
|
||||
"tooltip_slack_fields": "Extra entries Slack renders as a small table inside the alert attachment.",
|
||||
"tooltip_slack_actions": "Buttons Slack renders under the attachment. A button with a URL links out; the rest are for a Slack app to handle.",
|
||||
"tooltip_slack_action_text": "The label on the button.",
|
||||
"tooltip_slack_action_url": "Where the button sends the reader. Leave empty for a button a Slack app handles instead.",
|
||||
"tooltip_slack_action_name": "Identifies the button to a Slack app. Not needed for a button that only links out.",
|
||||
"tooltip_slack_action_value": "Sent to a Slack app along with the name, so one app can tell its buttons apart.",
|
||||
"tooltip_slack_action_style": "default, primary or danger. Slack colours the button accordingly.",
|
||||
"tooltip_slack_action_confirm": "Text for a confirmation prompt shown before the action runs. Leave empty for no prompt.",
|
||||
"bot_token_required": "Bot token is mandatory for creating a telegram channel",
|
||||
"chat_id_required": "Chat ID is mandatory for creating a telegram channel",
|
||||
"telegram_tip": "Create a bot with @BotFather, add it to the chat, then paste its token and the chat's numeric id below.",
|
||||
"telegram_tip_link": "Learn how",
|
||||
"field_telegram_bot_token": "Bot token",
|
||||
"tooltip_telegram_bot_token": "The token BotFather returns, in the form 123456789:AA...",
|
||||
"field_telegram_chat_id": "Chat ID",
|
||||
"tooltip_telegram_chat_id": "The numeric chat the bot posts to, negative for groups and channels. Telegram's @username form is not accepted.",
|
||||
"placeholder_telegram_chat_id": "e.g. -1001234567890",
|
||||
"field_telegram_message_thread_id": "Message thread ID",
|
||||
"tooltip_telegram_message_thread_id": "Posts into one message thread of a forum group. Leave empty to post in the group itself.",
|
||||
"field_telegram_title": "Title",
|
||||
"tooltip_telegram_title": "Sent in bold above the message. Telegram HTML, so interpolated values are escaped.",
|
||||
"field_telegram_message": "Message",
|
||||
"tooltip_telegram_message": "Template for the message body. Telegram HTML, so interpolated values are escaped.",
|
||||
"field_telegram_api_url": "Bot API URL",
|
||||
"tooltip_telegram_api_url": "Point at a self-hosted Bot API server. Leave empty to use Telegram's.",
|
||||
"field_telegram_disable_notifications": "Send silently",
|
||||
"tooltip_telegram_disable_notifications": "Delivers without a sound or vibration on the recipients' devices."
|
||||
}
|
||||
"to_required": "To field is mandatory",
|
||||
"channel_name_required": "Channel name is mandatory"
|
||||
}
|
||||
@@ -300,17 +300,20 @@ 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],
|
||||
])('should redirect the old views route %s to %s', (from, to) => {
|
||||
renderPrivateRoute({ initialRoute: from });
|
||||
it('should redirect /logs-save-views to /logs/saved-views', () => {
|
||||
renderPrivateRoute({ initialRoute: '/logs-save-views' });
|
||||
|
||||
expect(screen.getByTestId('location-display').textContent).toBe(to);
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
'/logs/saved-views',
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect /traces-save-views to /traces/saved-views', () => {
|
||||
renderPrivateRoute({ initialRoute: '/traces-save-views' });
|
||||
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
'/traces/saved-views',
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect /settings/access-tokens to /settings/service-accounts', () => {
|
||||
@@ -1451,9 +1454,10 @@ describe('PrivateRoute', () => {
|
||||
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
|
||||
});
|
||||
|
||||
it('lets a VIEWER reach /alerts/channels/new, which authz then gates', () => {
|
||||
// CHANNELS_NEW runs on fine-grained authz, so the router no longer decides
|
||||
// on the role: the page's own guard denies when `create` is not granted.
|
||||
it('should redirect VIEWER from /alerts/channels/new (ADMIN only)', async () => {
|
||||
// After moving channels under /alerts, CHANNELS_NEW ('/alerts/channels/new')
|
||||
// is an exact, ADMIN-only route with no overlapping non-exact ALL_CHANNELS
|
||||
// route to match last, so a VIEWER is now correctly redirected.
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.CHANNELS_NEW,
|
||||
appContext: {
|
||||
@@ -1462,7 +1466,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertStaysOnRoute(ROUTES.CHANNELS_NEW);
|
||||
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
|
||||
});
|
||||
|
||||
it('should allow EDITOR to access /get-started-with-signoz-cloud route', () => {
|
||||
@@ -1546,11 +1550,6 @@ describe('PrivateRoute', () => {
|
||||
keyof typeof routeWithInitialAuthZSupport,
|
||||
AuthzRouteCase
|
||||
> = {
|
||||
CHANNELS_NEW: { path: ROUTES.CHANNELS_NEW, deniedRoles: DENIED_ROLES },
|
||||
CHANNELS_EDIT: {
|
||||
path: ROUTES.CHANNELS_EDIT.replace(':channelId', 'channel-id-1'),
|
||||
deniedRoles: DENIED_ROLES,
|
||||
},
|
||||
ALL_DASHBOARD: { path: ROUTES.ALL_DASHBOARD, deniedRoles: DENIED_ROLES },
|
||||
DASHBOARD: {
|
||||
path: ROUTES.DASHBOARD.replace(':dashboardId', 'dashboard-id-1'),
|
||||
@@ -1636,6 +1635,11 @@ describe('PrivateRoute', () => {
|
||||
['LIST_LICENSES', ROUTES.LIST_LICENSES, USER_ROLES.EDITOR as ROLES],
|
||||
['ONBOARDING', ROUTES.ONBOARDING, USER_ROLES.VIEWER as ROLES],
|
||||
['APPLICATION', ROUTES.APPLICATION, USER_ROLES.ANONYMOUS as ROLES],
|
||||
[
|
||||
'METRICS_EXPLORER_VIEWS',
|
||||
ROUTES.METRICS_EXPLORER_VIEWS,
|
||||
USER_ROLES.ANONYMOUS as ROLES,
|
||||
],
|
||||
];
|
||||
|
||||
it.each(authzRouteRolePairs)(
|
||||
|
||||
@@ -513,12 +513,8 @@ export const LIST_LICENSES: AppRoutes = {
|
||||
export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/pipelines': '/logs/pipelines',
|
||||
'/logs-explorer': '/logs/logs-explorer',
|
||||
'/logs-save-views': '/logs/logs-explorer',
|
||||
'/traces-save-views': '/traces-explorer',
|
||||
'/logs/saved-views': '/logs/logs-explorer',
|
||||
'/traces/saved-views': '/traces-explorer',
|
||||
'/metrics-explorer/views': '/metrics-explorer/explorer',
|
||||
'/meter/explorer/views': '/meter/explorer',
|
||||
'/logs-save-views': '/logs/saved-views',
|
||||
'/traces-save-views': '/traces/saved-views',
|
||||
'/settings/access-tokens': '/settings/service-accounts',
|
||||
'/settings/api-keys': '/settings/service-accounts',
|
||||
'/messaging-queues': '/messaging-queues/overview',
|
||||
|
||||
@@ -220,7 +220,8 @@ export type ClarificationSummaryDTODiscoveredContextAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type ClarificationSummaryDTODiscoveredContext = ClarificationSummaryDTODiscoveredContextAnyOf | null;
|
||||
export type ClarificationSummaryDTODiscoveredContext =
|
||||
ClarificationSummaryDTODiscoveredContextAnyOf | null;
|
||||
|
||||
export interface ClarificationSummaryDTO {
|
||||
/**
|
||||
@@ -783,7 +784,8 @@ export type ThreadDetailResponseDTOActiveExecutionId = string | null;
|
||||
|
||||
export type ThreadDetailResponseDTOPendingApproval = ApprovalSummaryDTO | null;
|
||||
|
||||
export type ThreadDetailResponseDTOPendingClarification = ClarificationSummaryDTO | null;
|
||||
export type ThreadDetailResponseDTOPendingClarification =
|
||||
ClarificationSummaryDTO | null;
|
||||
|
||||
/**
|
||||
* Full thread state for cold-start reconstruction (GET /threads/{id}).
|
||||
@@ -973,7 +975,8 @@ export type ClarificationEventDTODiscoveredContextAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type ClarificationEventDTODiscoveredContext = ClarificationEventDTODiscoveredContextAnyOf | null;
|
||||
export type ClarificationEventDTODiscoveredContext =
|
||||
ClarificationEventDTODiscoveredContextAnyOf | null;
|
||||
|
||||
export interface ClarificationEventDTO {
|
||||
/**
|
||||
@@ -1296,7 +1299,8 @@ export type CreateThreadApiV1AssistantThreadsPostHeaders = {
|
||||
'X-SigNoz-URL'?: string | null;
|
||||
};
|
||||
|
||||
export type CreateThreadApiV1AssistantThreadsPostBody = CreateThreadRequestDTO | null;
|
||||
export type CreateThreadApiV1AssistantThreadsPostBody =
|
||||
CreateThreadRequestDTO | null;
|
||||
|
||||
export type ListThreadsApiV1AssistantThreadsGetParams = {
|
||||
/**
|
||||
|
||||
40
frontend/src/api/channels/createEmail.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createEmail';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/channels', {
|
||||
name: props.name,
|
||||
email_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
to: props.to,
|
||||
html: props.html,
|
||||
headers: props.headers,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
||||
40
frontend/src/api/channels/createMsTeams.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createMsTeams';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/channels', {
|
||||
name: props.name,
|
||||
msteamsv2_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
webhook_url: props.webhook_url,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
||||
43
frontend/src/api/channels/createOpsgenie.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createOpsgenie';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/channels', {
|
||||
name: props.name,
|
||||
opsgenie_configs: [
|
||||
{
|
||||
api_key: props.api_key,
|
||||
description: props.description,
|
||||
priority: props.priority,
|
||||
message: props.message,
|
||||
details: {
|
||||
...props.detailsArray,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
||||
48
frontend/src/api/channels/createPager.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createPager';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/channels', {
|
||||
name: props.name,
|
||||
pagerduty_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
routing_key: props.routing_key,
|
||||
client: props.client,
|
||||
client_url: props.client_url,
|
||||
description: props.description,
|
||||
severity: props.severity,
|
||||
class: props.class,
|
||||
component: props.component,
|
||||
group: props.group,
|
||||
details: {
|
||||
...props.detailsArray,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
||||
41
frontend/src/api/channels/createSlack.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createSlack';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/channels', {
|
||||
name: props.name,
|
||||
slack_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
api_url: props.api_url,
|
||||
channel: props.channel,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
||||
59
frontend/src/api/channels/createWebhook.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createWebhook';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
let httpConfig = {};
|
||||
const username = props.username ? props.username.trim() : '';
|
||||
const password = props.password ? props.password.trim() : '';
|
||||
|
||||
if (username !== '' && password !== '') {
|
||||
httpConfig = {
|
||||
basic_auth: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
};
|
||||
} else if (username === '' && password !== '') {
|
||||
httpConfig = {
|
||||
authorization: {
|
||||
type: 'Bearer',
|
||||
credentials: password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const response = await axios.post<PayloadProps>('/channels', {
|
||||
name: props.name,
|
||||
webhook_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
url: props.api_url,
|
||||
http_config: httpConfig,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
||||
30
frontend/src/api/channels/delete.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/delete';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useDeleteChannelByID` hook (or `deleteChannelByID` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const deleteChannel = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.delete<PayloadProps>(`/channels/${props.id}`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteChannel;
|
||||
40
frontend/src/api/channels/editEmail.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/editEmail';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const editEmail = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(`/channels/${props.id}`, {
|
||||
name: props.name,
|
||||
email_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
to: props.to,
|
||||
html: props.html,
|
||||
headers: props.headers,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default editEmail;
|
||||
40
frontend/src/api/channels/editMsTeams.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/editMsTeams';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const editMsTeams = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(`/channels/${props.id}`, {
|
||||
name: props.name,
|
||||
msteamsv2_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
webhook_url: props.webhook_url,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default editMsTeams;
|
||||
44
frontend/src/api/channels/editOpsgenie.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/editOpsgenie';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const editOpsgenie = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(`/channels/${props.id}`, {
|
||||
name: props.name,
|
||||
opsgenie_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
api_key: props.api_key,
|
||||
description: props.description,
|
||||
priority: props.priority,
|
||||
message: props.message,
|
||||
details: {
|
||||
...props.detailsArray,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default editOpsgenie;
|
||||
48
frontend/src/api/channels/editPager.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/editPager';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const editPager = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(`/channels/${props.id}`, {
|
||||
name: props.name,
|
||||
pagerduty_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
routing_key: props.routing_key,
|
||||
client: props.client,
|
||||
client_url: props.client_url,
|
||||
description: props.description,
|
||||
severity: props.severity,
|
||||
class: props.class,
|
||||
component: props.component,
|
||||
group: props.group,
|
||||
details: {
|
||||
...props.detailsArray,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default editPager;
|
||||
41
frontend/src/api/channels/editSlack.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/editSlack';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const editSlack = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(`/channels/${props.id}`, {
|
||||
name: props.name,
|
||||
slack_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
api_url: props.api_url,
|
||||
channel: props.channel,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default editSlack;
|
||||
59
frontend/src/api/channels/editWebhook.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/editWebhook';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const editWebhook = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
let httpConfig = {};
|
||||
const username = props.username ? props.username.trim() : '';
|
||||
const password = props.password ? props.password.trim() : '';
|
||||
|
||||
if (username !== '' && password !== '') {
|
||||
httpConfig = {
|
||||
basic_auth: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
};
|
||||
} else if (username === '' && password !== '') {
|
||||
httpConfig = {
|
||||
authorization: {
|
||||
type: 'Bearer',
|
||||
credentials: password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const response = await axios.put<PayloadProps>(`/channels/${props.id}`, {
|
||||
name: props.name,
|
||||
webhook_configs: [
|
||||
{
|
||||
send_resolved: props.send_resolved,
|
||||
url: props.api_url,
|
||||
http_config: httpConfig,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default editWebhook;
|
||||
29
frontend/src/api/channels/get.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/get';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useGetChannelByID` hook (or `getChannelByID` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const get = async (props: Props): Promise<SuccessResponseV2<Channels>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/channels/${props.id}`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
28
frontend/src/api/channels/getAll.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Channels, PayloadProps } from 'types/api/channels/getAll';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useListChannels` hook (or `listChannels` fetcher) from
|
||||
* `api/generated/services/channels` 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.
|
||||
*/
|
||||
const getAll = async (): Promise<SuccessResponseV2<Channels[]>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>('/channels');
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default getAll;
|
||||
33
frontend/src/api/channels/testEmail.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createEmail';
|
||||
|
||||
const testEmail = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/testChannel', {
|
||||
name: props.name,
|
||||
email_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
to: props.to,
|
||||
html: props.html,
|
||||
headers: props.headers,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default testEmail;
|
||||
33
frontend/src/api/channels/testMsTeams.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createMsTeams';
|
||||
|
||||
const testMsTeams = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/testChannel', {
|
||||
name: props.name,
|
||||
msteamsv2_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
webhook_url: props.webhook_url,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default testMsTeams;
|
||||
36
frontend/src/api/channels/testOpsgenie.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createOpsgenie';
|
||||
|
||||
const testOpsgenie = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/testChannel', {
|
||||
name: props.name,
|
||||
opsgenie_configs: [
|
||||
{
|
||||
api_key: props.api_key,
|
||||
description: props.description,
|
||||
priority: props.priority,
|
||||
message: props.message,
|
||||
details: {
|
||||
...props.detailsArray,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default testOpsgenie;
|
||||
41
frontend/src/api/channels/testPager.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createPager';
|
||||
|
||||
const testPager = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/testChannel', {
|
||||
name: props.name,
|
||||
pagerduty_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
routing_key: props.routing_key,
|
||||
client: props.client,
|
||||
client_url: props.client_url,
|
||||
description: props.description,
|
||||
severity: props.severity,
|
||||
class: props.class,
|
||||
component: props.component,
|
||||
group: props.group,
|
||||
details: {
|
||||
...props.detailsArray,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default testPager;
|
||||
34
frontend/src/api/channels/testSlack.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createSlack';
|
||||
|
||||
const testSlack = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/testChannel', {
|
||||
name: props.name,
|
||||
slack_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
api_url: props.api_url,
|
||||
channel: props.channel,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default testSlack;
|
||||
52
frontend/src/api/channels/testWebhook.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createWebhook';
|
||||
|
||||
const testWebhook = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
let httpConfig = {};
|
||||
const username = props.username ? props.username.trim() : '';
|
||||
const password = props.password ? props.password.trim() : '';
|
||||
|
||||
if (username !== '' && password !== '') {
|
||||
httpConfig = {
|
||||
basic_auth: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
};
|
||||
} else if (username === '' && password !== '') {
|
||||
httpConfig = {
|
||||
authorization: {
|
||||
type: 'Bearer',
|
||||
credentials: password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const response = await axios.post<PayloadProps>('/testChannel', {
|
||||
name: props.name,
|
||||
webhook_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
url: props.api_url,
|
||||
http_config: httpConfig,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default testWebhook;
|
||||
@@ -431,7 +431,8 @@ export type AlertmanagertypesChannelEmailConfigDTOHeadersAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelEmailConfigDTOHeaders = AlertmanagertypesChannelEmailConfigDTOHeadersAnyOf | null;
|
||||
export type AlertmanagertypesChannelEmailConfigDTOHeaders =
|
||||
AlertmanagertypesChannelEmailConfigDTOHeadersAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelEmailConfigDTO {
|
||||
/**
|
||||
@@ -509,7 +510,8 @@ export type AlertmanagertypesChannelPagerdutyConfigDTODetailsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelPagerdutyConfigDTODetails = AlertmanagertypesChannelPagerdutyConfigDTODetailsAnyOf | null;
|
||||
export type AlertmanagertypesChannelPagerdutyConfigDTODetails =
|
||||
AlertmanagertypesChannelPagerdutyConfigDTODetailsAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelPagerdutyConfigDTO {
|
||||
/**
|
||||
@@ -582,7 +584,8 @@ export type AlertmanagertypesChannelOpsgenieConfigDTODetailsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelOpsgenieConfigDTODetails = AlertmanagertypesChannelOpsgenieConfigDTODetailsAnyOf | null;
|
||||
export type AlertmanagertypesChannelOpsgenieConfigDTODetails =
|
||||
AlertmanagertypesChannelOpsgenieConfigDTODetailsAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelOpsgenieConfigDTO {
|
||||
/**
|
||||
@@ -703,7 +706,8 @@ export type AlertmanagertypesChannelJiraConfigDTOCustomFieldsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelJiraConfigDTOCustomFields = AlertmanagertypesChannelJiraConfigDTOCustomFieldsAnyOf | null;
|
||||
export type AlertmanagertypesChannelJiraConfigDTOCustomFields =
|
||||
AlertmanagertypesChannelJiraConfigDTOCustomFieldsAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelJiraConfigDTO {
|
||||
/**
|
||||
@@ -828,7 +832,8 @@ export type AlertmanagertypesChannelIncidentIOConfigDTOMetadataAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelIncidentIOConfigDTOMetadata = AlertmanagertypesChannelIncidentIOConfigDTOMetadataAnyOf | null;
|
||||
export type AlertmanagertypesChannelIncidentIOConfigDTOMetadata =
|
||||
AlertmanagertypesChannelIncidentIOConfigDTOMetadataAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelIncidentIOConfigDTO {
|
||||
/**
|
||||
@@ -867,55 +872,6 @@ export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTy
|
||||
spec: AlertmanagertypesChannelIncidentIOConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfigDTOKind {
|
||||
telegram = 'telegram',
|
||||
}
|
||||
export interface AlertmanagertypesChannelTelegramConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
apiUrl?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
*/
|
||||
botToken: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
chatId: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
disableNotifications?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
messageThreadId?: number;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfigDTO {
|
||||
/**
|
||||
* @enum telegram
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelTelegramConfigDTO;
|
||||
}
|
||||
|
||||
export type AlertmanagertypesChannelConfigDTO =
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfigDTO
|
||||
@@ -926,8 +882,7 @@ export type AlertmanagertypesChannelConfigDTO =
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelGoogleChatConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfigDTO;
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTO;
|
||||
|
||||
export enum AlertmanagertypesChannelDefectDTO {
|
||||
none = 'none',
|
||||
@@ -947,7 +902,6 @@ export enum AlertmanagertypesChannelKindDTO {
|
||||
jira = 'jira',
|
||||
jsmops = 'jsmops',
|
||||
incidentio = 'incidentio',
|
||||
telegram = 'telegram',
|
||||
}
|
||||
export enum AlertmanagertypesChannelListOrderDTO {
|
||||
asc = 'asc',
|
||||
@@ -2508,7 +2462,7 @@ export interface ConfigSNSConfigDTO {
|
||||
topic_arn?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesTelegramReceiverConfigDTO {
|
||||
export interface ConfigTelegramConfigDTO {
|
||||
api_url?: ConfigURLType2DTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -2540,10 +2494,6 @@ export interface AlertmanagertypesTelegramReceiverConfigDTO {
|
||||
* @type boolean
|
||||
*/
|
||||
send_resolved?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2749,7 +2699,7 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
telegram_configs?: AlertmanagertypesTelegramReceiverConfigDTO[];
|
||||
telegram_configs?: ConfigTelegramConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
@@ -2896,7 +2846,7 @@ export interface AlertmanagertypesReceiverDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
telegram_configs?: AlertmanagertypesTelegramReceiverConfigDTO[];
|
||||
telegram_configs?: ConfigTelegramConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
@@ -2949,9 +2899,6 @@ export interface AuthtypesAttributeMappingDTO {
|
||||
role?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of authn provider.
|
||||
*/
|
||||
export enum AuthtypesAuthDomainConfigSAMLDTOKind {
|
||||
saml = 'saml',
|
||||
}
|
||||
@@ -2979,18 +2926,11 @@ export interface AuthtypesAuthDomainConfigSAMLDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum saml
|
||||
* @description The kind of authn provider.
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind;
|
||||
/**
|
||||
* @description The saml configuration.
|
||||
*/
|
||||
spec: AuthtypesSamlConfigDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of authn provider.
|
||||
*/
|
||||
export enum AuthtypesAuthDomainConfigGoogleDTOKind {
|
||||
google = 'google',
|
||||
}
|
||||
@@ -3039,18 +2979,11 @@ export interface AuthtypesAuthDomainConfigGoogleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum google
|
||||
* @description The kind of authn provider.
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind;
|
||||
/**
|
||||
* @description The google auth configuration.
|
||||
*/
|
||||
spec: AuthtypesGoogleConfigDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of authn provider.
|
||||
*/
|
||||
export enum AuthtypesAuthDomainConfigOIDCDTOKind {
|
||||
oidc = 'oidc',
|
||||
}
|
||||
@@ -3087,12 +3020,8 @@ export interface AuthtypesAuthDomainConfigOIDCDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum oidc
|
||||
* @description The kind of authn provider.
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind;
|
||||
/**
|
||||
* @description The oidc configuration.
|
||||
*/
|
||||
spec: AuthtypesOIDCConfigDTO;
|
||||
}
|
||||
|
||||
@@ -3151,7 +3080,8 @@ export type AuthtypesRoleMappingDTOGroupMappingsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AuthtypesRoleMappingDTOGroupMappings = AuthtypesRoleMappingDTOGroupMappingsAnyOf | null;
|
||||
export type AuthtypesRoleMappingDTOGroupMappings =
|
||||
AuthtypesRoleMappingDTOGroupMappingsAnyOf | null;
|
||||
|
||||
export interface AuthtypesRoleMappingDTO {
|
||||
/**
|
||||
@@ -3291,7 +3221,6 @@ export enum CoretypesKindDTO {
|
||||
'meter-metrics' = 'meter-metrics',
|
||||
'logs-field' = 'logs-field',
|
||||
'traces-field' = 'traces-field',
|
||||
'metrics-field' = 'metrics-field',
|
||||
}
|
||||
export enum CoretypesTypeDTO {
|
||||
user = 'user',
|
||||
@@ -3763,7 +3692,8 @@ export type CloudintegrationtypesAgentReportDTODataAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type CloudintegrationtypesAgentReportDTOData = CloudintegrationtypesAgentReportDTODataAnyOf | null;
|
||||
export type CloudintegrationtypesAgentReportDTOData =
|
||||
CloudintegrationtypesAgentReportDTODataAnyOf | null;
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
@@ -3792,12 +3722,7 @@ export interface CloudintegrationtypesAzureAccountConfigDTO {
|
||||
resourceGroups: string[];
|
||||
}
|
||||
|
||||
export enum CloudintegrationtypesGCPConnectionMethodDTO {
|
||||
manual = 'manual',
|
||||
agent = 'agent',
|
||||
}
|
||||
export interface CloudintegrationtypesGCPAccountConfigDTO {
|
||||
connectionMethod: CloudintegrationtypesGCPConnectionMethodDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -4044,10 +3969,7 @@ export interface CloudintegrationtypesCollectedMetricDTO {
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesGCPConnectionArtifactDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
cliCommand: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesConnectionArtifactDTO {
|
||||
@@ -4086,62 +4008,8 @@ export interface CloudintegrationtypesDataCollectedDTO {
|
||||
metrics?: CloudintegrationtypesCollectedMetricDTO[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type CloudintegrationtypesGCPLogsCollectionStrategyDTO = {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
sinkFilter: string;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type CloudintegrationtypesGCPMetricsCollectionStrategyDTO = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
metricNames: string[];
|
||||
} | null;
|
||||
|
||||
export interface CloudintegrationtypesGCPTelemetryCollectionStrategyDTO {
|
||||
logs: CloudintegrationtypesGCPLogsCollectionStrategyDTO | null;
|
||||
metrics: CloudintegrationtypesGCPMetricsCollectionStrategyDTO | null;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesGCPIntegrationConfigDTO {
|
||||
connectionMethod: CloudintegrationtypesGCPConnectionMethodDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
deploymentProjectId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
deploymentRegion: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
projectIds: string[];
|
||||
telemetryCollectionStrategy: CloudintegrationtypesGCPTelemetryCollectionStrategyDTO;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesGCPPostableAccountConfigDTO {
|
||||
connectionMethod: CloudintegrationtypesGCPConnectionMethodDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
deploymentProjectId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
deploymentRegion: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
projectIds: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesGettableAccountWithConnectionArtifactDTO {
|
||||
@@ -4296,7 +4164,7 @@ export interface CloudintegrationtypesGettableServicesMetadataDTO {
|
||||
export interface CloudintegrationtypesPostableAccountConfigDTO {
|
||||
aws?: CloudintegrationtypesAWSPostableAccountConfigDTO;
|
||||
azure?: CloudintegrationtypesAzureAccountConfigDTO;
|
||||
gcp?: CloudintegrationtypesGCPPostableAccountConfigDTO;
|
||||
gcp?: CloudintegrationtypesGCPAccountConfigDTO;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesPostableAccountDTO {
|
||||
@@ -4311,7 +4179,8 @@ export type CloudintegrationtypesPostableAgentCheckInDTODataAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type CloudintegrationtypesPostableAgentCheckInDTOData = CloudintegrationtypesPostableAgentCheckInDTODataAnyOf | null;
|
||||
export type CloudintegrationtypesPostableAgentCheckInDTOData =
|
||||
CloudintegrationtypesPostableAgentCheckInDTODataAnyOf | null;
|
||||
|
||||
export interface CloudintegrationtypesPostableAgentCheckInDTO {
|
||||
/**
|
||||
@@ -4439,9 +4308,9 @@ export interface CloudintegrationtypesUpdatableGCPAccountConfigDTO {
|
||||
*/
|
||||
deploymentRegion: string;
|
||||
/**
|
||||
* @type array
|
||||
* @type array,null
|
||||
*/
|
||||
projectIds: string[];
|
||||
projectIds: string[] | null;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesUpdatableAccountConfigDTO {
|
||||
@@ -4553,48 +4422,6 @@ export interface ConfigJiraConfigDTO {
|
||||
wont_fix_resolution?: string;
|
||||
}
|
||||
|
||||
export interface ConfigTelegramConfigDTO {
|
||||
api_url?: ConfigURLType2DTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
chat?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
chat_file?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
disable_notifications?: boolean;
|
||||
http_config?: ConfigHTTPClientConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
message_thread_id?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
parse_mode?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
send_resolved?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
token_file?: string;
|
||||
}
|
||||
|
||||
export interface DashboardGridItemDTO {
|
||||
content?: CommonJSONRefDTO;
|
||||
/**
|
||||
@@ -4656,9 +4483,6 @@ export interface Querybuildertypesv5TraceAggregationDTO {
|
||||
expression?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* How the upper bounds are spaced.
|
||||
*/
|
||||
export enum Querybuildertypesv5BucketOptionsLinearDTOKind {
|
||||
linear = 'linear',
|
||||
}
|
||||
@@ -4678,18 +4502,11 @@ export interface Querybuildertypesv5BucketOptionsLinearDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum linear
|
||||
* @description How the upper bounds are spaced.
|
||||
*/
|
||||
kind: Querybuildertypesv5BucketOptionsLinearDTOKind;
|
||||
/**
|
||||
* @description The evenly spaced bucket specification.
|
||||
*/
|
||||
spec: Querybuildertypesv5LinearBucketsSpecDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* How the upper bounds are spaced.
|
||||
*/
|
||||
export enum Querybuildertypesv5BucketOptionsLogDTOKind {
|
||||
log = 'log',
|
||||
}
|
||||
@@ -4704,12 +4521,8 @@ export interface Querybuildertypesv5BucketOptionsLogDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum log
|
||||
* @description How the upper bounds are spaced.
|
||||
*/
|
||||
kind: Querybuildertypesv5BucketOptionsLogDTOKind;
|
||||
/**
|
||||
* @description The logarithmic bucket specification.
|
||||
*/
|
||||
spec: Querybuildertypesv5LogBucketsSpecDTO;
|
||||
}
|
||||
|
||||
@@ -5055,7 +4868,8 @@ export type DashboardtypesLegendDTOCustomColorsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type DashboardtypesLegendDTOCustomColors = DashboardtypesLegendDTOCustomColorsAnyOf | null;
|
||||
export type DashboardtypesLegendDTOCustomColors =
|
||||
DashboardtypesLegendDTOCustomColorsAnyOf | null;
|
||||
|
||||
export interface DashboardtypesLegendDTO {
|
||||
/**
|
||||
@@ -5647,7 +5461,8 @@ export type DashboardtypesTableFormattingDTOColumnUnitsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type DashboardtypesTableFormattingDTOColumnUnits = DashboardtypesTableFormattingDTOColumnUnitsAnyOf | null;
|
||||
export type DashboardtypesTableFormattingDTOColumnUnits =
|
||||
DashboardtypesTableFormattingDTOColumnUnitsAnyOf | null;
|
||||
|
||||
export interface DashboardtypesTableFormattingDTO {
|
||||
/**
|
||||
@@ -5932,40 +5747,26 @@ export type Querybuildertypesv5BuilderQuerySpecDTO =
|
||||
| Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregationDTO
|
||||
| Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregationDTO;
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeBuilderDTOType {
|
||||
builder_query = 'builder_query',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeBuilderDTO {
|
||||
/**
|
||||
* @description The builder query specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5BuilderQuerySpecDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_query
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeBuilderAIDTOType {
|
||||
builder_ai_query = 'builder_ai_query',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeBuilderAIDTO {
|
||||
/**
|
||||
* @description The AI builder query specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregationDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_ai_query
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderAIDTOType;
|
||||
}
|
||||
@@ -6003,21 +5804,14 @@ export interface Querybuildertypesv5QueryBuilderFormulaDTO {
|
||||
order?: Querybuildertypesv5OrderByDTO[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeFormulaDTOType {
|
||||
builder_formula = 'builder_formula',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeFormulaDTO {
|
||||
/**
|
||||
* @description The formula specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5QueryBuilderFormulaDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_formula
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeFormulaDTOType;
|
||||
}
|
||||
@@ -6080,21 +5874,14 @@ export interface Querybuildertypesv5QueryBuilderTraceOperatorDTO {
|
||||
stepInterval?: Querybuildertypesv5StepDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeTraceOperatorDTOType {
|
||||
builder_trace_operator = 'builder_trace_operator',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeTraceOperatorDTO {
|
||||
/**
|
||||
* @description The trace operator specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5QueryBuilderTraceOperatorDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_trace_operator
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeTraceOperatorDTOType;
|
||||
}
|
||||
@@ -6116,10 +5903,6 @@ export interface Querybuildertypesv5PromQueryDTO {
|
||||
* @type string
|
||||
*/
|
||||
query?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
skipStepAlignment?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -6127,21 +5910,14 @@ export interface Querybuildertypesv5PromQueryDTO {
|
||||
step?: Querybuildertypesv5StepDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopePromQLDTOType {
|
||||
promql = 'promql',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopePromQLDTO {
|
||||
/**
|
||||
* @description The PromQL query specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5PromQueryDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum promql
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopePromQLDTOType;
|
||||
}
|
||||
@@ -6165,21 +5941,14 @@ export interface Querybuildertypesv5ClickHouseQueryDTO {
|
||||
query?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType {
|
||||
clickhouse_sql = 'clickhouse_sql',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO {
|
||||
/**
|
||||
* @description The ClickHouse SQL query specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5ClickHouseQueryDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum clickhouse_sql
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType;
|
||||
}
|
||||
@@ -7008,7 +6777,8 @@ export type FeaturetypesGettableFeatureDTOVariantsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type FeaturetypesGettableFeatureDTOVariants = FeaturetypesGettableFeatureDTOVariantsAnyOf | null;
|
||||
export type FeaturetypesGettableFeatureDTOVariants =
|
||||
FeaturetypesGettableFeatureDTOVariantsAnyOf | null;
|
||||
|
||||
export interface FeaturetypesGettableFeatureDTO {
|
||||
/**
|
||||
@@ -7486,7 +7256,8 @@ export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesClusterRecordDTOMeta = InframonitoringtypesClusterRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesClusterRecordDTOMeta =
|
||||
InframonitoringtypesClusterRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNodeCountsByReadinessDTO {
|
||||
/**
|
||||
@@ -7759,7 +7530,8 @@ export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesContainerRecordDTOMeta = InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesContainerRecordDTOMeta =
|
||||
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesContainerRecordDTO {
|
||||
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
|
||||
@@ -7850,7 +7622,8 @@ export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMeta = InframonitoringtypesDaemonSetRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMeta =
|
||||
InframonitoringtypesDaemonSetRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesDaemonSetRecordDTO {
|
||||
/**
|
||||
@@ -7945,7 +7718,8 @@ export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesDeploymentRecordDTOMeta = InframonitoringtypesDeploymentRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesDeploymentRecordDTOMeta =
|
||||
InframonitoringtypesDeploymentRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesDeploymentRecordDTO {
|
||||
/**
|
||||
@@ -8034,7 +7808,8 @@ export type InframonitoringtypesHostRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesHostRecordDTOMeta = InframonitoringtypesHostRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesHostRecordDTOMeta =
|
||||
InframonitoringtypesHostRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesHostRecordDTO {
|
||||
/**
|
||||
@@ -8116,7 +7891,8 @@ export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesJobRecordDTOMeta = InframonitoringtypesJobRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesJobRecordDTOMeta =
|
||||
InframonitoringtypesJobRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesJobRecordDTO {
|
||||
/**
|
||||
@@ -8234,7 +8010,8 @@ export type InframonitoringtypesNamespaceRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesNamespaceRecordDTOMeta = InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesNamespaceRecordDTOMeta =
|
||||
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
/**
|
||||
@@ -8301,7 +8078,8 @@ export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesNodeRecordDTOMeta = InframonitoringtypesNodeRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesNodeRecordDTOMeta =
|
||||
InframonitoringtypesNodeRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNodeRecordDTO {
|
||||
condition: InframonitoringtypesNodeConditionDTO;
|
||||
@@ -8372,7 +8150,8 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesPodRecordDTOMeta = InframonitoringtypesPodRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesPodRecordDTOMeta =
|
||||
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesPodRecordDTO {
|
||||
/**
|
||||
@@ -8759,7 +8538,8 @@ export type InframonitoringtypesStatefulSetRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesStatefulSetRecordDTOMeta = InframonitoringtypesStatefulSetRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesStatefulSetRecordDTOMeta =
|
||||
InframonitoringtypesStatefulSetRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesStatefulSetRecordDTO {
|
||||
/**
|
||||
@@ -8835,7 +8615,8 @@ export type InframonitoringtypesVolumeRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesVolumeRecordDTOMeta = InframonitoringtypesVolumeRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesVolumeRecordDTOMeta =
|
||||
InframonitoringtypesVolumeRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesVolumeRecordDTO {
|
||||
/**
|
||||
@@ -9953,7 +9734,8 @@ export type PrometheusMatrixSeriesSchemaDTOMetricAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusMatrixSeriesSchemaDTOMetric = PrometheusMatrixSeriesSchemaDTOMetricAnyOf | null;
|
||||
export type PrometheusMatrixSeriesSchemaDTOMetric =
|
||||
PrometheusMatrixSeriesSchemaDTOMetricAnyOf | null;
|
||||
|
||||
export interface PrometheusMatrixSeriesSchemaDTO {
|
||||
/**
|
||||
@@ -9985,7 +9767,8 @@ export type PrometheusVectorSampleSchemaDTOMetricAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusVectorSampleSchemaDTOMetric = PrometheusVectorSampleSchemaDTOMetricAnyOf | null;
|
||||
export type PrometheusVectorSampleSchemaDTOMetric =
|
||||
PrometheusVectorSampleSchemaDTOMetricAnyOf | null;
|
||||
|
||||
export interface PrometheusVectorSampleSchemaDTO {
|
||||
/**
|
||||
@@ -10395,7 +10178,8 @@ export type Querybuildertypesv5RawRowDTODataAnyOf = { [key: string]: unknown };
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type Querybuildertypesv5RawRowDTOData = Querybuildertypesv5RawRowDTODataAnyOf | null;
|
||||
export type Querybuildertypesv5RawRowDTOData =
|
||||
Querybuildertypesv5RawRowDTODataAnyOf | null;
|
||||
|
||||
export interface Querybuildertypesv5RawRowDTO {
|
||||
/**
|
||||
@@ -10466,7 +10250,8 @@ export type Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQueryAnyOf =
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQuery = Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQueryAnyOf | null;
|
||||
export type Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQuery =
|
||||
Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQueryAnyOf | null;
|
||||
|
||||
/**
|
||||
* Response from the v5 query range preview (dry-run) endpoint. For each query in the composite query, returns the underlying ClickHouse statement(s) it renders to without executing them (one per PromQL metric selector; exactly one for builder/ClickHouse/trace-operator queries), with the optional EXPLAIN ESTIMATE and granule analysis attached per statement when requested.
|
||||
@@ -10866,9 +10651,6 @@ export interface RuletypesCumulativeWindowDTO {
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of evaluation.
|
||||
*/
|
||||
export enum RuletypesEvaluationCumulativeDTOKind {
|
||||
cumulative = 'cumulative',
|
||||
}
|
||||
@@ -10876,18 +10658,11 @@ export interface RuletypesEvaluationCumulativeDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum cumulative
|
||||
* @description The kind of evaluation.
|
||||
*/
|
||||
kind: RuletypesEvaluationCumulativeDTOKind;
|
||||
/**
|
||||
* @description The cumulative window evaluation specification.
|
||||
*/
|
||||
spec: RuletypesCumulativeWindowDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of evaluation.
|
||||
*/
|
||||
export enum RuletypesEvaluationRollingDTOKind {
|
||||
rolling = 'rolling',
|
||||
}
|
||||
@@ -10906,12 +10681,8 @@ export interface RuletypesEvaluationRollingDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum rolling
|
||||
* @description The kind of evaluation.
|
||||
*/
|
||||
kind: RuletypesEvaluationRollingDTOKind;
|
||||
/**
|
||||
* @description The rolling window evaluation specification.
|
||||
*/
|
||||
spec: RuletypesRollingWindowDTO;
|
||||
}
|
||||
|
||||
@@ -11117,28 +10888,16 @@ export enum RuletypesSeasonalityDTO {
|
||||
daily = 'daily',
|
||||
weekly = 'weekly',
|
||||
}
|
||||
/**
|
||||
* The kind of threshold.
|
||||
*/
|
||||
export enum RuletypesThresholdBasicDTOKind {
|
||||
basic = 'basic',
|
||||
}
|
||||
/**
|
||||
* The basic threshold specification (array of thresholds).
|
||||
*/
|
||||
export type RuletypesThresholdBasicDTOSpec = RuletypesBasicRuleThresholdsDTO | null;
|
||||
|
||||
export interface RuletypesThresholdBasicDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum basic
|
||||
* @description The kind of threshold.
|
||||
*/
|
||||
kind: RuletypesThresholdBasicDTOKind;
|
||||
/**
|
||||
* @description The basic threshold specification (array of thresholds).
|
||||
*/
|
||||
spec: RuletypesThresholdBasicDTOSpec;
|
||||
spec: RuletypesBasicRuleThresholdsDTO | null;
|
||||
}
|
||||
|
||||
export type RuletypesRuleThresholdDataDTO = RuletypesThresholdBasicDTO;
|
||||
@@ -11801,7 +11560,8 @@ export type SpantypesSpanMapperTestSpanDTOAttributesAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributes = SpantypesSpanMapperTestSpanDTOAttributesAnyOf | null;
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributes =
|
||||
SpantypesSpanMapperTestSpanDTOAttributesAnyOf | null;
|
||||
|
||||
export type SpantypesSpanMapperTestSpanDTOResourceAnyOf = {
|
||||
[key: string]: unknown;
|
||||
@@ -11810,7 +11570,8 @@ export type SpantypesSpanMapperTestSpanDTOResourceAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOResource = SpantypesSpanMapperTestSpanDTOResourceAnyOf | null;
|
||||
export type SpantypesSpanMapperTestSpanDTOResource =
|
||||
SpantypesSpanMapperTestSpanDTOResourceAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanMapperTestSpanDTO {
|
||||
/**
|
||||
@@ -11922,7 +11683,8 @@ export type SpantypesSpanAggregationResultDTOValueAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanAggregationResultDTOValue = SpantypesSpanAggregationResultDTOValueAnyOf | null;
|
||||
export type SpantypesSpanAggregationResultDTOValue =
|
||||
SpantypesSpanAggregationResultDTOValueAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanAggregationResultDTO {
|
||||
aggregation: SpantypesSpanAggregationTypeDTO;
|
||||
@@ -12124,14 +11886,16 @@ export type SpantypesWaterfallSpanDTOAttributesAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesWaterfallSpanDTOAttributes = SpantypesWaterfallSpanDTOAttributesAnyOf | null;
|
||||
export type SpantypesWaterfallSpanDTOAttributes =
|
||||
SpantypesWaterfallSpanDTOAttributesAnyOf | null;
|
||||
|
||||
export type SpantypesWaterfallSpanDTOResourceAnyOf = { [key: string]: string };
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesWaterfallSpanDTOResource = SpantypesWaterfallSpanDTOResourceAnyOf | null;
|
||||
export type SpantypesWaterfallSpanDTOResource =
|
||||
SpantypesWaterfallSpanDTOResourceAnyOf | null;
|
||||
|
||||
export interface SpantypesWaterfallSpanDTO {
|
||||
/**
|
||||
@@ -12571,7 +12335,8 @@ export type TelemetrytypesGettableFieldKeysDTOKeysAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TelemetrytypesGettableFieldKeysDTOKeys = TelemetrytypesGettableFieldKeysDTOKeysAnyOf | null;
|
||||
export type TelemetrytypesGettableFieldKeysDTOKeys =
|
||||
TelemetrytypesGettableFieldKeysDTOKeysAnyOf | null;
|
||||
|
||||
export interface TelemetrytypesGettableFieldKeysDTO {
|
||||
/**
|
||||
|
||||
29
frontend/src/api/querySuggestions/getValueSuggestion.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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}`,
|
||||
);
|
||||
};
|
||||
@@ -165,15 +165,17 @@ function createBaseSpec(
|
||||
filter: getFilter(queryData),
|
||||
groupBy:
|
||||
queryData.groupBy?.length > 0
|
||||
? queryData.groupBy.map((item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType || '',
|
||||
fieldContext: item?.type || '',
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}))
|
||||
? queryData.groupBy.map(
|
||||
(item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType || '',
|
||||
fieldContext: item?.type || '',
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
limit:
|
||||
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||
@@ -185,12 +187,14 @@ function createBaseSpec(
|
||||
: undefined,
|
||||
order:
|
||||
queryData.orderBy?.length > 0
|
||||
? queryData.orderBy.map((order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}))
|
||||
? queryData.orderBy.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||
having: normalizeHaving(queryData.having),
|
||||
@@ -393,15 +397,17 @@ function createTraceOperatorBaseSpec(
|
||||
stepInterval: stepInterval || undefined,
|
||||
groupBy:
|
||||
groupBy?.length > 0
|
||||
? groupBy.map((item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType,
|
||||
fieldContext: item?.type,
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}))
|
||||
? groupBy.map(
|
||||
(item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType,
|
||||
fieldContext: item?.type,
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
limit:
|
||||
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||
@@ -410,24 +416,28 @@ function createTraceOperatorBaseSpec(
|
||||
offset: requestType === 'raw' || requestType === 'trace' ? offset : undefined,
|
||||
order:
|
||||
orderBy?.length > 0
|
||||
? orderBy.map((order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}))
|
||||
? orderBy.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
legend: isEmpty(legend) ? undefined : legend,
|
||||
having: normalizeHaving(having),
|
||||
selectFields: isEmpty(nonEmptySelectColumns)
|
||||
? undefined
|
||||
: nonEmptySelectColumns?.map((column: any): TelemetryFieldKey => ({
|
||||
name: column.name ?? column.key,
|
||||
fieldDataType:
|
||||
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||
signal: column?.signal ?? undefined,
|
||||
})),
|
||||
: nonEmptySelectColumns?.map(
|
||||
(column: any): TelemetryFieldKey => ({
|
||||
name: column.name ?? column.key,
|
||||
fieldDataType:
|
||||
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||
signal: column?.signal ?? undefined,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -600,12 +610,14 @@ export const prepareQueryRangePayloadV5 = (
|
||||
disabled: formulaData.disabled,
|
||||
limit: formulaData.limit ?? undefined,
|
||||
legend: isEmpty(formulaData.legend) ? undefined : formulaData.legend,
|
||||
order: formulaData.orderBy?.map((order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
})),
|
||||
order: formulaData.orderBy?.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
90
frontend/src/assets/CustomIcons/EmptyQuickFilterIcon.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
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;
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,5 +1,5 @@
|
||||
.breadcrumb {
|
||||
padding: var(--spacing-4) 16px var(--spacing-2);
|
||||
padding-left: 16px;
|
||||
|
||||
ol {
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
@@ -11,7 +10,7 @@ export type BreadcrumbItemConfig =
|
||||
route?: string;
|
||||
}
|
||||
| {
|
||||
title: ReactNode;
|
||||
title: string | null;
|
||||
isLast?: true;
|
||||
};
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--text-vanilla-100);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--text-vanilla-400);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,18 +9,18 @@
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--l2-foreground);
|
||||
color: var(--text-vanilla-400);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--text-vanilla-100);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--text-vanilla-400);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/* The space sits between the way back and the title, rather than above both of
|
||||
them, so the two read as separate steps. */
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
min-height: 26px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.divider {
|
||||
--divider-color: var(--l1-border);
|
||||
--divider-margin: 10px 0 16px 0;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ArrowLeft } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import styles from './BackHeader.module.scss';
|
||||
|
||||
export interface BackHeaderProps {
|
||||
/** Where the page came from, named as the place rather than the action. */
|
||||
backLabel: string;
|
||||
backRoute: string;
|
||||
title: ReactNode;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A way back on its own line, then the page's own title. The two rows keep the
|
||||
* title clear of whatever sits beside it, such as a copy button.
|
||||
*/
|
||||
function BackHeader({
|
||||
backLabel,
|
||||
backRoute,
|
||||
title,
|
||||
testId,
|
||||
}: BackHeaderProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.header} data-testid={testId}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<ArrowLeft size={12} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
safeNavigate(backRoute, { newTab: isModifierKeyPressed(e) });
|
||||
}}
|
||||
testId="back-link"
|
||||
>
|
||||
{backLabel}
|
||||
</Button>
|
||||
<div className={styles.title}>{title}</div>
|
||||
</div>
|
||||
<Divider className={styles.divider} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
BackHeader.defaultProps = {
|
||||
testId: undefined,
|
||||
};
|
||||
|
||||
export default BackHeader;
|
||||
@@ -9,8 +9,3 @@
|
||||
margin-inline-end: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--danger-background);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
@@ -111,11 +111,14 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
||||
|
||||
const lineChartRef = useRef<Chart>();
|
||||
|
||||
useImperativeHandle(ref, (): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
toggleGraph(graphIndex, isVisible, lineChartRef);
|
||||
},
|
||||
}));
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
toggleGraph(graphIndex, isVisible, lineChartRef);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const getGridColor = useCallback(() => {
|
||||
if (currentTheme === undefined) {
|
||||
|
||||
@@ -2,11 +2,8 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, InputNumber, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
LogViewMode,
|
||||
FontSize,
|
||||
OptionsMenuConfig,
|
||||
} from 'container/OptionsMenu/types';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
Check,
|
||||
ChevronLeft,
|
||||
@@ -178,19 +175,21 @@ function OptionsMenu({
|
||||
<div className="title">FORMAT</div>
|
||||
|
||||
<div className="menu-items">
|
||||
{items.map((item: any): JSX.Element => (
|
||||
<div
|
||||
className="item"
|
||||
key={item.label}
|
||||
onClick={(): void => handleMenuItemClick(item.key)}
|
||||
>
|
||||
<div className={cx('item-label')}>
|
||||
{item.label}
|
||||
{items.map(
|
||||
(item: any): JSX.Element => (
|
||||
<div
|
||||
className="item"
|
||||
key={item.label}
|
||||
onClick={(): void => handleMenuItemClick(item.key)}
|
||||
>
|
||||
<div className={cx('item-label')}>
|
||||
{item.label}
|
||||
|
||||
{selectedItem === item.key && <Check size={12} />}
|
||||
{selectedItem === item.key && <Check size={12} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,8 +11,13 @@
|
||||
border-top: 1px solid var(--l1-border);
|
||||
|
||||
font-family:
|
||||
Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', sans-serif;
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
|
||||
@@ -12,8 +12,13 @@ $max-recents-shown: 5;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-family:
|
||||
Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', sans-serif;
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
|
||||
.query-where-clause-editor-container {
|
||||
position: relative;
|
||||
@@ -133,7 +138,8 @@ $max-recents-shown: 5;
|
||||
// least N recents visible.
|
||||
@for $i from 1 through $max-recents-shown {
|
||||
&:has(> li:nth-of-type(#{$i}) .cm-completionIcon-recent) {
|
||||
max-height: $dropdown-base-height + $recents-header-height +
|
||||
max-height: $dropdown-base-height +
|
||||
$recents-header-height +
|
||||
($i * $recent-row-height) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
.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);
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
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']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
/* 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,
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
@@ -0,0 +1,77 @@
|
||||
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;
|
||||
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
@@ -0,0 +1,99 @@
|
||||
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;
|
||||
@@ -18,25 +18,6 @@ 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 () => {
|
||||
@@ -444,63 +425,6 @@ 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 }}
|
||||
source={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 }}
|
||||
source={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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('show more functionality', () => {
|
||||
|
||||
@@ -34,6 +34,7 @@ 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';
|
||||
@@ -309,6 +310,25 @@ 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
|
||||
@@ -320,8 +340,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
);
|
||||
case FiltersType.SLIDER:
|
||||
return <Slider key={filter.attributeKey.key} />;
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
default:
|
||||
return (
|
||||
return useFieldApis ? (
|
||||
<CheckboxV2
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
@@ -330,6 +351,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
onQuickFilterChange={onQuickFilterChange}
|
||||
useFieldApis={useFieldApis}
|
||||
/>
|
||||
) : (
|
||||
<Checkbox
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
onQuickFilterChange={onQuickFilterChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
@@ -411,4 +440,5 @@ QuickFilters.defaultProps = {
|
||||
config: [],
|
||||
showFilterCollapse: true,
|
||||
showQueryName: true,
|
||||
useFieldApis: undefined,
|
||||
};
|
||||
|
||||
@@ -63,7 +63,6 @@ jest.mock('../../QuickFilters', () => ({
|
||||
|
||||
const quickFilterProps = {
|
||||
source: QuickFiltersSource.TRACES_EXPLORER,
|
||||
useFieldApis: { startUnixMilli: 0, endUnixMilli: 0 },
|
||||
};
|
||||
const savedViewProps = { source: SavedviewtypesSourceDTO.traces };
|
||||
|
||||
|
||||
@@ -11,13 +11,11 @@ import {
|
||||
import { rest, type RequestHandler } from 'msw';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import {
|
||||
fieldKeysResponse,
|
||||
fieldValuesResponse,
|
||||
} from '@/storybook/msw/__story_mockdata__/fields';
|
||||
import { attributeValuesResponse } from '@/storybook/msw/__story_mockdata__/attributes';
|
||||
import { fieldKeysResponse } from '@/storybook/msw/__story_mockdata__/fields';
|
||||
import { quickFiltersResponse } from '@/storybook/msw/__story_mockdata__/quickFilters';
|
||||
|
||||
import { FiltersType, QuickFilterCheckboxUseFieldApis } from '../types';
|
||||
import { FiltersType } from '../types';
|
||||
|
||||
const customFilters = [
|
||||
{
|
||||
@@ -63,15 +61,13 @@ export const checkboxConfig = [
|
||||
},
|
||||
];
|
||||
|
||||
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 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 handlers = [
|
||||
@@ -86,7 +82,7 @@ export const handlers = [
|
||||
rest.get('http://localhost/api/v1/fields/keys', (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(fieldKeysResponse(['k8s.namespace.name']))),
|
||||
),
|
||||
fieldValuesHandler(['checkout', 'frontend', 'payments']),
|
||||
attributeValuesHandler(['checkout', 'frontend', 'payments']),
|
||||
];
|
||||
|
||||
export const loadingFiltersHandlers = [
|
||||
|
||||
@@ -7,9 +7,8 @@ import type { GlobalMockArgs } from '@/storybook/globals';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import {
|
||||
attributeValuesHandler,
|
||||
checkboxConfig,
|
||||
fieldApis,
|
||||
fieldValuesHandler,
|
||||
handlers,
|
||||
LONG_FILTER_VALUES,
|
||||
loadingFiltersHandlers,
|
||||
@@ -35,7 +34,6 @@ const meta = {
|
||||
handleFilterVisibilityChange: (): void => undefined,
|
||||
signal: SignalType.LOGS,
|
||||
source: QuickFiltersSource.LOGS_EXPLORER,
|
||||
useFieldApis: fieldApis,
|
||||
},
|
||||
parameters: {
|
||||
msw: { handlers },
|
||||
@@ -116,7 +114,7 @@ export const SelectedExpandedCheckbox: Story = {
|
||||
export const Tooltips: TooltipsStory = {
|
||||
args: { signal: undefined, tooltipsOpen: true },
|
||||
parameters: {
|
||||
msw: { handlers: [fieldValuesHandler(LONG_FILTER_VALUES), ...handlers] },
|
||||
msw: { handlers: [attributeValuesHandler(LONG_FILTER_VALUES), ...handlers] },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import {
|
||||
otherFiltersResponse,
|
||||
quickFiltersFieldValuesResponse,
|
||||
quickFiltersAttributeValuesResponse,
|
||||
quickFiltersListResponse,
|
||||
} from 'mocks-server/__mockdata__/customQuickFilters';
|
||||
import { server } from 'mocks-server/server';
|
||||
@@ -25,7 +25,7 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from '../types';
|
||||
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
|
||||
import { QuickFiltersConfig } from './constants';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
@@ -38,6 +38,7 @@ const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
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;
|
||||
@@ -70,8 +71,11 @@ 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(quickFiltersFieldValuesResponse)),
|
||||
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -87,7 +91,6 @@ function renderWithSignal(): void {
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
signal={SIGNAL}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
@@ -98,7 +101,6 @@ function renderStaticConfig(): void {
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
config={QuickFiltersConfig}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
otherFiltersResponse,
|
||||
quickFiltersFieldValuesResponse,
|
||||
quickFiltersAttributeValuesResponse,
|
||||
quickFiltersListResponse,
|
||||
} from 'mocks-server/__mockdata__/customQuickFilters';
|
||||
import { server } from 'mocks-server/server';
|
||||
@@ -17,7 +17,7 @@ import '@testing-library/jest-dom';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
|
||||
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
|
||||
import { QuickFiltersConfig } from './constants';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
@@ -37,6 +37,7 @@ const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
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';
|
||||
@@ -61,8 +62,11 @@ 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(quickFiltersFieldValuesResponse)),
|
||||
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -79,7 +83,6 @@ function TestQuickFilters({
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
config={config}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
signal={signal}
|
||||
/>
|
||||
);
|
||||
@@ -102,7 +105,6 @@ function TestQuickFiltersApiMonitoring({
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
config={config}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
signal={signal}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { FiltersType, QuickFilterCheckboxUseFieldApis } from '../types';
|
||||
import { FiltersType } from '../types';
|
||||
|
||||
export const QuickFiltersConfig = [
|
||||
{
|
||||
@@ -24,9 +24,3 @@ export const QuickFiltersConfig = [
|
||||
defaultOpen: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const QuickFiltersFieldApis: QuickFilterCheckboxUseFieldApis = {
|
||||
startUnixMilli: 1700000000000,
|
||||
endUnixMilli: 1700003600000,
|
||||
existingQuery: null,
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface IQuickFiltersProps {
|
||||
className?: string;
|
||||
showFilterCollapse?: boolean;
|
||||
showQueryName?: boolean;
|
||||
useFieldApis: QuickFilterCheckboxUseFieldApis;
|
||||
useFieldApis?: QuickFilterCheckboxUseFieldApis;
|
||||
savedViewsHeader?: ReactNode;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,9 @@ export enum QuickFiltersSource {
|
||||
AI_OBSERVABILITY = 'ai-observability',
|
||||
}
|
||||
|
||||
/**
|
||||
* Opt-in: fetch values from the /v1/fields/values API instead of /v3/autocomplete/attribute_values
|
||||
*/
|
||||
export type QuickFilterCheckboxUseFieldApis = {
|
||||
startUnixMilli: number;
|
||||
endUnixMilli: number;
|
||||
|
||||
@@ -13,22 +13,26 @@ function LabelColumn({ labels, value }: LabelColumnProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="label-column">
|
||||
{newLabels.map((label: string): JSX.Element => (
|
||||
<BadgeWithTooltip key={label} label={label} value={value} />
|
||||
))}
|
||||
{newLabels.map(
|
||||
(label: string): JSX.Element => (
|
||||
<BadgeWithTooltip key={label} label={label} value={value} />
|
||||
),
|
||||
)}
|
||||
{remainingLabels.length > 0 && (
|
||||
<Popover
|
||||
placement="bottomRight"
|
||||
showArrow={false}
|
||||
content={
|
||||
<div>
|
||||
{labels.map((label: string): JSX.Element => (
|
||||
<div key={label}>
|
||||
<Badge textTransform="none" variant="solid" color="secondary">
|
||||
{getLabelAndValueContent(label, value && value[label])}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
{labels.map(
|
||||
(label: string): JSX.Element => (
|
||||
<div key={label}>
|
||||
<Badge textTransform="none" variant="solid" color="secondary">
|
||||
{getLabelAndValueContent(label, value && value[label])}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
trigger="hover"
|
||||
|
||||
@@ -2,7 +2,6 @@ import { TanStackHoverTooltip } from './TanStackHoverTooltip';
|
||||
import { TanStackTableBase } from './TanStackTable';
|
||||
import TanStackTableText from './TanStackTableText';
|
||||
|
||||
export { default as ColumnSelector } from './ColumnSelector';
|
||||
export * from './TanStackHoverTooltip';
|
||||
export * from './TanStackTableStateContext';
|
||||
export * from './types';
|
||||
|
||||
@@ -50,11 +50,14 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
const onCreateRef = useRef(onCreate);
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
|
||||
useImperativeHandle(ref, (): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
chartRef.current?.setSeries(graphIndex, { show: isVisible });
|
||||
},
|
||||
}));
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
chartRef.current?.setSeries(graphIndex, { show: isVisible });
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onCreateRef.current = onCreate;
|
||||
|
||||
@@ -511,7 +511,7 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
||||
};
|
||||
|
||||
export enum OperatorConfigKeys {
|
||||
EXCEPTIONS = 'EXCEPTIONS',
|
||||
'EXCEPTIONS' = 'EXCEPTIONS',
|
||||
}
|
||||
|
||||
export const OPERATORS_CONFIG = {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const REACT_QUERY_KEY = {
|
||||
GET_ALL_ALERTS: 'GET_ALL_ALERTS',
|
||||
ALERT_RULES_CHART_PREVIEW: 'ALERT_RULES_CHART_PREVIEW',
|
||||
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
|
||||
CLONE_ALERT_RULE: 'CLONE_ALERT_RULE',
|
||||
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
|
||||
GET_HOST_LIST: 'GET_HOST_LIST',
|
||||
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
|
||||
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
|
||||
|
||||
@@ -2,7 +2,6 @@ export const GlobalShortcuts = {
|
||||
NavigateToServices: 'shift+s',
|
||||
NavigateToDashboards: 'shift+d',
|
||||
NavigateToAlerts: 'shift+a',
|
||||
NavigateToNotificationChannels: 'shift+g+n',
|
||||
NavigateToExceptions: 'shift+e',
|
||||
NavigateToMessagingQueues: 'shift+q',
|
||||
ToggleSidebar: 'shift+b',
|
||||
@@ -24,6 +23,7 @@ export const GlobalShortcuts = {
|
||||
NavigateToSettings: 'shift+g',
|
||||
NavigateToSettingsIngestion: 'shift+g+i',
|
||||
NavigateToSettingsBilling: 'shift+g+b',
|
||||
NavigateToSettingsNotificationChannels: 'shift+g+n',
|
||||
NavigateToSettingsServiceAccounts: 'shift+g+k',
|
||||
NavigateToSettingsRoles: 'shift+g+r',
|
||||
NavigateToSettingsMembers: 'shift+g+m',
|
||||
@@ -34,7 +34,6 @@ export const GlobalShortcutsName = {
|
||||
NavigateToTraces: 'shift+t',
|
||||
NavigateToDashboards: 'shift+d',
|
||||
NavigateToAlerts: 'shift+a',
|
||||
NavigateToNotificationChannels: 'shift+g+n',
|
||||
NavigateToExceptions: 'shift+e',
|
||||
NavigateToMessagingQueues: 'shift+q',
|
||||
ToggleSidebar: 'shift+b',
|
||||
@@ -45,6 +44,7 @@ export const GlobalShortcutsName = {
|
||||
NavigateToSettings: 'shift+g',
|
||||
NavigateToSettingsIngestion: 'shift+g+i',
|
||||
NavigateToSettingsBilling: 'shift+g+b',
|
||||
NavigateToSettingsNotificationChannels: 'shift+g+n',
|
||||
NavigateToSettingsServiceAccounts: 'shift+g+k',
|
||||
NavigateToSettingsRoles: 'shift+g+r',
|
||||
NavigateToSettingsMembers: 'shift+g+m',
|
||||
@@ -59,7 +59,6 @@ export const GlobalShortcutsDescription = {
|
||||
NavigateToLogs: 'Navigate to Logs Explorer',
|
||||
NavigateToDashboards: 'Navigate to Dashboards List',
|
||||
NavigateToAlerts: 'Navigate to Alerts List',
|
||||
NavigateToNotificationChannels: 'Navigate to Notification Channels',
|
||||
NavigateToExceptions: 'Navigate to Exceptions List',
|
||||
NavigateToMessagingQueues: 'Navigate to Messaging Queues',
|
||||
ToggleSidebar: 'Toggle sidebar visibility',
|
||||
@@ -69,6 +68,8 @@ export const GlobalShortcutsDescription = {
|
||||
NavigateToSettings: 'Navigate to Settings',
|
||||
NavigateToSettingsIngestion: 'Navigate to Ingestion Settings',
|
||||
NavigateToSettingsBilling: 'Navigate to Billing Settings',
|
||||
NavigateToSettingsNotificationChannels:
|
||||
'Navigate to Notification Channels Settings',
|
||||
NavigateToLogsPipelines: 'Navigate to Logs Pipelines',
|
||||
NavigateToSettingsServiceAccounts: 'Navigate to Service Accounts Settings',
|
||||
NavigateToSettingsRoles: 'Navigate to Roles Settings',
|
||||
|
||||
77
frontend/src/container/AllAlertChannels/AlertChannels.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Button } from 'antd';
|
||||
import type { ColumnsType } from 'antd/lib/table';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
|
||||
import Delete from './Delete';
|
||||
|
||||
function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
const { t } = useTranslation(['channels']);
|
||||
const { notifications } = useNotifications();
|
||||
const { user } = useAppContext();
|
||||
const [action] = useComponentPermission(['new_alert_action'], user.role);
|
||||
|
||||
const onClickEditHandler = useCallback((id: string) => {
|
||||
history.push(
|
||||
generatePath(ROUTES.CHANNELS_EDIT, {
|
||||
channelId: id,
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const columns: ColumnsType<Channels> = [
|
||||
{
|
||||
title: t('column_channel_name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: t('column_channel_type'),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 80,
|
||||
},
|
||||
];
|
||||
|
||||
if (action) {
|
||||
columns.push({
|
||||
title: t('column_channel_action'),
|
||||
dataIndex: 'id',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: (id: string): JSX.Element => (
|
||||
<>
|
||||
<Button onClick={(): void => onClickEditHandler(id)} type="link">
|
||||
{t('column_channel_edit')}
|
||||
</Button>
|
||||
<Delete id={id} notifications={notifications} />
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={allChannels}
|
||||
rowKey="id"
|
||||
bordered
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface AlertChannelsProps {
|
||||
allChannels: Channels[];
|
||||
}
|
||||
|
||||
export default AlertChannels;
|
||||
@@ -0,0 +1,4 @@
|
||||
.alert-channels-container {
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-8);
|
||||
}
|
||||
54
frontend/src/container/AllAlertChannels/Delete.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from 'antd';
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import deleteChannel from 'api/channels/delete';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function Delete({ notifications, id }: DeleteProps): JSX.Element {
|
||||
const { t } = useTranslation(['channels']);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const onClickHandler = async (): Promise<void> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await deleteChannel({
|
||||
id,
|
||||
});
|
||||
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('channel_delete_success'),
|
||||
});
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries(['getChannels']);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as APIError).getErrorCode(),
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
type="link"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
interface DeleteProps {
|
||||
notifications: NotificationInstance;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default Delete;
|
||||
@@ -0,0 +1,84 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
const successNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
success: successNotification,
|
||||
error: jest.fn(),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Alert Channels Settings List page', () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2023-10-20'));
|
||||
render(<AlertChannels />);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('sending_channels_note')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
describe('Should display the Alert Channels page properly', () => {
|
||||
it('Should check if "The alerts will be sent to all the configured channels." is visible', () => {
|
||||
expect(screen.getByText('sending_channels_note')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if "New Alert Channel" Button is visble', () => {
|
||||
expect(screen.getByText('button_new_channel')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if the help icon is visible and displays "tooltip_notification_channels', async () => {
|
||||
const helpIcon = screen.getByRole('img', { name: /help/i });
|
||||
|
||||
fireEvent.mouseOver(helpIcon);
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltip = screen.getByText('tooltip_notification_channels');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Should check if the channels table is properly displayed', () => {
|
||||
it('Should check if the table columns are properly displayed', () => {
|
||||
expect(screen.getByText('column_channel_name')).toBeInTheDocument();
|
||||
expect(screen.getByText('column_channel_type')).toBeInTheDocument();
|
||||
expect(screen.getByText('column_channel_action')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if the data in the table is displayed properly', () => {
|
||||
expect(screen.getByText('Dummy-Channel')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('slack')[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByText('column_channel_edit')[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Delete')[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if clicking on Delete displays Success Toast "Channel Deleted Successfully"', async () => {
|
||||
const deleteButton = screen.getAllByRole('button', { name: 'Delete' })[0];
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(deleteButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(successNotification).toHaveBeenCalledWith({
|
||||
message: 'Success',
|
||||
description: 'channel_delete_success',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||