mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-29 12:30:42 +00:00
Compare commits
19 Commits
feat/scatt
...
docs/e2e-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a0f4df6bd | ||
|
|
5097d1578f | ||
|
|
5b5b9794f9 | ||
|
|
1957e5a112 | ||
|
|
18eb8ca849 | ||
|
|
3b34a0d951 | ||
|
|
0036cda4f1 | ||
|
|
665e70b17a | ||
|
|
450ceed5fd | ||
|
|
63d0470894 | ||
|
|
c69b77e030 | ||
|
|
4daf425a19 | ||
|
|
6f2ee41dcb | ||
|
|
8ab8c63bea | ||
|
|
0513ab7f1f | ||
|
|
83b4754eaa | ||
|
|
120da85b49 | ||
|
|
b57e5e8e09 | ||
|
|
ba155067e7 |
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: playwright-test-generator
|
||||
description: Use this agent to convert a SigNoz E2E test plan into Playwright spec files under `tests/e2e/tests/<feature>/`. Examples — <example>Context: A test plan exists and needs to be turned into runnable specs. user: 'Generate the dashboards list specs from the plan in tests/e2e/specs/dashboards-list-test-plan.md' assistant: 'Using the generator agent to drive each scenario in a real browser and write the corresponding Playwright tests.'</example>
|
||||
tools: Glob, Grep, Read, Bash, mcp__playwright-test__browser_click, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_verify_element_visible, mcp__playwright-test__browser_verify_list_visible, mcp__playwright-test__browser_verify_text_visible, mcp__playwright-test__browser_verify_value, mcp__playwright-test__browser_wait_for, mcp__playwright-test__generator_read_log, mcp__playwright-test__generator_setup_page, mcp__playwright-test__generator_write_test
|
||||
description: Use this agent to convert a SigNoz E2E test plan into Playwright spec files under `tests/e2e/tests/<feature>/`, adding or extending the feature's driver under `tests/e2e/drivers/`. Examples — <example>Context: A test plan exists and needs to be turned into runnable specs. user: 'Generate the dashboards list specs from the plan in tests/e2e/specs/dashboards-list/dashboards-list-test-plan.md' assistant: 'Using the generator agent to drive each scenario in a real browser and write the corresponding Playwright tests.'</example>
|
||||
tools: Glob, Grep, Read, Write, Edit, Bash, mcp__playwright-test__browser_click, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_verify_element_visible, mcp__playwright-test__browser_verify_list_visible, mcp__playwright-test__browser_verify_text_visible, mcp__playwright-test__browser_verify_value, mcp__playwright-test__browser_wait_for, mcp__playwright-test__generator_read_log, mcp__playwright-test__generator_setup_page, mcp__playwright-test__generator_write_test
|
||||
model: sonnet
|
||||
color: blue
|
||||
---
|
||||
@@ -10,17 +10,23 @@ You are the Playwright Test Generator for the SigNoz frontend. You take a plan w
|
||||
|
||||
# Repo conventions you must follow
|
||||
|
||||
- **Spec location:** `tests/e2e/tests/<feature>/<spec-name>.spec.ts`. One file per resource; cross-resource concerns get their own file. Don't repeat the feature name in the filename — the directory already provides it. `dashboards/list.spec.ts`, not `dashboards/dashboards-list.spec.ts`.
|
||||
- **Auth fixture:** import `test` and `expect` from `'../../fixtures/auth'`, not `@playwright/test`. Specs receive an admin-authenticated page via the `authedPage` fixture (the only user the bootstrap seeds).
|
||||
- **Spec location:** `tests/e2e/tests/<feature>/<spec-name>.spec.ts`. One file per resource; cross-resource concerns get their own file. Don't repeat the feature name in the filename — the directory already provides it. `dashboards/list/search.spec.ts`, not `dashboards/list/dashboards-list-search.spec.ts`.
|
||||
- **Drivers first.** New specs import `test`, `expect` and constants from one driver module in [tests/e2e/drivers/](../../tests/e2e/drivers/) and receive the feature's driver as a fixture. A spec never passes `page` around.
|
||||
```ts
|
||||
import { test, expect } from '../../fixtures/auth';
|
||||
import { expect, PATH, test } from '../../drivers/routing';
|
||||
|
||||
test('TC-01 alerts page — tabs render', async ({ authedPage: page }) => {
|
||||
await page.goto('/alerts');
|
||||
await expect(page.getByRole('tab', { name: /alert rules/i })).toBeVisible();
|
||||
test('TC-09 RouteTab switches the url', async ({ routing }) => {
|
||||
await routing.goto(PATH.metricsSummary);
|
||||
await expect(routing.activeRouteTab(PATH.metricsSummary)).toBeVisible();
|
||||
});
|
||||
```
|
||||
- **Extended fixtures:** For features needing complex setup (seeded data, API calls, cleanup), import from domain-specific fixtures that extend `auth`. See [docs/contributing/tests/e2e.md](../../docs/contributing/tests/e2e.md) for the full pattern.
|
||||
- A driver is a function closing over the page that returns `Locator` getters and async actions. Actions wait inside themselves (e.g. `run()` pairs the click with `waitForResponse(/query_range/)`), so specs don't.
|
||||
- `driver(name, build, dispose?)` in `drivers/kit.ts` makes it a test-scoped fixture on `authedPage`; `dispose` deletes what it seeded. `seeded(name, open, close)` makes a worker-scoped seed.
|
||||
- Layers compose: `withNewPanel(withEditor(panelsTest))`. Extend the closest existing driver instead of starting a parallel one.
|
||||
- When no driver covers the feature, add `drivers/<feature>.ts` with the spec. Locators live in the driver, not the spec. `drivers/` is AI-owned: keep the banner, and change a driver only alongside a spec that needs it.
|
||||
- Read [tests/e2e/tests/dashboards/editor/query.spec.ts](../../tests/e2e/tests/dashboards/editor/query.spec.ts) and [drivers/panel-editor.ts](../../tests/e2e/drivers/panel-editor.ts) for the canonical shape.
|
||||
- **Plain auth fixture** only for a spec no driver fits: import `test` and `expect` from `'../../fixtures/auth'` (never `@playwright/test`) and use the `authedPage` fixture (the admin, the only user the bootstrap seeds).
|
||||
- **Extended fixtures:** older suites import domain fixtures that extend `auth`. See [docs/contributing/tests/e2e.md](../../docs/contributing/tests/e2e.md).
|
||||
- `fixtures/alerts/alert-rules` — worker-scoped rule list + test-scoped rule factory
|
||||
- `fixtures/alerts/alert-history` — extends alert-rules, adds history fixtures (waits on ruler evaluation)
|
||||
```ts
|
||||
@@ -31,15 +37,18 @@ You are the Playwright Test Generator for the SigNoz frontend. You take a plan w
|
||||
import { test, expect } from '../../../fixtures/alerts/alert-history';
|
||||
```
|
||||
- **Test titles:** `TC-NN <short description>` — matches the planner's IDs.
|
||||
- **Self-contained state.** The bootstrap creates a fresh stack with **zero** dashboards / alerts / etc. — never assume pre-existing data. Two cleanup shapes are valid; pick based on the spec size:
|
||||
- **Per-test `try / finally`** — small specs (~ <10 scenarios) where each test owns its data.
|
||||
- **Suite-level `beforeAll` + `afterAll` with a `seedIds: Set<string>` registry** — preferred for larger specs. Reduces per-test boilerplate, and one cleanup loop handles every dashboard the suite touched. See [tests/e2e/tests/dashboards/list.spec.ts](../../tests/e2e/tests/dashboards/list.spec.ts) for the canonical shape.
|
||||
- **Reuse helpers from `tests/e2e/helpers/`.** Don't reinvent. The current set:
|
||||
- [`helpers/auth.ts`](../../tests/e2e/helpers/auth.ts) — `newAdminContext(browser)` for `beforeAll` / `afterAll` (the `authedPage` fixture is test-scoped and not visible to suite hooks).
|
||||
- [`helpers/dashboards.ts`](../../tests/e2e/helpers/dashboards.ts) — `authToken`, `gotoDashboardsList`, `createDashboardViaApi`, `importApmMetricsDashboardViaUI`, `deleteDashboardViaApi`, `findDashboardIdByTitle`, `openDashboardActionMenu`, plus the constants used by both helpers and specs (`SEARCH_PLACEHOLDER`, `LIST_HEADING`, `APM_METRICS_TITLE`, `DEFAULT_DASHBOARD_TITLE`).
|
||||
- **Seed via API when the UI flow is multi-step or brittle.** Implementation lives in `createDashboardViaApi` — use it. `page.request.*` does **not** auto-attach `Authorization`; the helpers handle that for you. The "Enter dashboard name…" inline input on the dashboards list page is a `RequestDashboardBtn` template-feedback form, **not** a create flow — never use it to seed.
|
||||
- **Reusable JSON fixtures live in [tests/e2e/fixtures/](../../tests/e2e/fixtures/).** `apm-metrics.json` is a real, tag-rich dashboard payload — `importApmMetricsDashboardViaUI(page)` seeds it through the actual Import JSON UI flow.
|
||||
- **Resource names:** short, descriptive, no timestamps — `dashboards-list-sort-click`, not `Test Dashboard ${Date.now()}`. Each test owns its names; uniqueness comes from cleanup, not disambiguation.
|
||||
- **Self-contained state.** The bootstrap creates a fresh stack with **zero** dashboards / alerts / etc. — never assume pre-existing resources. Cleanup shapes, in order of preference:
|
||||
- **A driver's `dispose`** — the driver tracks what it seeded and deletes it after the test (`dashboards.seed(...)` in `drivers/dashboards.ts`). The spec has no cleanup code.
|
||||
- **A worker-scoped `seeded(...)` layer** — read-only data shared by a worker's tests.
|
||||
- **Per-test `try / finally`** or **suite-level `afterAll` with an id registry** — specs without a driver. Suite hooks can't see `authedPage`; use `newAdminContext(browser)` from `helpers/auth.ts`.
|
||||
- **Golden telemetry is pre-seeded.** `bootstrap/global.setup.ts` loads logs, traces and metrics over the last 6h (constants in `GOLDEN`, `helpers/dashboard-spec.ts`). Query within ≤ 6h and assert structure (series, rows, request payload), never exact counts: per-test seeds from parallel workers land in the same tenant. Seed missing shapes through the seeder with unique service names (`helpers/alerts/seeding.ts`).
|
||||
- **Reuse helpers from `tests/e2e/helpers/`** inside drivers. Don't reinvent:
|
||||
- [`helpers/common.ts`](../../tests/e2e/helpers/common.ts) — `authToken`, `watchConsole`, `collectRequests`, `requestUrl`, `seederUrl`.
|
||||
- [`helpers/auth.ts`](../../tests/e2e/helpers/auth.ts) — `newAdminContext(browser)`.
|
||||
- [`helpers/query-range-mock.ts`](../../tests/e2e/helpers/query-range-mock.ts) — `query_range` mocks for error / no-data / warning / loading states only; install before `goto`.
|
||||
- **Seed via API when the UI flow is multi-step or brittle**, through the driver (`dashboards.seed`, `seedAndOpen`, `seedAndEdit`). `page.request.*` does **not** auto-attach `Authorization`; drivers add `authToken(page)`.
|
||||
- **Static payloads live in [tests/e2e/testdata/](../../tests/e2e/testdata/)** (`dashboards/panels.ts`, `traces/large-trace.json`).
|
||||
- **Resource names:** short and descriptive, no timestamps. Where parallel workers share a list, add a random suffix as `uniqueName` in `drivers/dashboard-list.ts` does.
|
||||
- **Serial mode** when tests in a file mutate the same list page:
|
||||
```ts
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
@@ -64,13 +73,14 @@ For each scenario in the plan:
|
||||
- For verifications, use the dedicated `browser_verify_*` tools — they capture the assertion as Playwright code in the log.
|
||||
4. **Read the log.** Call `generator_read_log` immediately after the last step. Don't intersperse other tool calls.
|
||||
5. **Write the spec.** Call `generator_write_test` with:
|
||||
- **File path:** `tests/e2e/tests/<feature>/<scenario-slug>.spec.ts` — fs-friendly slug from the scenario title. Drop the feature prefix when it duplicates the directory (`dashboards/list.spec.ts`, not `dashboards/dashboards-list.spec.ts`).
|
||||
- **File path:** `tests/e2e/tests/<feature>/<scenario-slug>.spec.ts` — fs-friendly slug from the scenario title. Drop the feature prefix when it duplicates the directory (`dashboards/list/search.spec.ts`, not `dashboards/list/dashboards-list-search.spec.ts`).
|
||||
- **Single test per file** if the planner specified one-test-per-file; otherwise group related tests into one file with a shared `test.describe('<Feature>', () => { … })`.
|
||||
- **`describe` block** matches the top-level plan section.
|
||||
- **Title** matches `TC-NN <description>` exactly.
|
||||
- **Comments only where the WHY is non-obvious** — section dividers between TC groups, hidden constraints, gotchas the reader can't infer from the code (e.g. "Monaco swallows Escape — click the title to blur first"). **Do not narrate steps** by pasting the plan's bullets back as `// 1. Navigate…` `// 2. Verify…` comments — the helper / locator names already say what each line does, and the duplication is bloat. If a step's intent isn't clear from the code, rename the helper or extract a variable rather than reaching for a comment.
|
||||
- **Imports** from `../../fixtures/auth`. **Do not** import from `@playwright/test` directly.
|
||||
- **Try / finally** cleanup using the API (delete the resources you seeded).
|
||||
- **Imports** from the feature's driver module (or `../../fixtures/auth` when none fits). **Do not** import from `@playwright/test` directly.
|
||||
- **Cleanup** through the driver's `dispose`; `try / finally` only without a driver.
|
||||
- **Move recorded locators and waits into the driver.** The log gives raw `page.*` calls; the spec should read as driver actions.
|
||||
|
||||
# Quality bar — what to write, what to skip
|
||||
|
||||
@@ -88,81 +98,57 @@ When you cut, merge, or renumber TCs vs the plan, note it in your final summary.
|
||||
For a plan section:
|
||||
|
||||
```markdown
|
||||
### 1. Page Load
|
||||
### 1. Search
|
||||
|
||||
#### TC-01 page chrome and core controls render
|
||||
#### TC-01 searching by name writes the query to the URL and lists the match
|
||||
**Preconditions:** one dashboard with a unique name (seed via the `dashboards` driver).
|
||||
**Steps:**
|
||||
1. Navigate to `/dashboard`
|
||||
2. Verify the page title is "SigNoz | All Dashboards"
|
||||
3. Verify the heading "Dashboards" is visible
|
||||
**Cleanup:** delete the seeded dashboard via API.
|
||||
1. Open the dashboards list.
|
||||
2. Search `name CONTAINS '<name>'`.
|
||||
**Expected:** the `query` URL param carries the search; only the seeded dashboard is listed.
|
||||
```
|
||||
|
||||
You produce (suite-level shape, preferred for files with multiple scenarios):
|
||||
You produce:
|
||||
|
||||
```ts
|
||||
// tests/e2e/tests/dashboards/list.spec.ts
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from '../../fixtures/auth';
|
||||
import { newAdminContext } from '../../helpers/auth';
|
||||
// tests/e2e/tests/dashboards/list/search.spec.ts
|
||||
import {
|
||||
authToken,
|
||||
createDashboardViaApi,
|
||||
deleteDashboardViaApi,
|
||||
gotoDashboardsList,
|
||||
} from '../../helpers/dashboards';
|
||||
expect,
|
||||
listDashboard,
|
||||
nameContains,
|
||||
test,
|
||||
uniqueName,
|
||||
} from '../../../drivers/dashboard-list';
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
const seedIds = new Set<string>();
|
||||
|
||||
async function seed(page: Page, title: string): Promise<string> {
|
||||
const id = await createDashboardViaApi(page, title);
|
||||
seedIds.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
if (seedIds.size === 0) return;
|
||||
const ctx = await newAdminContext(browser);
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
const token = await authToken(page);
|
||||
for (const id of [...seedIds]) {
|
||||
await deleteDashboardViaApi(ctx.request, id, token);
|
||||
seedIds.delete(id);
|
||||
}
|
||||
} finally {
|
||||
await ctx.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('Dashboards List Page', () => {
|
||||
test('TC-01 page chrome and core controls render', async ({
|
||||
test.describe('Dashboards — list search', () => {
|
||||
test('TC-01 searching by name writes the query to the URL and lists the match', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
dashboardList,
|
||||
}) => {
|
||||
await seed(page, 'list-chrome');
|
||||
const name = uniqueName('search');
|
||||
await dashboards.seed(listDashboard(name));
|
||||
await dashboardList.open();
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
await dashboardList.runSearch(nameContains(name));
|
||||
|
||||
await expect(page).toHaveTitle('SigNoz | All Dashboards');
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Dashboards', level: 1 }),
|
||||
).toBeVisible();
|
||||
expect(new URL(page.url()).searchParams.get('query')).toBe(
|
||||
nameContains(name),
|
||||
);
|
||||
await expect(dashboardList.rows()).toHaveText([name]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Note how the example carries no `// 1. …` `// 2. …` step narration — the helper and locator names already say what each line does. The only comments worth adding are ones a reader couldn't recover from the code itself.
|
||||
Note: no cleanup code (the `dashboards` driver deletes its seed), no raw locators (they live in `drivers/dashboard-list.ts`), and no `// 1. …` step narration. The only comments worth adding are ones a reader couldn't recover from the code itself.
|
||||
|
||||
# Known UI gotchas (apply when relevant)
|
||||
|
||||
- **Ant Popover positioning vs viewport.** Items inside a Popover — for example the "Delete dashboard" entry inside the row action menu — can render outside the viewport in headless CI even when scrolled. `click({ force: true })` skips actionability checks but Playwright still requires the click coordinates to land inside the viewport. Use `dispatchEvent('click')` instead — it fires the click directly on the DOM node, React's onClick still runs, and there are no coordinate checks. Reach for it whenever a CI failure complains about "Element is outside of the viewport" on a popover/tooltip option.
|
||||
- **Sticky-header rows below the fold.** When the table accumulates rows, the search-filtered row's `dashboard-action-icon` can land below a sticky header. Always `await actionIcon.scrollIntoViewIfNeeded()` before clicking. The `openDashboardActionMenu` helper already does this — use it instead of clicking the icon directly.
|
||||
- **Sticky-header rows below the fold.** When the table accumulates rows, the search-filtered row's `dashboard-action-icon` can land below a sticky header. Always `await actionIcon.scrollIntoViewIfNeeded()` before clicking, inside the driver action.
|
||||
- **React Query mutations vs navigation.** UI delete clicks fire an async DELETE through React Query. Navigating away before the mutation completes cancels it. Pair the click with `page.waitForResponse((r) => r.request().method() === 'DELETE' && /\/dashboards\//.test(r.url()))` and `await expect(dialog).not.toBeVisible()` before the next `page.goto(...)`.
|
||||
- **Monaco editor swallows Escape.** Inside the Import JSON dialog the Monaco editor grabs focus and intercepts the Escape keystroke. Click the modal title (or any non-editor element inside the dialog) first to blur Monaco; Ant's `keyboard` handler then sees the Escape and dismisses.
|
||||
- **Empty zero-state hides controls.** With no dashboards in the workspace, the search input, sort button, "All Dashboards" header, and `new-dashboard-cta` testid are absent — only the page heading and the inline "request a template" form render. Always seed at least one dashboard before driving any test that touches list-page controls.
|
||||
- **Monaco editor swallows Escape.** Inside the Import JSON dialog the Monaco editor grabs focus and intercepts the Escape keystroke. Click the modal title first to blur Monaco; `dashboardList.dismissWithEscape()` does this.
|
||||
- **Empty zero-state hides controls.** With no dashboards in the workspace, the list controls and `new-dashboard-cta` are absent. Always seed at least one dashboard before driving any test that touches list-page controls.
|
||||
|
||||
# Quality bar
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: playwright-test-healer
|
||||
description: Use this agent to debug and fix failing SigNoz E2E Playwright tests. Examples — <example>Context: A spec is red. user: 'tests/e2e/tests/dashboards/list.spec.ts is failing, fix it' assistant: 'Using the healer agent to debug each failing scenario and adjust the spec.'</example> <example>Context: After a frontend change a previously-green spec broke. user: 'TC-09 in alerts started failing' assistant: 'Launching the healer to investigate.'</example>
|
||||
description: Use this agent to debug and fix failing SigNoz E2E Playwright tests. Examples — <example>Context: A spec is red. user: 'tests/e2e/tests/dashboards/list/search.spec.ts is failing, fix it' assistant: 'Using the healer agent to debug each failing scenario and adjust the spec.'</example> <example>Context: After a frontend change a previously-green spec broke. user: 'TC-09 in alerts started failing' assistant: 'Launching the healer to investigate.'</example>
|
||||
tools: Glob, Grep, Read, Write, Edit, Bash, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_generate_locator, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_snapshot, mcp__playwright-test__test_debug, mcp__playwright-test__test_list, mcp__playwright-test__test_run
|
||||
model: sonnet
|
||||
color: red
|
||||
@@ -27,11 +27,11 @@ Don't try to start the stack yourself — it can take ~4 minutes on a cold build
|
||||
- `browser_network_requests` for API failures (the SigNoz API requires `Authorization: Bearer <localStorage.AUTH_TOKEN>`; 401s usually mean the test bypassed the fixture).
|
||||
- `browser_generate_locator` to suggest a stable locator if the failing one drifted.
|
||||
4. **Root-cause.** Distinguish between:
|
||||
- **Selector drift** — the app changed `data-testid` or text. Fix the locator. Prefer `data-testid` (grep `frontend/src/<feature-dir>/` for the new one).
|
||||
- **Timing** — the test races a load. Replace `waitForTimeout` with `await expect(locator).toBe…()` or `page.waitForResponse(...)` on the triggering action.
|
||||
- **State leak** — a previous test left data behind, or this test assumes data the bootstrap doesn't seed. Ensure the test seeds via API and cleans up in `try / finally`. The bootstrap creates a fresh stack with **zero** dashboards / alerts.
|
||||
- **Selector drift** — the app changed `data-testid` or text. Fix the locator where it lives: in the driver under `tests/e2e/drivers/` when the spec uses one, so every spec sharing it heals at once. Prefer `data-testid` (grep `frontend/src/<feature-dir>/` for the new one).
|
||||
- **Timing** — the test races a load. Replace `waitForTimeout` with `await expect(locator).toBe…()` or `page.waitForResponse(...)` on the triggering action — inside the driver action when there is one.
|
||||
- **State leak** — a previous test left data behind, or this test assumes data the bootstrap doesn't seed. Seed through the driver (its `dispose` cleans up) or via API with `try / finally`. The bootstrap creates a fresh stack with **zero** dashboards / alerts and only the golden telemetry; parallel per-test seeds share the tenant, so an exact-count assertion on telemetry is a test bug — assert structure instead.
|
||||
- **Genuine app bug** — the app is broken, not the test. Mark the test with `test.fixme(...)` and add a one-line `// known: <description>` comment. Don't silently change the assertion to make it pass.
|
||||
5. **Fix.** Edit the spec. Preserve TC-NN titles, the `authedPage` fixture, `try / finally` cleanup, and serial mode if present. If you renumber, retitle, or `test.fixme(...)` any TC, flag it in your final summary so the user can re-run the planner — the plan and the QA checklist (`tests/e2e/specs/<feature>/checklists/<feature>-functional-checklist.md`) re-derive from the current `.spec.ts` and will otherwise drift.
|
||||
5. **Fix.** Edit the spec or its driver. Preserve TC-NN titles, the driver / `authedPage` fixtures, cleanup, and serial mode if present. A driver change must keep every other spec importing it green — run them too. If you renumber, retitle, or `test.fixme(...)` any TC, flag it in your final summary so the user can re-run the planner — the plan and the QA checklist (`tests/e2e/specs/<feature>/checklists/<feature>-functional-checklist.md`) re-derive from the current `.spec.ts` and will otherwise drift.
|
||||
6. **Re-run only the fixed test** before moving to the next failure. Three options:
|
||||
- `npx playwright test -g 'TC-09' --project=chromium` — target a single TC by title
|
||||
- `npx playwright test --last-failed --project=chromium` — replay everything that failed last run
|
||||
@@ -41,14 +41,13 @@ Don't try to start the stack yourself — it can take ~4 minutes on a cold build
|
||||
|
||||
# Repo-specific signals
|
||||
|
||||
- **Reuse helpers before adding new code.** [`tests/e2e/helpers/dashboards.ts`](../../tests/e2e/helpers/dashboards.ts) and [`tests/e2e/helpers/auth.ts`](../../tests/e2e/helpers/auth.ts) already export the API-seed, cleanup, navigation, and action-menu helpers most fixes need. Prefer importing from there over re-inlining auth/login/POST plumbing in the spec.
|
||||
- **Reuse drivers and helpers before adding new code.** [`tests/e2e/drivers/`](../../tests/e2e/drivers/) holds the per-feature seed, cleanup, navigation and action methods (`kit.ts` explains `driver` / `seeded`); [`helpers/common.ts`](../../tests/e2e/helpers/common.ts) and [`helpers/auth.ts`](../../tests/e2e/helpers/auth.ts) the plain plumbing. Prefer them over re-inlining auth/login/POST code in the spec.
|
||||
- **Ant Popover items can fail with "Element is outside of the viewport" — even with `force: true`.** `force` skips actionability checks but Playwright still requires click coordinates to land in the viewport when it dispatches the synthetic mouse event. The robust fix is `tooltip.getByText('…').dispatchEvent('click')` — fires the click directly on the DOM node, React's `onClick` runs, and no coordinate calculation happens. Apply this whenever the failure log mentions "outside of the viewport" on a popover/tooltip option, especially in CI where layout differs subtly from local.
|
||||
- **Action-icon rows below the fold.** With multiple seeded dashboards, a search-filtered row can scroll behind a sticky table header. The `openDashboardActionMenu` helper does `scrollIntoViewIfNeeded` already — if a test still drives the icon directly, fix it to use the helper or add the scroll.
|
||||
- **Action-icon rows below the fold.** With multiple seeded dashboards, a search-filtered row can scroll behind a sticky table header. Add `scrollIntoViewIfNeeded` before the click, in the driver action.
|
||||
- **React Query mutations vs page.goto.** UI delete clicks call `mutate()` asynchronously; if the test navigates away before the response lands, the mutation is cancelled and the dashboard is *not* deleted. Wait for the DELETE response and the dialog dismissal explicitly: `page.waitForResponse((r) => r.request().method() === 'DELETE' && /\/dashboards\//.test(r.url()))` plus `await expect(dialog).not.toBeVisible()`.
|
||||
- **Monaco editor swallows Escape inside the Import JSON dialog.** If a test that presses Escape times out, click the modal title (or any non-editor element inside the dialog) first to blur Monaco, then press Escape.
|
||||
- **The list pages render zero-state when the workspace is empty.** Many locators (search input, sort button, `new-dashboard-cta` testid, "All Dashboards" header) are absent in zero-state. A 30s timeout on those usually means the workspace was empty — seed first via `createDashboardViaApi`.
|
||||
- **The "Enter dashboard name…" inline field is a `RequestDashboardBtn` (template-request feedback form), not a create flow.** Tests that try to use it to create a named dashboard will silently no-op. The only UI create paths are the "New dashboard" dropdown → "Create dashboard" (default name "Sample Title", see `DEFAULT_DASHBOARD_TITLE`) or "Import JSON".
|
||||
- **Auth.** `tests/e2e/fixtures/auth.ts` logs in once per worker and caches `storageState` (cookies + localStorage with `AUTH_TOKEN`). For API-driven seeding/cleanup, use `authToken(page)` from `helpers/dashboards.ts` and pass `Authorization: Bearer <token>`. Never re-implement login.
|
||||
- **The list pages render zero-state when the workspace is empty.** List controls and the `new-dashboard-cta` testid are absent in zero-state. A timeout on those usually means the workspace was empty — seed first via `dashboards.seed(...)`.
|
||||
- **Auth.** `tests/e2e/fixtures/auth.ts` logs in once per worker and caches `storageState` (cookies + localStorage with `AUTH_TOKEN`). For API-driven seeding/cleanup, use `authToken(page)` from `helpers/common.ts` and pass `Authorization: Bearer <token>`. Never re-implement login.
|
||||
- **Extended fixtures.** Domain-specific fixtures extend `auth` and add seeded data. Alerts uses `fixtures/alerts/alert-rules` (worker-scoped rule list, test-scoped factory) and `fixtures/alerts/alert-history` (extends alert-rules, waits on ruler evaluation). See [docs/contributing/tests/e2e.md](../../docs/contributing/tests/e2e.md) for the pattern. When a test fails on missing data, check if it imports the wrong fixture level.
|
||||
- **Ant Design popovers** (sort menu, action menu) are click-toggle. The trigger element is often an inline `<svg>` with a `data-testid` — clicking it opens the popover; clicking it again closes. After selecting an option, the popover auto-closes. If a test interacts with the popover twice, wait for the menu items to be visible explicitly between toggles.
|
||||
- **Artifacts.** Every failed test writes to `tests/e2e/artifacts/results/<test-slug>/` — the `error-context.md` accessibility snapshot is the fastest way to see what the page actually looked like when it failed.
|
||||
|
||||
@@ -20,8 +20,10 @@ You will:
|
||||
- The frontend stores its JWT in `localStorage` under `AUTH_TOKEN` and the API requires `Authorization: Bearer <token>` for protected endpoints. Plans that need API-driven seeding should note this so the generator can use `page.request.*`.
|
||||
|
||||
2. **Check what's already wired up.**
|
||||
- [tests/e2e/helpers/dashboards.ts](../../tests/e2e/helpers/dashboards.ts) and [tests/e2e/helpers/auth.ts](../../tests/e2e/helpers/auth.ts) hold reusable helpers (`createDashboardViaApi`, `gotoDashboardsList`, `openDashboardActionMenu`, `newAdminContext`, `importApmMetricsDashboardViaUI`, etc.). When the plan touches dashboards, reference these by name in the steps so the generator can reuse them rather than reinvent.
|
||||
- [tests/e2e/fixtures/apm-metrics.json](../../tests/e2e/fixtures/apm-metrics.json) is a real-world dashboard payload (rich tags, panels, description) suitable as a seed fixture — note in the plan if a scenario benefits from it.
|
||||
- [tests/e2e/drivers/](../../tests/e2e/drivers/) holds one driver per feature (e.g. `dashboards.ts`, `dashboard-list.ts`, `panel-editor.ts`, `routing.ts`, `channels.ts`). A driver is a fixture exposing the feature's locators and actions, including API seeding that cleans itself up (`dashboards.seed(...)`). When one covers the feature, write steps in terms of its actions by name so the generator reuses them. When none does, say so: the generator adds one.
|
||||
- [tests/e2e/helpers/](../../tests/e2e/helpers/) holds plain helpers: `common.ts` (`authToken`, `watchConsole`, `collectRequests`, `seederUrl`), `auth.ts` (`newAdminContext`), `query-range-mock.ts` (mock `query_range` for error, no-data, warning and loading states only).
|
||||
- **Telemetry is pre-seeded.** `bootstrap/global.setup.ts` loads a golden OTel-demo dataset (logs, traces, metrics over the last 6h; constants in `GOLDEN` in `helpers/dashboard-spec.ts`). Plans that query data use a window ≤ 6h and assert structure, not exact counts. Data the golden set lacks is seeded per test through the seeder (`helpers/alerts/seeding.ts` shows how).
|
||||
- [tests/e2e/testdata/](../../tests/e2e/testdata/) holds static payloads (`dashboards/panels.ts`, `traces/large-trace.json`) — note in the plan if a scenario benefits from one.
|
||||
|
||||
3. **Navigate and explore**
|
||||
- Invoke `planner_setup_page` once before any other browser tool.
|
||||
@@ -39,7 +41,7 @@ You will:
|
||||
|
||||
Each scenario must include:
|
||||
- **TC-NN** title — `TC-NN <short description>` (matches the naming this repo uses for test titles).
|
||||
- Preconditions (what state the test expects — note that the bootstrap creates a fresh stack with **zero dashboards / alerts / etc.**, so plans must seed their own data).
|
||||
- Preconditions (what state the test expects — note that the bootstrap creates a fresh stack with **zero dashboards / alerts / etc.** (only the golden telemetry), so plans must seed their own resources).
|
||||
- Step-by-step user actions.
|
||||
- Expected outcomes per step.
|
||||
- Cleanup notes (what gets created and how to remove it — usually via API).
|
||||
@@ -64,7 +66,7 @@ The dashboards list page (`/dashboard`) lists all dashboards in the workspace. F
|
||||
- Open per-row actions: View, Open in New Tab, Copy Link, Export JSON, Delete dashboard
|
||||
- Create a new dashboard via the "New dashboard" dropdown (Create dashboard / Import JSON / View templates)
|
||||
|
||||
**Bootstrap state:** the pytest harness creates a fresh stack with no pre-seeded dashboards. Every test must seed its own data. The "Enter dashboard name…" inline input is a "request a new template" feedback form — **not** a create flow. The only UI create path is the dropdown.
|
||||
**Bootstrap state:** the pytest harness creates a fresh stack with no pre-seeded dashboards. Every test seeds its own through the `dashboards` driver (`dashboards.seed(...)`, deleted when the test ends).
|
||||
|
||||
**Known limitations:**
|
||||
- Ascending sort is not yet implemented — repeated clicks on the sort button keep `order=descend`.
|
||||
@@ -87,7 +89,7 @@ The dashboards list page (`/dashboard`) lists all dashboards in the workspace. F
|
||||
- All Dashboards section header rendered.
|
||||
- Search input, sort button, and at least one dashboard thumbnail visible.
|
||||
|
||||
**Cleanup:** delete the seeded dashboard via `DELETE /api/v1/dashboards/<id>`.
|
||||
**Cleanup:** none in the spec — the `dashboards` driver deletes what it seeded.
|
||||
|
||||
#### TC-02 ...
|
||||
</example-spec>
|
||||
@@ -95,7 +97,7 @@ The dashboards list page (`/dashboard`) lists all dashboards in the workspace. F
|
||||
**Quality bar:**
|
||||
- Steps must be specific enough that any tester (or the generator agent) can follow without ambiguity.
|
||||
- Include negative scenarios — empty state, no-match search, validation errors.
|
||||
- Each scenario must own its preconditions and cleanup. **Do not invent cross-file global fixtures** — they break parallel-by-file execution. Suite-level `beforeAll` / `afterAll` *within* a single spec file is fine and is the preferred shape for files with > ~10 scenarios; per-test `try / finally` is fine for smaller specs.
|
||||
- Each scenario must own its preconditions and cleanup. **Do not invent cross-file global fixtures** — they break parallel-by-file execution. Prefer a driver's self-cleaning seed (`dispose`) or worker-scoped `seeded(...)` layer; without a driver, suite-level `beforeAll` / `afterAll` within one spec file or per-test `try / finally` both work.
|
||||
- Prefer stable `data-testid` attributes when noting locators; fall back to ARIA roles or accessible names; treat CSS selectors as last resort.
|
||||
- **Don't pad coverage.** Every TC must catch a regression that would actually ship if the test were missing — a real branch in the source, a real user-visible failure mode. A TC that asserts a hard-coded string is still rendered, or that a button is still a button, adds nothing but maintenance cost: it inflates the suite, slows CI, and trains readers to skim past the directory. Before writing a scenario, ask "what code change would break this?" — if the only answer is "deleting the literal under test," cut it or fold it into a richer scenario as one assertion among many.
|
||||
- **Collapse near-duplicates.** Two TCs that differ only in input value (search by title vs search by description, when the underlying code path is the same) should merge into one parameterised scenario unless each input genuinely exercises a distinct branch. Prefer one assertion-rich TC over three thin ones.
|
||||
|
||||
@@ -46,9 +46,9 @@ One endpoint feeding several widgets stays one handler that reads the request. `
|
||||
|
||||
```ts
|
||||
rest.get(
|
||||
'http://localhost/api/v1/explorer/views',
|
||||
'http://localhost/api/v2/saved_views',
|
||||
response.json((req) =>
|
||||
savedViews(values.savedViews, req.url.searchParams.get('sourcePage') ?? 'logs'),
|
||||
savedViews(values.savedViews, req.url.searchParams.get('source') ?? 'logs'),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
@@ -11389,8 +11389,7 @@ components:
|
||||
evaluation:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationEnvelope'
|
||||
description: The evaluation window, as a kind and spec. Required when schemaVersion
|
||||
is v2alpha1; derived from evalWindow and frequency for v1.
|
||||
description: The evaluation window, as a kind and spec.
|
||||
frequency:
|
||||
description: How often the rule is evaluated, as a Go duration such as 1m.
|
||||
Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set
|
||||
@@ -11406,8 +11405,7 @@ components:
|
||||
notificationSettings:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesNotificationSettings'
|
||||
description: Grouping, re-notification and routing behaviour. Required when
|
||||
schemaVersion is v2alpha1; derived from the v1 fields otherwise.
|
||||
description: Grouping, re-notification and routing behaviour.
|
||||
preferredChannels:
|
||||
description: The displayName of each notification channel to notify, for
|
||||
schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries
|
||||
@@ -11420,26 +11418,30 @@ components:
|
||||
- $ref: '#/components/schemas/RuletypesRuleType'
|
||||
description: 'The evaluation strategy: threshold_rule compares against thresholds,
|
||||
promql_rule evaluates a PromQL expression, anomaly_rule scores deviation
|
||||
from seasonal history. Defaults to threshold_rule for builder queries
|
||||
and is forced to promql_rule for promql queries.'
|
||||
from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql
|
||||
queries and is forced to promql_rule for promql queries.'
|
||||
schemaVersion:
|
||||
description: 'The rule payload schema: v1 (the default when omitted) uses
|
||||
the flat condition fields, v2alpha1 uses condition.thresholds, evaluation
|
||||
and notificationSettings.'
|
||||
description: The rule payload schema.
|
||||
enum:
|
||||
- v2alpha1
|
||||
type: string
|
||||
source:
|
||||
deprecated: true
|
||||
description: The URL of the page the rule was created from. Informational;
|
||||
it is not used for evaluation.
|
||||
type: string
|
||||
version:
|
||||
description: The query payload version. Only v5 is accepted and it is filled
|
||||
in when omitted.
|
||||
deprecated: true
|
||||
description: The query payload version. Nothing reads it.
|
||||
type: string
|
||||
required:
|
||||
- alert
|
||||
- alertType
|
||||
- ruleType
|
||||
- condition
|
||||
- evaluation
|
||||
- schemaVersion
|
||||
- notificationSettings
|
||||
type: object
|
||||
RuletypesPostableRuleView:
|
||||
properties:
|
||||
@@ -11528,8 +11530,7 @@ components:
|
||||
evaluation:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationEnvelope'
|
||||
description: The evaluation window, as a kind and spec. Required when schemaVersion
|
||||
is v2alpha1; derived from evalWindow and frequency for v1.
|
||||
description: The evaluation window, as a kind and spec.
|
||||
frequency:
|
||||
description: How often the rule is evaluated, as a Go duration such as 1m.
|
||||
Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set
|
||||
@@ -11548,8 +11549,7 @@ components:
|
||||
notificationSettings:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesNotificationSettings'
|
||||
description: Grouping, re-notification and routing behaviour. Required when
|
||||
schemaVersion is v2alpha1; derived from the v1 fields otherwise.
|
||||
description: Grouping, re-notification and routing behaviour.
|
||||
preferredChannels:
|
||||
description: The displayName of each notification channel to notify, for
|
||||
schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries
|
||||
@@ -11562,14 +11562,15 @@ components:
|
||||
- $ref: '#/components/schemas/RuletypesRuleType'
|
||||
description: 'The evaluation strategy: threshold_rule compares against thresholds,
|
||||
promql_rule evaluates a PromQL expression, anomaly_rule scores deviation
|
||||
from seasonal history. Defaults to threshold_rule for builder queries
|
||||
and is forced to promql_rule for promql queries.'
|
||||
from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql
|
||||
queries and is forced to promql_rule for promql queries.'
|
||||
schemaVersion:
|
||||
description: 'The rule payload schema: v1 (the default when omitted) uses
|
||||
the flat condition fields, v2alpha1 uses condition.thresholds, evaluation
|
||||
and notificationSettings.'
|
||||
description: The rule payload schema.
|
||||
enum:
|
||||
- v2alpha1
|
||||
type: string
|
||||
source:
|
||||
deprecated: true
|
||||
description: The URL of the page the rule was created from. Informational;
|
||||
it is not used for evaluation.
|
||||
type: string
|
||||
@@ -11584,8 +11585,8 @@ components:
|
||||
updatedBy:
|
||||
type: string
|
||||
version:
|
||||
description: The query payload version. Only v5 is accepted and it is filled
|
||||
in when omitted.
|
||||
deprecated: true
|
||||
description: The query payload version. Nothing reads it.
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
@@ -11594,6 +11595,9 @@ components:
|
||||
- alertType
|
||||
- ruleType
|
||||
- condition
|
||||
- evaluation
|
||||
- schemaVersion
|
||||
- notificationSettings
|
||||
type: object
|
||||
RuletypesRuleCondition:
|
||||
properties:
|
||||
@@ -11643,8 +11647,7 @@ components:
|
||||
against, hourly, daily or weekly.'
|
||||
selectedQueryName:
|
||||
description: The name of the query whose result is evaluated, such as A
|
||||
or F1. Must match a query in compositeQuery. When omitted, F1 is used
|
||||
if present, otherwise the enabled query with the highest name.
|
||||
or F1. Must match a query in compositeQuery.
|
||||
type: string
|
||||
target:
|
||||
description: 'schemaVersion v1 only: the value compared against, in targetUnit.
|
||||
@@ -11660,10 +11663,11 @@ components:
|
||||
thresholds:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesRuleThresholdData'
|
||||
description: The thresholds to evaluate, as a kind and spec. Required when
|
||||
schemaVersion is v2alpha1; derived from op, target and matchType for v1.
|
||||
description: The thresholds to evaluate, as a kind and spec.
|
||||
required:
|
||||
- compositeQuery
|
||||
- selectedQueryName
|
||||
- thresholds
|
||||
type: object
|
||||
RuletypesRuleThresholdData:
|
||||
discriminator:
|
||||
@@ -25312,13 +25316,13 @@ paths:
|
||||
schemaVersion: v2alpha1
|
||||
version: v5
|
||||
metric_anomaly:
|
||||
description: Anomaly rules are not yet supported under schemaVersion
|
||||
v2alpha1, so this example uses the v1 shape. Wraps a builder query
|
||||
in the `anomaly` function with daily seasonality SigNoz compares
|
||||
each point against the forecast for that time of day. Fires when
|
||||
the anomaly score stays below the threshold for the entire window;
|
||||
`requireMinPoints` guards against noisy intervals.
|
||||
summary: Metric anomaly rule (v1 only)
|
||||
description: Wraps a builder query in the `anomaly` function with
|
||||
daily seasonality, so SigNoz compares each point against the forecast
|
||||
for that time of day. The threshold is compared against the signed
|
||||
anomaly score, so `below` with a target of -2 fires when an abrupt
|
||||
drop holds for the entire window; `requireMinPoints` guards against
|
||||
noisy intervals.
|
||||
summary: Metric anomaly rule
|
||||
value:
|
||||
alert: Anomalous drop in ingested spans
|
||||
alertType: METRIC_BASED_ALERT
|
||||
@@ -25353,22 +25357,36 @@ paths:
|
||||
stepInterval: 21600
|
||||
type: builder_query
|
||||
queryType: builder
|
||||
matchType: all_the_times
|
||||
op: below
|
||||
requireMinPoints: true
|
||||
requiredNumPoints: 3
|
||||
seasonality: daily
|
||||
selectedQueryName: A
|
||||
target: 2
|
||||
thresholds:
|
||||
kind: basic
|
||||
spec:
|
||||
- channels:
|
||||
- slack-ingestion
|
||||
matchType: all_the_times
|
||||
name: warning
|
||||
op: below
|
||||
target: -2
|
||||
description: Detect an abrupt drop in span ingestion using a z-score
|
||||
anomaly function
|
||||
evalWindow: 24h
|
||||
frequency: 3h
|
||||
evaluation:
|
||||
kind: rolling
|
||||
spec:
|
||||
evalWindow: 24h
|
||||
frequency: 3h
|
||||
labels:
|
||||
severity: warning
|
||||
preferredChannels:
|
||||
- slack-ingestion
|
||||
notificationSettings:
|
||||
renotify:
|
||||
alertStates:
|
||||
- firing
|
||||
enabled: true
|
||||
interval: 4h
|
||||
ruleType: anomaly_rule
|
||||
schemaVersion: v2alpha1
|
||||
version: v5
|
||||
metric_promql:
|
||||
description: PromQL expression instead of the builder. Dotted OTEL
|
||||
@@ -26264,13 +26282,13 @@ paths:
|
||||
schemaVersion: v2alpha1
|
||||
version: v5
|
||||
metric_anomaly:
|
||||
description: Anomaly rules are not yet supported under schemaVersion
|
||||
v2alpha1, so this example uses the v1 shape. Wraps a builder query
|
||||
in the `anomaly` function with daily seasonality SigNoz compares
|
||||
each point against the forecast for that time of day. Fires when
|
||||
the anomaly score stays below the threshold for the entire window;
|
||||
`requireMinPoints` guards against noisy intervals.
|
||||
summary: Metric anomaly rule (v1 only)
|
||||
description: Wraps a builder query in the `anomaly` function with
|
||||
daily seasonality, so SigNoz compares each point against the forecast
|
||||
for that time of day. The threshold is compared against the signed
|
||||
anomaly score, so `below` with a target of -2 fires when an abrupt
|
||||
drop holds for the entire window; `requireMinPoints` guards against
|
||||
noisy intervals.
|
||||
summary: Metric anomaly rule
|
||||
value:
|
||||
alert: Anomalous drop in ingested spans
|
||||
alertType: METRIC_BASED_ALERT
|
||||
@@ -26305,22 +26323,36 @@ paths:
|
||||
stepInterval: 21600
|
||||
type: builder_query
|
||||
queryType: builder
|
||||
matchType: all_the_times
|
||||
op: below
|
||||
requireMinPoints: true
|
||||
requiredNumPoints: 3
|
||||
seasonality: daily
|
||||
selectedQueryName: A
|
||||
target: 2
|
||||
thresholds:
|
||||
kind: basic
|
||||
spec:
|
||||
- channels:
|
||||
- slack-ingestion
|
||||
matchType: all_the_times
|
||||
name: warning
|
||||
op: below
|
||||
target: -2
|
||||
description: Detect an abrupt drop in span ingestion using a z-score
|
||||
anomaly function
|
||||
evalWindow: 24h
|
||||
frequency: 3h
|
||||
evaluation:
|
||||
kind: rolling
|
||||
spec:
|
||||
evalWindow: 24h
|
||||
frequency: 3h
|
||||
labels:
|
||||
severity: warning
|
||||
preferredChannels:
|
||||
- slack-ingestion
|
||||
notificationSettings:
|
||||
renotify:
|
||||
alertStates:
|
||||
- firing
|
||||
enabled: true
|
||||
interval: 4h
|
||||
ruleType: anomaly_rule
|
||||
schemaVersion: v2alpha1
|
||||
version: v5
|
||||
metric_promql:
|
||||
description: PromQL expression instead of the builder. Dotted OTEL
|
||||
@@ -27117,13 +27149,13 @@ paths:
|
||||
schemaVersion: v2alpha1
|
||||
version: v5
|
||||
metric_anomaly:
|
||||
description: Anomaly rules are not yet supported under schemaVersion
|
||||
v2alpha1, so this example uses the v1 shape. Wraps a builder query
|
||||
in the `anomaly` function with daily seasonality SigNoz compares
|
||||
each point against the forecast for that time of day. Fires when
|
||||
the anomaly score stays below the threshold for the entire window;
|
||||
`requireMinPoints` guards against noisy intervals.
|
||||
summary: Metric anomaly rule (v1 only)
|
||||
description: Wraps a builder query in the `anomaly` function with
|
||||
daily seasonality, so SigNoz compares each point against the forecast
|
||||
for that time of day. The threshold is compared against the signed
|
||||
anomaly score, so `below` with a target of -2 fires when an abrupt
|
||||
drop holds for the entire window; `requireMinPoints` guards against
|
||||
noisy intervals.
|
||||
summary: Metric anomaly rule
|
||||
value:
|
||||
alert: Anomalous drop in ingested spans
|
||||
alertType: METRIC_BASED_ALERT
|
||||
@@ -27158,22 +27190,36 @@ paths:
|
||||
stepInterval: 21600
|
||||
type: builder_query
|
||||
queryType: builder
|
||||
matchType: all_the_times
|
||||
op: below
|
||||
requireMinPoints: true
|
||||
requiredNumPoints: 3
|
||||
seasonality: daily
|
||||
selectedQueryName: A
|
||||
target: 2
|
||||
thresholds:
|
||||
kind: basic
|
||||
spec:
|
||||
- channels:
|
||||
- slack-ingestion
|
||||
matchType: all_the_times
|
||||
name: warning
|
||||
op: below
|
||||
target: -2
|
||||
description: Detect an abrupt drop in span ingestion using a z-score
|
||||
anomaly function
|
||||
evalWindow: 24h
|
||||
frequency: 3h
|
||||
evaluation:
|
||||
kind: rolling
|
||||
spec:
|
||||
evalWindow: 24h
|
||||
frequency: 3h
|
||||
labels:
|
||||
severity: warning
|
||||
preferredChannels:
|
||||
- slack-ingestion
|
||||
notificationSettings:
|
||||
renotify:
|
||||
alertStates:
|
||||
- firing
|
||||
enabled: true
|
||||
interval: 4h
|
||||
ruleType: anomaly_rule
|
||||
schemaVersion: v2alpha1
|
||||
version: v5
|
||||
metric_promql:
|
||||
description: PromQL expression instead of the builder. Dotted OTEL
|
||||
@@ -28676,13 +28722,13 @@ paths:
|
||||
schemaVersion: v2alpha1
|
||||
version: v5
|
||||
metric_anomaly:
|
||||
description: Anomaly rules are not yet supported under schemaVersion
|
||||
v2alpha1, so this example uses the v1 shape. Wraps a builder query
|
||||
in the `anomaly` function with daily seasonality SigNoz compares
|
||||
each point against the forecast for that time of day. Fires when
|
||||
the anomaly score stays below the threshold for the entire window;
|
||||
`requireMinPoints` guards against noisy intervals.
|
||||
summary: Metric anomaly rule (v1 only)
|
||||
description: Wraps a builder query in the `anomaly` function with
|
||||
daily seasonality, so SigNoz compares each point against the forecast
|
||||
for that time of day. The threshold is compared against the signed
|
||||
anomaly score, so `below` with a target of -2 fires when an abrupt
|
||||
drop holds for the entire window; `requireMinPoints` guards against
|
||||
noisy intervals.
|
||||
summary: Metric anomaly rule
|
||||
value:
|
||||
alert: Anomalous drop in ingested spans
|
||||
alertType: METRIC_BASED_ALERT
|
||||
@@ -28717,22 +28763,36 @@ paths:
|
||||
stepInterval: 21600
|
||||
type: builder_query
|
||||
queryType: builder
|
||||
matchType: all_the_times
|
||||
op: below
|
||||
requireMinPoints: true
|
||||
requiredNumPoints: 3
|
||||
seasonality: daily
|
||||
selectedQueryName: A
|
||||
target: 2
|
||||
thresholds:
|
||||
kind: basic
|
||||
spec:
|
||||
- channels:
|
||||
- slack-ingestion
|
||||
matchType: all_the_times
|
||||
name: warning
|
||||
op: below
|
||||
target: -2
|
||||
description: Detect an abrupt drop in span ingestion using a z-score
|
||||
anomaly function
|
||||
evalWindow: 24h
|
||||
frequency: 3h
|
||||
evaluation:
|
||||
kind: rolling
|
||||
spec:
|
||||
evalWindow: 24h
|
||||
frequency: 3h
|
||||
labels:
|
||||
severity: warning
|
||||
preferredChannels:
|
||||
- slack-ingestion
|
||||
notificationSettings:
|
||||
renotify:
|
||||
alertStates:
|
||||
- firing
|
||||
enabled: true
|
||||
interval: 4h
|
||||
ruleType: anomaly_rule
|
||||
schemaVersion: v2alpha1
|
||||
version: v5
|
||||
metric_promql:
|
||||
description: PromQL expression instead of the builder. Dotted OTEL
|
||||
@@ -31970,6 +32030,132 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
ai_traces_raw:
|
||||
summary: 'Raw: LLM, tool, and agent spans of one service''s AI traces
|
||||
with many tool calls'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
filter:
|
||||
expression: service.name = 'ai-agent' AND trace.tool_call_count
|
||||
> 10
|
||||
limit: 100
|
||||
name: A
|
||||
order:
|
||||
- direction: desc
|
||||
key:
|
||||
fieldContext: span
|
||||
name: timestamp
|
||||
signal: traces
|
||||
type: builder_ai_query
|
||||
end: 1640998800000
|
||||
requestType: raw
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
ai_traces_raw_select_fields:
|
||||
summary: 'Raw: the same spans with the span name and gen_ai model,
|
||||
tool, and token fields selected'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
filter:
|
||||
expression: service.name = 'ai-agent' AND trace.tool_call_count
|
||||
> 10
|
||||
limit: 100
|
||||
name: A
|
||||
order:
|
||||
- direction: desc
|
||||
key:
|
||||
fieldContext: span
|
||||
name: timestamp
|
||||
selectFields:
|
||||
- fieldContext: span
|
||||
name: name
|
||||
- fieldContext: attribute
|
||||
name: gen_ai.request.model
|
||||
- fieldContext: attribute
|
||||
name: gen_ai.tool.name
|
||||
- fieldContext: attribute
|
||||
name: gen_ai.usage.input_tokens
|
||||
- fieldContext: attribute
|
||||
name: gen_ai.usage.output_tokens
|
||||
signal: traces
|
||||
type: builder_ai_query
|
||||
end: 1640998800000
|
||||
requestType: raw
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
ai_traces_scalar:
|
||||
summary: 'Scalar: average per-trace AI cost by model'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- expression: avg(trace.estimated_total_cost)
|
||||
groupBy:
|
||||
- fieldContext: attribute
|
||||
name: gen_ai.request.model
|
||||
limit: 10
|
||||
name: A
|
||||
order:
|
||||
- direction: desc
|
||||
key:
|
||||
name: avg(trace.estimated_total_cost)
|
||||
signal: traces
|
||||
type: builder_ai_query
|
||||
end: 1640998800000
|
||||
requestType: scalar
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
ai_traces_time_series:
|
||||
summary: 'Time series: per-trace AI cost by model'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- expression: sum(trace.estimated_total_cost)
|
||||
groupBy:
|
||||
- fieldContext: attribute
|
||||
name: gen_ai.request.model
|
||||
limit: 10
|
||||
name: A
|
||||
order:
|
||||
- direction: desc
|
||||
key:
|
||||
name: sum(trace.estimated_total_cost)
|
||||
signal: traces
|
||||
stepInterval: 60s
|
||||
type: builder_ai_query
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
ai_traces_trace:
|
||||
summary: 'Trace list: AI traces with many tool calls, most expensive
|
||||
first'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
filter:
|
||||
expression: trace.tool_call_count > 10 AND trace.distinct_tool_count
|
||||
< 3
|
||||
limit: 10
|
||||
name: A
|
||||
order:
|
||||
- direction: desc
|
||||
key:
|
||||
name: trace.estimated_total_cost
|
||||
signal: traces
|
||||
type: builder_ai_query
|
||||
end: 1640998800000
|
||||
requestType: trace
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
clickhouse_sql_logs_raw:
|
||||
summary: 'ClickHouse SQL: raw logs with resource filter'
|
||||
value:
|
||||
|
||||
@@ -93,9 +93,9 @@ tests/
|
||||
│ └── auth.ts
|
||||
├── helpers/ # function helpers + the constants they share with tests
|
||||
│ ├── auth.ts
|
||||
│ └── dashboards.ts
|
||||
│ └── logs-explorer.ts
|
||||
├── testdata/ # static data files (JSON) used by helpers and tests
|
||||
│ └── apm-metrics.json # (example)
|
||||
│ └── traces/large-trace.json # (example)
|
||||
├── tests/ # Playwright .spec.ts files, one dir per feature area
|
||||
│ └── alerts/
|
||||
│ └── alerts.spec.ts # (example)
|
||||
@@ -110,8 +110,8 @@ tests/
|
||||
These two folders look similar but mean different things:
|
||||
|
||||
- **`fixtures/`** holds *Playwright test fixtures* (created via `test.extend({...})`). By the canonical definition, a fixture is "a consistent, predefined set of data, objects, or environmental conditions used to ensure tests run in a stable state" — i.e. setup/teardown that runs *automatically* around each test or worker. `auth.ts` matches: it extends Playwright's `test` with an `authedPage` that's logged-in before every test runs and torn down after. If the only thing in this folder ever is `auth.ts`, that's fine — fixtures are a deliberately small surface.
|
||||
- **`helpers/`** holds plain function helpers that you call *explicitly* from a test or hook — they don't extend Playwright's `test`. This covers both behaviour helpers (e.g. `gotoDashboardsList(page)`) and the constants those helpers and the tests both refer to (e.g. `SEARCH_PLACEHOLDER`). Constants live next to the helpers that use them so a single import line in a test covers both.
|
||||
- **`testdata/`** holds static data files (typically JSON / YAML) consumed by the helpers — for example, `apm-metrics.json`, a real dashboard payload uploaded through the UI by an importer helper.
|
||||
- **`helpers/`** holds plain function helpers that you call *explicitly* from a test or hook — they don't extend Playwright's `test`. This covers both behaviour helpers (e.g. `gotoLogsExplorer(page)`) and the constants those helpers and the tests both refer to (e.g. `LINKED_ROW_CLASS`). Constants live next to the helpers that use them so a single import line in a test covers both.
|
||||
- **`testdata/`** holds static data files (typically JSON / YAML) consumed by the helpers — for example, `traces/large-trace.json`, a real trace the trace-details helpers seed.
|
||||
|
||||
Rule of thumb: if it's a `test.extend` fixture, put it in `fixtures/`. If it's a function you call explicitly (or a constant the function uses), put it in `helpers/`. If it's a static file the helpers read, put it in `testdata/`.
|
||||
|
||||
@@ -128,7 +128,7 @@ test('TC-09 RouteTab switches the url', async ({ routing }) => {
|
||||
});
|
||||
```
|
||||
|
||||
- `driver(name, build)` adds a test-scoped fixture built on `authedPage`.
|
||||
- `driver(name, build, dispose?)` adds a test-scoped fixture built on `authedPage`. `dispose` runs after the test on the same page, so a driver can delete what it seeded.
|
||||
- `seeded(name, open, close)` adds a worker-scoped seed. `open` and `close` run on a throwaway admin page.
|
||||
- A driver reuses helpers by calling them, never by reaching for another fixture.
|
||||
- `drivers/` is AI-owned: coding agents generate and maintain it with the specs, and `.gitattributes` collapses it in diffs.
|
||||
@@ -173,9 +173,10 @@ Each spec follows these principles:
|
||||
1. **Directory per feature**: `tests/e2e/tests/<feature>/*.spec.ts`. Cross-resource junction concerns (e.g. cascade-delete) go in their own file, not packed into one giant spec.
|
||||
2. **Test titles use `TC-NN`**: `test('TC-01 alerts page — tabs render', ...)`. Preserves ordering at a glance and maps to external coverage tracking.
|
||||
3. **UI-first**: drive flows through the UI. Playwright traces capture every BE request/response the UI triggers, so asserting on UI outcomes implicitly validates BE contracts. Reach for direct `page.request.*` only when the test's *purpose* is asserting a response contract (use `page.waitForResponse` on a UI click) or when a specific UI step is structurally flaky (e.g. Ant DatePicker calendar-cell indices) — and even then try UI first.
|
||||
4. **Self-contained state**: each spec seeds its own data and cleans up at suite teardown. The pytest harness creates a fresh stack with **zero** dashboards / alerts / etc. — never assume pre-existing data. Two patterns work:
|
||||
- **Per-test seed + cleanup in `try / finally`** — small specs where each test owns its data.
|
||||
- **Suite-level seed + `afterAll` teardown** — preferred for larger specs. Each `createDashboard(...)` call adds the resulting ID to a module-level `Set<string>`, and one `test.afterAll(...)` deletes everything in the set. See `tests/e2e/tests/dashboards/list.spec.ts` for the full pattern. `test.beforeAll` / `test.afterAll` cannot use `authedPage` directly (it's test-scoped); use `newAdminContext(browser)` from `helpers/auth.ts` instead — it performs one fresh login per suite hook.
|
||||
4. **Self-contained state**: each spec seeds its own data and cleans up at suite teardown. The pytest harness creates a fresh stack with **zero** dashboards / alerts / etc. — never assume pre-existing data. Patterns, in order of preference:
|
||||
- **Driver `dispose`** — the driver tracks what it seeded and deletes it after the test, so the spec has no cleanup code. See `dashboards.seed(...)` in `tests/e2e/drivers/dashboards.ts`.
|
||||
- **Per-test seed + cleanup in `try / finally`** — small specs without a driver, where each test owns its data.
|
||||
- **Suite-level seed + `afterAll` teardown** — larger specs without a driver. One `test.afterAll(...)` deletes everything the suite seeded. See `tests/e2e/tests/llm-o11y/llm-pricing.spec.ts`. `test.beforeAll` / `test.afterAll` cannot use `authedPage` directly (it's test-scoped); use `newAdminContext(browser)` from `helpers/auth.ts` instead — it performs one fresh login per suite hook.
|
||||
5. **Seed via API when the UI flow is multi-step or brittle.** The frontend stores its JWT in `localStorage` under `AUTH_TOKEN`; `page.request.*` inherits the auth fixture's storage state. A typical pattern:
|
||||
```ts
|
||||
const token = await page.evaluate(
|
||||
@@ -187,7 +188,7 @@ Each spec follows these principles:
|
||||
});
|
||||
```
|
||||
This is faster and more reliable than a multi-step UI seed. Reach for the UI flow only when the test's *purpose* is asserting that flow.
|
||||
6. **Reusable static data lives in `tests/e2e/testdata/`.** For example, `apm-metrics.json` is a real dashboard payload that `importApmMetricsDashboardViaUI` (in `helpers/dashboards.ts`) uploads through the actual Import JSON UI flow to seed a richly-tagged dashboard for search/list tests.
|
||||
6. **Reusable static data lives in `tests/e2e/testdata/`.** For example, `traces/large-trace.json` is a real trace that `loadLargeTrace` (in `helpers/trace-details.ts`) seeds for the trace-details tests.
|
||||
|
||||
## How to write an E2E test?
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ const preview: Preview = {
|
||||
'Detail',
|
||||
[
|
||||
'Overview',
|
||||
'Panels',
|
||||
'Settings',
|
||||
'Variables',
|
||||
'New Variable',
|
||||
@@ -140,7 +141,7 @@ const preview: Preview = {
|
||||
'Services',
|
||||
['List', 'Detail', 'Top Level Operations', 'Service Map'],
|
||||
'Logs',
|
||||
['Explorer', 'Live Tail', 'Saved Views', 'Pipelines', 'Settings'],
|
||||
['Explorer', 'Live Tail', 'Pipelines', 'Settings'],
|
||||
'Traces',
|
||||
['Explorer', 'Trace Details', 'Funnel Details'],
|
||||
'Metrics',
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
"tooltip_opsgenie_api_key": "Learn how to obtain the API key from your OpsGenie account [here](https://support.atlassian.com/opsgenie/docs/integrate-opsgenie-with-prometheus/).",
|
||||
"tooltip_email_to": "Enter email addresses separated by commas.",
|
||||
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to. It must be an https URL on chat.googleapis.com.",
|
||||
"google_chat_webhook_url_invalid": "Webhook URL must be an https URL on chat.googleapis.com",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to.",
|
||||
"field_jira_site": "Site URL",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net. Only Jira Cloud is supported.",
|
||||
"jira_site_invalid": "Site URL must be an https URL on an atlassian.net domain",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
|
||||
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
|
||||
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
|
||||
"jira_service_account_tip_link": "Learn how",
|
||||
@@ -82,7 +80,6 @@
|
||||
"field_incidentio_description": "Description",
|
||||
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
|
||||
"incidentio_required_fields": "Alert source URL and token are required",
|
||||
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
|
||||
"field_incidentio_metadata": "Additional metadata",
|
||||
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
|
||||
"placeholder_incidentio_metadata_key": "Key",
|
||||
@@ -92,8 +89,8 @@
|
||||
"field_slack_recipient": "Recipient",
|
||||
"field_slack_title": "Title",
|
||||
"field_slack_description": "Description",
|
||||
"field_webhook_username": "User Name (optional)",
|
||||
"field_webhook_password": "Password (optional)",
|
||||
"field_webhook_username": "Username",
|
||||
"field_webhook_password": "Password",
|
||||
"field_pager_routing_key": "Routing Key",
|
||||
"field_pager_description": "Description",
|
||||
"field_pager_severity": "Severity",
|
||||
@@ -112,13 +109,10 @@
|
||||
"help_pager_group": "A cluster or grouping of sources",
|
||||
"help_pager_component": "The part or component of the affected system that is broke",
|
||||
"help_pager_severity": "Severity of the incident, must be one of: must be one of the following: 'critical', 'warning', 'error' or 'info'",
|
||||
"help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.",
|
||||
"help_webhook_password": "Specify a password or bearer token",
|
||||
"help_pager_description": "Shows up as description in pagerduty",
|
||||
"channel_creation_done": "Successfully created the channel",
|
||||
"channel_edit_done": "Channels Edited Successfully",
|
||||
"selected_channel_invalid": "Channel type selected is invalid",
|
||||
"username_no_password": "A Password must be provided with user name",
|
||||
"channel_test_done": "An alert has been sent to this channel",
|
||||
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
||||
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"name_of_the_view": "Name of the view"
|
||||
}
|
||||
@@ -47,7 +47,6 @@
|
||||
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
||||
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
|
||||
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
|
||||
@@ -25,11 +25,9 @@
|
||||
"tooltip_opsgenie_api_key": "Learn how to obtain the API key from your OpsGenie account [here](https://support.atlassian.com/opsgenie/docs/integrate-opsgenie-with-prometheus/).",
|
||||
"tooltip_email_to": "Enter email addresses separated by commas.",
|
||||
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to. It must be an https URL on chat.googleapis.com.",
|
||||
"google_chat_webhook_url_invalid": "Webhook URL must be an https URL on chat.googleapis.com",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to.",
|
||||
"field_jira_site": "Site URL",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net. Only Jira Cloud is supported.",
|
||||
"jira_site_invalid": "Site URL must be an https URL on an atlassian.net domain",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
|
||||
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
|
||||
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
|
||||
"jira_service_account_tip_link": "Learn how",
|
||||
@@ -90,7 +88,8 @@
|
||||
"field_incidentio_description": "Description",
|
||||
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
|
||||
"incidentio_required_fields": "Alert source URL and token are required",
|
||||
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
|
||||
"incidentio_token_bearer_prefix": "Paste the token only, without the Bearer prefix",
|
||||
"incidentio_token_whitespace": "Remove the spaces around the token",
|
||||
"field_incidentio_metadata": "Additional metadata",
|
||||
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
|
||||
"placeholder_incidentio_metadata_key": "Key",
|
||||
@@ -116,6 +115,7 @@
|
||||
"field_slack_field_short": "Short",
|
||||
"add_slack_field": "Add field",
|
||||
"remove_slack_field": "Remove field",
|
||||
"slack_field_value_required": "Enter a value for this field",
|
||||
"field_slack_actions": "Actions",
|
||||
"help_slack_actions": "Buttons rendered under the attachment. A button with a URL links out.",
|
||||
"label_slack_action_text": "Text",
|
||||
@@ -133,6 +133,7 @@
|
||||
"placeholder_slack_action_style": "Style: default, primary or danger",
|
||||
"placeholder_slack_action_confirm": "Confirmation prompt (optional)",
|
||||
"add_slack_action": "Add action",
|
||||
"slack_action_url_or_name_required": "Enter a URL or a name for this button",
|
||||
"remove_slack_action": "Remove action",
|
||||
"field_opsgenie_api_key": "API Key",
|
||||
"field_opsgenie_description": "Description",
|
||||
@@ -143,9 +144,15 @@
|
||||
"help_email_html": "Send email in html format",
|
||||
"field_email_html": "Email body template",
|
||||
"placeholder_email_html": "Email body template",
|
||||
"field_webhook_username": "User Name (optional)",
|
||||
"field_webhook_password": "Password (optional)",
|
||||
"field_webhook_bearer_token": "Bearer token (optional)",
|
||||
"field_webhook_username": "Username",
|
||||
"field_webhook_password": "Password",
|
||||
"field_webhook_bearer_token": "Bearer token",
|
||||
"field_webhook_auth": "Authentication",
|
||||
"option_webhook_auth_none": "None",
|
||||
"option_webhook_auth_basic": "Basic auth",
|
||||
"option_webhook_auth_bearer": "Bearer token",
|
||||
"webhook_basic_auth_required": "Enter a username or password",
|
||||
"webhook_bearer_token_required": "Enter a bearer token",
|
||||
"field_pager_routing_key": "Routing Key",
|
||||
"field_pager_description": "Description",
|
||||
"field_pager_severity": "Severity",
|
||||
@@ -168,9 +175,7 @@
|
||||
"help_pager_group": "A cluster or grouping of sources",
|
||||
"help_pager_component": "The part or component of the affected system that is broke",
|
||||
"help_pager_severity": "Severity of the incident, must be one of: must be one of the following: 'critical', 'warning', 'error' or 'info'",
|
||||
"help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.",
|
||||
"help_webhook_password": "Specify a password or bearer token",
|
||||
"help_webhook_bearer_token": "Sent as an Authorization: Bearer header. Leave the username and password empty when using it.",
|
||||
"help_webhook_bearer_token": "Sent as an Authorization: Bearer header.",
|
||||
"help_pager_description": "Shows up as description in pagerduty",
|
||||
"help_opsgenie_message": "Shows up as message in opsgenie",
|
||||
"help_opsgenie_priority": "Priority of the incident",
|
||||
@@ -178,7 +183,6 @@
|
||||
"channel_creation_done": "Successfully created the channel",
|
||||
"channel_edit_done": "Channels Edited Successfully",
|
||||
"selected_channel_invalid": "Channel type selected is invalid",
|
||||
"username_no_password": "A Password must be provided with user name",
|
||||
"channel_test_done": "An alert has been sent to this channel",
|
||||
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
||||
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name_of_the_view": "Name of the view",
|
||||
"delete_confirm_message": "Are you sure you want to delete {{viewName}} view? Deleting a view is irreversible and cannot be undone."
|
||||
}
|
||||
@@ -50,8 +50,6 @@
|
||||
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
|
||||
"WORKSPACE_ACCESS_RESTRICTED": "SigNoz | Workspace Access Restricted",
|
||||
"SUPPORT": "SigNoz | Support",
|
||||
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
|
||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
||||
"TRACES_FUNNELS": "SigNoz | Traces Funnels",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||
"SHORTCUTS": "SigNoz | Shortcuts",
|
||||
@@ -66,10 +64,8 @@
|
||||
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
|
||||
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
|
||||
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
|
||||
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
|
||||
"API_MONITORING": "SigNoz | External APIs",
|
||||
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
|
||||
@@ -303,10 +303,10 @@ describe('PrivateRoute', () => {
|
||||
it.each([
|
||||
['/logs-save-views', ROUTES.LOGS_EXPLORER],
|
||||
['/traces-save-views', ROUTES.TRACES_EXPLORER],
|
||||
[ROUTES.LOGS_SAVE_VIEWS, ROUTES.LOGS_EXPLORER],
|
||||
[ROUTES.TRACES_SAVE_VIEWS, ROUTES.TRACES_EXPLORER],
|
||||
[ROUTES.METRICS_EXPLORER_VIEWS, ROUTES.METRICS_EXPLORER_EXPLORER],
|
||||
[ROUTES.METER_EXPLORER_VIEWS, ROUTES.METER_EXPLORER],
|
||||
['/logs/saved-views', ROUTES.LOGS_EXPLORER],
|
||||
['/traces/saved-views', ROUTES.TRACES_EXPLORER],
|
||||
['/metrics-explorer/views', ROUTES.METRICS_EXPLORER_EXPLORER],
|
||||
['/meter/explorer/views', ROUTES.METER_EXPLORER],
|
||||
])('should redirect the old views route %s to %s', (from, to) => {
|
||||
renderPrivateRoute({ initialRoute: from });
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import AppLayout from 'container/AppLayout';
|
||||
import Hex from 'crypto-js/enc-hex';
|
||||
import HmacSHA256 from 'crypto-js/hmac-sha256';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
|
||||
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -66,7 +65,6 @@ function App(): JSX.Element {
|
||||
} = useAppContext();
|
||||
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
||||
const isAIAssistantEnabled = useIsAIAssistantEnabled();
|
||||
const isSavedViewEnabled = useSavedViewEnabled();
|
||||
const chatSupport = useChatSupport();
|
||||
|
||||
const { hostname } = window.location;
|
||||
@@ -256,18 +254,8 @@ function App(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
// The bottom strip carries Support, so the floating bubble goes entirely.
|
||||
if (
|
||||
isSavedViewEnabled ||
|
||||
pathname === ROUTES.ONBOARDING ||
|
||||
pathname.startsWith('/public/dashboard/') ||
|
||||
pathname === '/ai-assistant' ||
|
||||
pathname.startsWith('/ai-assistant/')
|
||||
) {
|
||||
window.Pylon?.('hideChatBubble');
|
||||
} else {
|
||||
window.Pylon?.('showChatBubble');
|
||||
}
|
||||
}, [pathname, isSavedViewEnabled]);
|
||||
window.Pylon?.('hideChatBubble');
|
||||
}, [pathname]);
|
||||
|
||||
// Identity for the Pylon widget. Whether this user gets Pylon at all is
|
||||
// `useChatSupport`'s call — this only fills in who they are.
|
||||
|
||||
@@ -26,10 +26,6 @@ export const ServiceMapPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
|
||||
);
|
||||
|
||||
export const LogsSaveViews = Loadable(
|
||||
() => import(/* webpackChunkName: "LogsSaveViews" */ 'pages/LogsModulePage'), // TODO: Add a wrapper so that the same component can be used in traces
|
||||
);
|
||||
|
||||
export const TracesExplorer = Loadable(
|
||||
() =>
|
||||
import(
|
||||
@@ -37,11 +33,6 @@ export const TracesExplorer = Loadable(
|
||||
),
|
||||
);
|
||||
|
||||
export const TracesSaveViews = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Traces Save Views" */ 'pages/TracesModulePage'),
|
||||
);
|
||||
|
||||
export const TracesFunnels = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Traces Funnels" */ 'pages/TracesModulePage'),
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
Login,
|
||||
Logs,
|
||||
LogsIndexToFields,
|
||||
LogsSaveViews,
|
||||
MessagingQueuesMainPage,
|
||||
MeterExplorerPage,
|
||||
MetricsExplorer,
|
||||
@@ -48,7 +47,6 @@ import {
|
||||
TracesExplorer,
|
||||
TracesFunnelDetails,
|
||||
TracesFunnels,
|
||||
TracesSaveViews,
|
||||
UnAuthorized,
|
||||
UsageExplorerPage,
|
||||
WorkspaceAccessRestricted,
|
||||
@@ -120,13 +118,6 @@ const routes: AppRoutes[] = [
|
||||
exact: true,
|
||||
key: 'SERVICE_MAP',
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGS_SAVE_VIEWS,
|
||||
component: LogsSaveViews,
|
||||
isPrivate: true,
|
||||
exact: true,
|
||||
key: 'LOGS_SAVE_VIEWS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
exact: true,
|
||||
@@ -218,13 +209,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'TRACES_EXPLORER',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACES_SAVE_VIEWS,
|
||||
exact: true,
|
||||
component: TracesSaveViews,
|
||||
isPrivate: true,
|
||||
key: 'TRACES_SAVE_VIEWS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACES_FUNNELS,
|
||||
exact: true,
|
||||
@@ -414,13 +398,6 @@ const routes: AppRoutes[] = [
|
||||
key: 'METRICS_EXPLORER_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.METRICS_EXPLORER_VIEWS,
|
||||
exact: true,
|
||||
component: MetricsExplorer,
|
||||
key: 'METRICS_EXPLORER_VIEWS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.METRICS_EXPLORER_VOLUME_CONTROL,
|
||||
exact: true,
|
||||
@@ -443,13 +420,6 @@ const routes: AppRoutes[] = [
|
||||
key: 'METER_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.METER_EXPLORER_VIEWS,
|
||||
exact: true,
|
||||
component: MeterExplorerPage,
|
||||
key: 'METER_EXPLORER_VIEWS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.API_MONITORING,
|
||||
exact: true,
|
||||
|
||||
@@ -12584,6 +12584,12 @@ export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
|
||||
*/
|
||||
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
/**
|
||||
* The rule payload schema.
|
||||
*/
|
||||
export enum RuletypesPostableRuleDTOSchemaVersion {
|
||||
v2alpha1 = 'v2alpha1',
|
||||
}
|
||||
export enum RuletypesSeasonalityDTO {
|
||||
hourly = 'hourly',
|
||||
daily = 'daily',
|
||||
@@ -12660,9 +12666,9 @@ export interface RuletypesRuleConditionDTO {
|
||||
seasonality?: RuletypesSeasonalityDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description The name of the query whose result is evaluated, such as A or F1. Must match a query in compositeQuery. When omitted, F1 is used if present, otherwise the enabled query with the highest name.
|
||||
* @description The name of the query whose result is evaluated, such as A or F1. Must match a query in compositeQuery.
|
||||
*/
|
||||
selectedQueryName?: string;
|
||||
selectedQueryName: string;
|
||||
/**
|
||||
* @type number,null
|
||||
* @format double
|
||||
@@ -12675,9 +12681,9 @@ export interface RuletypesRuleConditionDTO {
|
||||
*/
|
||||
targetUnit?: string;
|
||||
/**
|
||||
* @description The thresholds to evaluate, as a kind and spec. Required when schemaVersion is v2alpha1; derived from op, target and matchType for v1.
|
||||
* @description The thresholds to evaluate, as a kind and spec.
|
||||
*/
|
||||
thresholds?: RuletypesRuleThresholdDataDTO;
|
||||
thresholds: RuletypesRuleThresholdDataDTO;
|
||||
}
|
||||
|
||||
export interface RuletypesPostableRuleDTO {
|
||||
@@ -12715,9 +12721,9 @@ export interface RuletypesPostableRuleDTO {
|
||||
*/
|
||||
evalWindow?: string;
|
||||
/**
|
||||
* @description The evaluation window, as a kind and spec. Required when schemaVersion is v2alpha1; derived from evalWindow and frequency for v1.
|
||||
* @description The evaluation window, as a kind and spec.
|
||||
*/
|
||||
evaluation?: RuletypesEvaluationEnvelopeDTO;
|
||||
evaluation: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description How often the rule is evaluated, as a Go duration such as 1m. Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set it in evaluation.spec instead.
|
||||
@@ -12729,23 +12735,24 @@ export interface RuletypesPostableRuleDTO {
|
||||
*/
|
||||
labels?: RuletypesPostableRuleDTOLabels;
|
||||
/**
|
||||
* @description Grouping, re-notification and routing behaviour. Required when schemaVersion is v2alpha1; derived from the v1 fields otherwise.
|
||||
* @description Grouping, re-notification and routing behaviour.
|
||||
*/
|
||||
notificationSettings?: RuletypesNotificationSettingsDTO;
|
||||
notificationSettings: RuletypesNotificationSettingsDTO;
|
||||
/**
|
||||
* @type array
|
||||
* @description The displayName of each notification channel to notify, for schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries its own channels.
|
||||
*/
|
||||
preferredChannels?: string[];
|
||||
/**
|
||||
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder queries and is forced to promql_rule for promql queries.
|
||||
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql queries and is forced to promql_rule for promql queries.
|
||||
*/
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
/**
|
||||
* @enum v2alpha1
|
||||
* @type string
|
||||
* @description The rule payload schema: v1 (the default when omitted) uses the flat condition fields, v2alpha1 uses condition.thresholds, evaluation and notificationSettings.
|
||||
* @description The rule payload schema.
|
||||
*/
|
||||
schemaVersion?: string;
|
||||
schemaVersion: RuletypesPostableRuleDTOSchemaVersion;
|
||||
/**
|
||||
* @type string
|
||||
* @description The URL of the page the rule was created from. Informational; it is not used for evaluation.
|
||||
@@ -12753,7 +12760,7 @@ export interface RuletypesPostableRuleDTO {
|
||||
source?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description The query payload version. Only v5 is accepted and it is filled in when omitted.
|
||||
* @description The query payload version. Nothing reads it.
|
||||
*/
|
||||
version?: string;
|
||||
}
|
||||
@@ -12776,6 +12783,12 @@ export type RuletypesRuleDTOAnnotations = { [key: string]: string };
|
||||
*/
|
||||
export type RuletypesRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
/**
|
||||
* The rule payload schema.
|
||||
*/
|
||||
export enum RuletypesRuleDTOSchemaVersion {
|
||||
v2alpha1 = 'v2alpha1',
|
||||
}
|
||||
export interface RuletypesRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -12820,9 +12833,9 @@ export interface RuletypesRuleDTO {
|
||||
*/
|
||||
evalWindow?: string;
|
||||
/**
|
||||
* @description The evaluation window, as a kind and spec. Required when schemaVersion is v2alpha1; derived from evalWindow and frequency for v1.
|
||||
* @description The evaluation window, as a kind and spec.
|
||||
*/
|
||||
evaluation?: RuletypesEvaluationEnvelopeDTO;
|
||||
evaluation: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description How often the rule is evaluated, as a Go duration such as 1m. Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set it in evaluation.spec instead.
|
||||
@@ -12839,23 +12852,24 @@ export interface RuletypesRuleDTO {
|
||||
*/
|
||||
labels?: RuletypesRuleDTOLabels;
|
||||
/**
|
||||
* @description Grouping, re-notification and routing behaviour. Required when schemaVersion is v2alpha1; derived from the v1 fields otherwise.
|
||||
* @description Grouping, re-notification and routing behaviour.
|
||||
*/
|
||||
notificationSettings?: RuletypesNotificationSettingsDTO;
|
||||
notificationSettings: RuletypesNotificationSettingsDTO;
|
||||
/**
|
||||
* @type array
|
||||
* @description The displayName of each notification channel to notify, for schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries its own channels.
|
||||
*/
|
||||
preferredChannels?: string[];
|
||||
/**
|
||||
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder queries and is forced to promql_rule for promql queries.
|
||||
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql queries and is forced to promql_rule for promql queries.
|
||||
*/
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
/**
|
||||
* @enum v2alpha1
|
||||
* @type string
|
||||
* @description The rule payload schema: v1 (the default when omitted) uses the flat condition fields, v2alpha1 uses condition.thresholds, evaluation and notificationSettings.
|
||||
* @description The rule payload schema.
|
||||
*/
|
||||
schemaVersion?: string;
|
||||
schemaVersion: RuletypesRuleDTOSchemaVersion;
|
||||
/**
|
||||
* @type string
|
||||
* @description The URL of the page the rule was created from. Informational; it is not used for evaluation.
|
||||
@@ -12876,7 +12890,7 @@ export interface RuletypesRuleDTO {
|
||||
updatedBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description The query payload version. Only v5 is accepted and it is filled in when omitted.
|
||||
* @description The query payload version. Nothing reads it.
|
||||
*/
|
||||
version?: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
|
||||
|
||||
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
|
||||
axios.delete(`/explorer/views/${uuid}`);
|
||||
@@ -1,9 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { AllViewsProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const getAllViews = (
|
||||
sourcepage: DataSource | 'meter',
|
||||
): Promise<AxiosResponse<AllViewsProps>> =>
|
||||
axios.get(`/explorer/views?sourcePage=${sourcepage}`);
|
||||
@@ -1,24 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { ViewProps } from 'types/api/saveViews/types';
|
||||
|
||||
/**
|
||||
* Fetches a single saved view by ID (`GET /api/v1/explorer/views/{viewId}`).
|
||||
*
|
||||
* Hand-maintained alongside the other `api/saveView/*` clients — explorer views
|
||||
* are not in `docs/api/openapi.yml`, so Orval does not generate a hook here
|
||||
* (unlike e.g. `useGetChannelByID` under `api/generated/services/channels`).
|
||||
*
|
||||
* Used by the AI assistant "Open view" action to load `compositeQuery` and
|
||||
* navigate to the correct explorer without listing every view per source page.
|
||||
* See `container/AIAssistant/components/ActionsSection/utils/openSavedView.ts`.
|
||||
*/
|
||||
export interface GetViewByIdProps {
|
||||
status: string;
|
||||
data: ViewProps;
|
||||
}
|
||||
|
||||
export const getViewById = (
|
||||
viewKey: string,
|
||||
): Promise<AxiosResponse<GetViewByIdProps>> =>
|
||||
axios.get(`/explorer/views/${viewKey}`);
|
||||
@@ -1,16 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
|
||||
|
||||
export const saveView = ({
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
viewName,
|
||||
extraData,
|
||||
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
|
||||
axios.post('/explorer/views', {
|
||||
name: viewName,
|
||||
sourcePage,
|
||||
compositeQuery,
|
||||
extraData,
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import axios from 'api';
|
||||
import {
|
||||
UpdateViewPayloadProps,
|
||||
UpdateViewProps,
|
||||
} from 'types/api/saveViews/types';
|
||||
|
||||
export const updateView = ({
|
||||
compositeQuery,
|
||||
viewName,
|
||||
extraData,
|
||||
sourcePage,
|
||||
viewKey,
|
||||
}: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
|
||||
axios.put(`/explorer/views/${viewKey}`, {
|
||||
name: viewName,
|
||||
compositeQuery,
|
||||
extraData,
|
||||
sourcePage,
|
||||
});
|
||||
1
frontend/src/assets/Logos/caddy.svg
Normal file
1
frontend/src/assets/Logos/caddy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#1F88C0" d="M11.094.47c-.842 0-1.696.092-2.552.288a11.37 11.37 0 0 0-4.87 2.423 10.632 10.632 0 0 0-2.36 2.826A10.132 10.132 0 0 0 .305 8.582c-.398 1.62-.4 3.336-.043 5.048.085.405.183.809.31 1.212a11.85 11.85 0 0 0 1.662 3.729 3.273 3.273 0 0 0-.086.427 3.323 3.323 0 0 0 2.848 3.71 3.279 3.279 0 0 0 1.947-.346c1.045.51 2.17.864 3.339 1.04a11.66 11.66 0 0 0 4.285-.155 11.566 11.566 0 0 0 4.936-2.485 10.643 10.643 0 0 0 2.352-2.894 11.164 11.164 0 0 0 1.356-4.424 11.214 11.214 0 0 0-.498-4.335c.175-.077.338-.175.486-.293a.444.444 89.992 0 0 .001 0c.402-.322.693-.794.777-1.342a2.146 2.146 0 0 0-1.79-2.434 2.115 2.115 0 0 0-1.205.171c-.038-.043-.078-.086-.113-.13a11.693 11.693 0 0 0-3.476-2.93 13.348 13.348 0 0 0-1.76-.81 13.55 13.55 0 0 0-2.06-.613A12.121 12.121 0 0 0 11.093.47Zm.714.328c.345-.004.688.01 1.028.042a9.892 9.892 0 0 1 2.743.639c.984.39 1.89.958 2.707 1.632.803.662 1.502 1.45 2.091 2.328.026.039.048.08.07.12a2.12 2.12 0 0 0-.435 2.646c-.158.114-.97.692-1.634 1.183-.414.308-.733.557-.733.557l.581.68s.296-.276.665-.638c.572-.562 1.229-1.233 1.395-1.403a2.122 2.122 0 0 0 1.907.677 11.229 11.229 0 0 1-.013 4.046 11.41 11.41 0 0 1-1.475 3.897 12.343 12.343 0 0 1-2.079 2.587c-1.19 1.125-2.633 2.022-4.306 2.531a10.826 10.826 0 0 1-3.973.484 11.04 11.04 0 0 1-3.057-.652 3.304 3.304 0 0 0 1.417-2.294 3.275 3.275 0 0 0-.294-1.842c.18-.162.403-.363.656-.6 1.015-.955 2.353-2.303 2.353-2.303l-.47-.599s-1.63.972-2.801 1.728c-.307.198-.573.378-.777.517a3.273 3.273 0 0 0-1.516-.611c-1.507-.198-2.927.672-3.487 2.017a10.323 10.323 0 0 1-.695-1.078A10.92 10.92 0 0 1 .728 14.8a10.35 10.35 0 0 1-.2-1.212c-.164-1.653.103-3.258.629-4.754a12.95 12.95 0 0 1 1.087-2.288c.57-.968 1.248-1.872 2.069-2.656A11.013 11.013 0 0 1 11.808.797Zm-.147 3.257a3.838 3.838 0 0 0-3.82 3.82v2.36h-.94c-.751 0-1.377.625-1.377 1.377v3.8h1.46v-3.718h9.354v6.264H10.02v1.46h6.4c.751 0 1.377-.625 1.377-1.377v-6.43c0-.751-.626-1.377-1.377-1.377h-.94v-2.36a3.838 3.838 0 0 0-3.82-3.819zm0 1.46a2.371 2.371 0 0 1 2.36 2.36v2.36H9.3v-2.36a2.372 2.372 0 0 1 2.36-2.36zm10.141.392a1.253 1.253 0 0 1 1.296 1.434c-.049.319-.217.59-.453.78-.266.213-.61.318-.968.264a1.253 1.253 0 0 1-1.045-1.42 1.255 1.255 0 0 1 1.17-1.058zM5.384 17.425a2.02 2.02 0 0 1 1.917 1.298c.116.3.159.628.114.967a2.015 2.015 0 0 1-2.249 1.728 2.016 2.016 0 0 1-1.727-2.25 2.017 2.017 0 0 1 1.945-1.743z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
frontend/src/assets/Logos/hono.svg
Normal file
1
frontend/src/assets/Logos/hono.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#E36002" d="M12.445.002a45.529 45.529 0 0 0-5.252 8.146 8.595 8.595 0 0 1-.555-.53 27.796 27.796 0 0 0-1.205-1.542 8.762 8.762 0 0 0-1.251 2.12 20.743 20.743 0 0 0-1.448 5.88 8.867 8.867 0 0 0 .338 3.468c1.312 3.48 3.794 5.593 7.445 6.337 3.055.438 5.755-.333 8.097-2.312 2.677-2.59 3.359-5.634 2.047-9.132a33.287 33.287 0 0 0-2.988-5.59A91.34 91.34 0 0 0 12.615.053a.216.216 0 0 0-.17-.051Zm-.336 3.906a50.93 50.93 0 0 1 4.794 6.552c.448.767.817 1.57 1.108 2.41.606 2.386-.044 4.354-1.951 5.904-1.845 1.298-3.87 1.683-6.072 1.156-2.376-.737-3.75-2.335-4.121-4.794a5.107 5.107 0 0 1 .242-2.266c.358-.908.79-1.774 1.3-2.601l1.446-2.121a397.33 397.33 0 0 0 3.254-4.24Z"/></svg>
|
||||
|
After Width: | Height: | Size: 758 B |
1
frontend/src/assets/Logos/pm2.svg
Normal file
1
frontend/src/assets/Logos/pm2.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.6 KiB |
@@ -1,43 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button } from 'antd';
|
||||
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { MessageSquareText } from '@signozhq/icons';
|
||||
|
||||
export default function ChatSupportGateway(): JSX.Element {
|
||||
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
|
||||
useState(false);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="chat-support-gateway">
|
||||
<Button
|
||||
className="chat-support-gateway-btn"
|
||||
onClick={(): void => {
|
||||
logEvent('Disabled Chat Support: Clicked', {
|
||||
source: `chat support icon`,
|
||||
page: pathname,
|
||||
});
|
||||
|
||||
setIsAddCreditCardModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<MessageSquareText size={24} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AddCreditCardModal
|
||||
open={isAddCreditCardModalOpen}
|
||||
onClose={(): void => setIsAddCreditCardModalOpen(false)}
|
||||
onAddCreditCard={(): void => {
|
||||
logEvent('Add Credit card modal: Clicked', {
|
||||
source: `chat support icon`,
|
||||
page: pathname,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button, Col, Popover, Row, Select, Space } from 'antd';
|
||||
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import axios from 'axios';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
|
||||
import useErrorNotification from 'hooks/useErrorNotification';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
|
||||
import MenuItemGenerator from './MenuItemGenerator';
|
||||
import SaveViewWithName from './SaveViewWithName';
|
||||
import {
|
||||
DropDownOverlay,
|
||||
ExplorerCardHeadContainer,
|
||||
OffSetCol,
|
||||
} from './styles';
|
||||
import { ExplorerCardProps } from './types';
|
||||
import { deleteViewHandler } from './utils';
|
||||
import { Ellipsis, Save, Share2, Trash2 } from '@signozhq/icons';
|
||||
|
||||
function ExplorerCard({
|
||||
sourcepage,
|
||||
children,
|
||||
}: ExplorerCardProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [, setCopyUrl] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onCopyUrlHandler = (): void => {
|
||||
setCopyUrl(window.location.href);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
updateAllQueriesOperators,
|
||||
isStagedQueryUpdated,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const {
|
||||
data: viewsData,
|
||||
isLoading,
|
||||
error,
|
||||
isRefetching,
|
||||
refetch: refetchAllView,
|
||||
} = useGetAllViews(sourcepage);
|
||||
|
||||
useErrorNotification(error);
|
||||
|
||||
const handleOpenChange = (newOpen = false): void => {
|
||||
setIsOpen(newOpen);
|
||||
};
|
||||
|
||||
const { viewName, viewKey } = useGetSavedViewParams();
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey:
|
||||
sourcepage === DataSource.TRACES
|
||||
? LOCALSTORAGE.TRACES_LIST_OPTIONS
|
||||
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: sourcepage,
|
||||
aggregateOperator: StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const isQueryUpdated = isStagedQueryUpdated(
|
||||
viewsData?.data?.data,
|
||||
viewKey,
|
||||
options,
|
||||
);
|
||||
|
||||
const { mutateAsync: updateViewAsync } = useUpdateView({
|
||||
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
|
||||
viewKey,
|
||||
extraData: '',
|
||||
sourcePage: sourcepage,
|
||||
viewName,
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteViewAsync } = useDeleteView(viewKey);
|
||||
|
||||
const showErrorNotification = (err: Error): void => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteHandler = (): void =>
|
||||
deleteViewHandler({
|
||||
deleteViewAsync,
|
||||
notifications,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
viewId: viewKey,
|
||||
viewKey,
|
||||
updateAllQueriesOperators,
|
||||
sourcePage: sourcepage,
|
||||
});
|
||||
|
||||
const onUpdateQueryHandler = (): void => {
|
||||
updateViewAsync(
|
||||
{
|
||||
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
|
||||
viewKey,
|
||||
extraData: '',
|
||||
sourcePage: sourcepage,
|
||||
viewName,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'View Updated Successfully',
|
||||
});
|
||||
refetchAllView();
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorNotification(err);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const moreOptionItems: DropdownItemType[] = [
|
||||
{
|
||||
type: 'item',
|
||||
value: 'delete',
|
||||
label: <Typography.Text strong>Delete</Typography.Text>,
|
||||
onClick: onDeleteHandler,
|
||||
prefix: <Trash2 size="md" />,
|
||||
},
|
||||
];
|
||||
|
||||
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
|
||||
const saveButtonIcon = isQueryUpdated ? null : <Save size="md" />;
|
||||
|
||||
const showSaveView = false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSaveView && (
|
||||
<ExplorerCardHeadContainer size="small">
|
||||
<Row align="middle">
|
||||
<Col span={6}>
|
||||
<Space>
|
||||
<Typography>Query Builder</Typography>
|
||||
<TextToolTip
|
||||
url={ExploreHeaderToolTip.url}
|
||||
text={ExploreHeaderToolTip.text}
|
||||
useFilledIcon={false}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<OffSetCol span={18}>
|
||||
<Space size="large">
|
||||
{viewsData?.data.data && viewsData?.data.data.length && (
|
||||
<Space>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
loading={isLoading || isRefetching}
|
||||
showSearch
|
||||
placeholder="Select a view"
|
||||
dropdownStyle={DropDownOverlay}
|
||||
dropdownMatchSelectWidth={false}
|
||||
optionLabelProp="value"
|
||||
value={viewName || undefined}
|
||||
>
|
||||
{viewsData?.data.data.map((view) => (
|
||||
<Select.Option key={view.id} value={view.name}>
|
||||
<MenuItemGenerator
|
||||
viewName={view.name}
|
||||
viewKey={viewKey}
|
||||
createdBy={view.createdBy}
|
||||
uuid={view.id}
|
||||
refetchAllView={refetchAllView}
|
||||
viewData={viewsData.data.data}
|
||||
sourcePage={sourcepage}
|
||||
/>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
)}
|
||||
{isQueryUpdated && (
|
||||
<Button type="primary" icon={<Save />} onClick={onUpdateQueryHandler}>
|
||||
Save changes
|
||||
</Button>
|
||||
)}
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
content={
|
||||
<SaveViewWithName
|
||||
sourcePage={sourcepage}
|
||||
handlePopOverClose={handleOpenChange}
|
||||
refetchAllView={refetchAllView}
|
||||
/>
|
||||
}
|
||||
showArrow={false}
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<Button
|
||||
type={saveButtonType}
|
||||
icon={saveButtonIcon}
|
||||
data-testid="traces-save-view-action"
|
||||
>
|
||||
{isQueryUpdated
|
||||
? SaveButtonText.SAVE_AS_NEW_VIEW
|
||||
: SaveButtonText.SAVE_VIEW}
|
||||
</Button>
|
||||
</Popover>
|
||||
<Share2 onClick={onCopyUrlHandler} size="md" />
|
||||
{viewKey && (
|
||||
<Dropdown
|
||||
items={moreOptionItems}
|
||||
nativeButton
|
||||
align="end"
|
||||
side="bottom"
|
||||
>
|
||||
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
|
||||
</Dropdown>
|
||||
)}
|
||||
</Space>
|
||||
</OffSetCol>
|
||||
</Row>
|
||||
</ExplorerCardHeadContainer>
|
||||
)}
|
||||
|
||||
<div>{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerCard;
|
||||
@@ -1,103 +0,0 @@
|
||||
import { MouseEvent, useCallback } from 'react';
|
||||
import { Trash2 } from '@signozhq/icons';
|
||||
import { Col, Row, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
|
||||
import { MenuItemContainer } from './styles';
|
||||
import { MenuItemLabelGeneratorProps } from './types';
|
||||
import {
|
||||
deleteViewHandler,
|
||||
getViewDetailsUsingViewKey,
|
||||
trimViewName,
|
||||
} from './utils';
|
||||
|
||||
function MenuItemGenerator({
|
||||
viewName,
|
||||
viewKey,
|
||||
createdBy,
|
||||
uuid,
|
||||
viewData,
|
||||
refetchAllView,
|
||||
sourcePage,
|
||||
}: MenuItemLabelGeneratorProps): JSX.Element {
|
||||
const { panelType, redirectWithQueryBuilderData, updateAllQueriesOperators } =
|
||||
useQueryBuilder();
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { mutateAsync: deleteViewAsync } = useDeleteView(uuid);
|
||||
|
||||
const onDeleteHandler = (event: MouseEvent<SVGSVGElement>): void => {
|
||||
event.stopPropagation();
|
||||
deleteViewHandler({
|
||||
deleteViewAsync,
|
||||
notifications,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
viewId: uuid,
|
||||
viewKey,
|
||||
updateAllQueriesOperators,
|
||||
sourcePage,
|
||||
});
|
||||
};
|
||||
|
||||
const onMenuItemSelectHandler = useCallback(
|
||||
({ key }: { key: string }): void => {
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(key, viewData);
|
||||
if (!currentViewDetails) {
|
||||
return;
|
||||
}
|
||||
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
|
||||
|
||||
handleExplorerTabChange(currentPanelType, {
|
||||
query,
|
||||
viewName: name,
|
||||
viewKey: id,
|
||||
});
|
||||
},
|
||||
[viewData, handleExplorerTabChange],
|
||||
);
|
||||
|
||||
const onLabelClickHandler = (): void => {
|
||||
onMenuItemSelectHandler({
|
||||
key: uuid,
|
||||
});
|
||||
};
|
||||
|
||||
const newViewName = trimViewName(viewName);
|
||||
|
||||
return (
|
||||
<MenuItemContainer onClick={onLabelClickHandler}>
|
||||
<Row justify="space-between">
|
||||
<Col span={22}>
|
||||
<Row>
|
||||
<Tooltip title={viewName}>
|
||||
<Typography.Text strong>{newViewName}</Typography.Text>
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row>
|
||||
<Typography.Text color="muted">Created by {createdBy}</Typography.Text>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Typography.Link>
|
||||
<Trash2
|
||||
role="img"
|
||||
aria-label="Delete view"
|
||||
onClick={onDeleteHandler}
|
||||
size="md"
|
||||
/>
|
||||
</Typography.Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</MenuItemContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default MenuItemGenerator;
|
||||
@@ -1,79 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Card, Form } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSaveView } from 'hooks/saveViews/useSaveView';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
|
||||
import { SaveButton } from './styles';
|
||||
import { SaveViewFormProps, SaveViewWithNameProps } from './types';
|
||||
import { saveViewHandler } from './utils';
|
||||
|
||||
function SaveViewWithName({
|
||||
sourcePage,
|
||||
handlePopOverClose,
|
||||
refetchAllView,
|
||||
}: SaveViewWithNameProps): JSX.Element {
|
||||
const [form] = Form.useForm<SaveViewFormProps>();
|
||||
const { t } = useTranslation(['explorer']);
|
||||
const { currentQuery, panelType, redirectWithQueryBuilderData } =
|
||||
useQueryBuilder();
|
||||
const { notifications } = useNotifications();
|
||||
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||
|
||||
const { isLoading, mutateAsync: saveViewAsync } = useSaveView({
|
||||
viewName: form.getFieldValue('viewName'),
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
extraData: '',
|
||||
});
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
saveViewHandler({
|
||||
compositeQuery,
|
||||
handlePopOverClose,
|
||||
extraData: '',
|
||||
notifications,
|
||||
panelType: panelType || PANEL_TYPES.LIST,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
saveViewAsync,
|
||||
sourcePage,
|
||||
viewName: form.getFieldValue('viewName'),
|
||||
form,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Typography>{t('name_of_the_view')}</Typography>
|
||||
<Form form={form} onFinish={onSaveHandler} requiredMark>
|
||||
<Form.Item
|
||||
name={['viewName']}
|
||||
required
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please enter view name',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="Enter Name" />
|
||||
</Form.Item>
|
||||
<SaveButton
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
loading={isLoading}
|
||||
data-testid="save-view-name-action-button"
|
||||
>
|
||||
Save
|
||||
</SaveButton>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SaveViewWithName;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { ViewProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const viewMockData: ViewProps[] = [
|
||||
{
|
||||
id: 'view1',
|
||||
name: 'View 1',
|
||||
createdBy: 'User 1',
|
||||
category: 'category 1',
|
||||
compositeQuery: {} as ICompositeMetricQuery,
|
||||
createdAt: '2021-07-07T06:31:00.000Z',
|
||||
updatedAt: '2021-07-07T06:33:00.000Z',
|
||||
extraData: '',
|
||||
sourcePage: DataSource.TRACES,
|
||||
tags: [],
|
||||
updatedBy: 'User 1',
|
||||
},
|
||||
{
|
||||
id: 'view2',
|
||||
name: 'View 2',
|
||||
createdBy: 'User 2',
|
||||
category: 'category 2',
|
||||
compositeQuery: {} as ICompositeMetricQuery,
|
||||
createdAt: '2021-07-07T06:30:00.000Z',
|
||||
updatedAt: '2021-07-07T06:30:00.000Z',
|
||||
extraData: '',
|
||||
sourcePage: DataSource.TRACES,
|
||||
tags: [],
|
||||
updatedBy: 'User 2',
|
||||
},
|
||||
];
|
||||
@@ -1,9 +0,0 @@
|
||||
export const ExploreHeaderToolTip = {
|
||||
url: 'https://signoz.io/docs/querying/overview/?utm_source=product&utm_medium=new-query-builder',
|
||||
text: 'More details on how to use query builder',
|
||||
};
|
||||
|
||||
export const SaveButtonText = {
|
||||
SAVE_AS_NEW_VIEW: 'Save as new view',
|
||||
SAVE_VIEW: 'Save view',
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Button, Card, Col } from 'antd';
|
||||
import styled, { CSSProperties } from 'styled-components';
|
||||
|
||||
export const ExplorerCardHeadContainer = styled(Card)`
|
||||
margin: 1rem 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const OffSetCol = styled(Col)`
|
||||
text-align: right;
|
||||
`;
|
||||
|
||||
export const SaveButton = styled(Button)`
|
||||
&&& {
|
||||
margin: 1rem 0;
|
||||
width: 5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DropDownOverlay: CSSProperties = {
|
||||
maxHeight: '20rem',
|
||||
overflowY: 'auto',
|
||||
width: '20rem',
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
export const MenuItemContainer = styled(Card)`
|
||||
padding: 0;
|
||||
`;
|
||||
@@ -1,96 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { viewMockData } from '../__mock__/viewData';
|
||||
import ExplorerCard from '../ExplorerCard';
|
||||
|
||||
const historyReplace = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
|
||||
}),
|
||||
useHistory: (): any => ({
|
||||
...jest.requireActual('react-router-dom').useHistory(),
|
||||
replace: historyReplace,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
|
||||
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useGetAllViews', () => ({
|
||||
useGetAllViews: jest.fn(() => ({
|
||||
data: { data: { data: viewMockData } },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isRefetching: false,
|
||||
refetch: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useUpdateView', () => ({
|
||||
useUpdateView: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useDeleteView', () => ({
|
||||
useDeleteView: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock usePreferenceSync
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 1,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ExplorerCard', () => {
|
||||
it('renders a card with a title and a description', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a save view button', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { viewMockData } from '../__mock__/viewData';
|
||||
import MenuItemGenerator from '../MenuItemGenerator';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('antd', () => ({
|
||||
...jest.requireActual('antd'),
|
||||
useForm: jest.fn().mockReturnValue({
|
||||
onFinish: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('MenuItemGenerator', () => {
|
||||
it('should render MenuItemGenerator component', () => {
|
||||
const screen = render(
|
||||
<MockQueryClientProvider>
|
||||
<MenuItemGenerator
|
||||
viewName={viewMockData[0].name}
|
||||
viewKey={viewMockData[0].id}
|
||||
createdBy={viewMockData[0].createdBy}
|
||||
uuid={viewMockData[0].id}
|
||||
refetchAllView={jest.fn()}
|
||||
viewData={viewMockData}
|
||||
sourcePage={DataSource.TRACES}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(viewMockData[0].name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onMenuItemSelectHandler on click of MenuItemGenerator', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<MenuItemGenerator
|
||||
viewName={viewMockData[0].name}
|
||||
viewKey={viewMockData[0].id}
|
||||
createdBy={viewMockData[0].createdBy}
|
||||
uuid={viewMockData[0].id}
|
||||
refetchAllView={jest.fn()}
|
||||
viewData={viewMockData}
|
||||
sourcePage={DataSource.TRACES}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
const spanElement = screen.getByRole('img', {
|
||||
name: /delete view/i,
|
||||
});
|
||||
|
||||
expect(spanElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import SaveViewWithName from '../SaveViewWithName';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
|
||||
}),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
|
||||
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useSaveView', () => ({
|
||||
useSaveView: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('SaveViewWithName', () => {
|
||||
it('should render SaveViewWithName component', () => {
|
||||
const screen = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SaveViewWithName
|
||||
sourcePage={DataSource.TRACES}
|
||||
handlePopOverClose={jest.fn()}
|
||||
refetchAllView={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Save')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call saveViewAsync on click of Save button', () => {
|
||||
const screen = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SaveViewWithName
|
||||
sourcePage={DataSource.TRACES}
|
||||
handlePopOverClose={jest.fn()}
|
||||
refetchAllView={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Save'));
|
||||
|
||||
expect(screen.getByText('Save')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
import { UseMutateAsyncFunction } from 'react-query';
|
||||
import { FormInstance } from 'antd';
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
DeleteViewPayloadProps,
|
||||
SaveViewPayloadProps,
|
||||
SaveViewProps,
|
||||
ViewProps,
|
||||
} from 'types/api/saveViews/types';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
export interface ExplorerCardProps {
|
||||
sourcepage: DataSource;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export type GetViewDetailsUsingViewKey = (
|
||||
viewKey: string,
|
||||
data: ViewProps[] | undefined,
|
||||
) =>
|
||||
| {
|
||||
query: Query;
|
||||
name: string;
|
||||
id: string;
|
||||
panelType: PANEL_TYPES;
|
||||
extraData?: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
export interface IsQueryUpdatedInViewProps {
|
||||
viewKey: string;
|
||||
data: ViewProps[] | undefined;
|
||||
stagedQuery: Query | null;
|
||||
currentPanelType: PANEL_TYPES | null;
|
||||
options: OptionsQuery;
|
||||
}
|
||||
|
||||
export interface SaveViewWithNameProps {
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
handlePopOverClose: VoidFunction;
|
||||
refetchAllView: VoidFunction;
|
||||
}
|
||||
|
||||
export interface SaveViewFormProps {
|
||||
viewName: string;
|
||||
}
|
||||
|
||||
export interface MenuItemLabelGeneratorProps {
|
||||
viewName: string;
|
||||
viewKey: string;
|
||||
createdBy: string;
|
||||
uuid: string;
|
||||
viewData: ViewProps[];
|
||||
refetchAllView: VoidFunction;
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
}
|
||||
|
||||
export interface SaveViewHandlerProps {
|
||||
viewName: string;
|
||||
compositeQuery: ICompositeMetricQuery;
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
extraData: string;
|
||||
panelType: PANEL_TYPES | null;
|
||||
notifications: NotificationInstance;
|
||||
refetchAllView: SaveViewWithNameProps['refetchAllView'];
|
||||
saveViewAsync: UseMutateAsyncFunction<
|
||||
AxiosResponse<SaveViewPayloadProps>,
|
||||
Error,
|
||||
SaveViewProps,
|
||||
SaveViewPayloadProps
|
||||
>;
|
||||
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
|
||||
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
|
||||
form: FormInstance<SaveViewFormProps>;
|
||||
}
|
||||
|
||||
export interface DeleteViewHandlerProps {
|
||||
deleteViewAsync: UseMutateAsyncFunction<DeleteViewPayloadProps, Error, string>;
|
||||
refetchAllView: MenuItemLabelGeneratorProps['refetchAllView'];
|
||||
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
|
||||
notifications: NotificationInstance;
|
||||
panelType: PANEL_TYPES | null;
|
||||
viewKey: string;
|
||||
viewId: string;
|
||||
updateAllQueriesOperators: QueryBuilderContextType['updateAllQueriesOperators'];
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
DeleteViewHandlerProps,
|
||||
GetViewDetailsUsingViewKey,
|
||||
IsQueryUpdatedInViewProps,
|
||||
SaveViewHandlerProps,
|
||||
} from './types';
|
||||
|
||||
export const showErrorNotification = (
|
||||
notifications: NotificationInstance,
|
||||
err: Error,
|
||||
): void => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
|
||||
});
|
||||
};
|
||||
|
||||
export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
|
||||
viewKey,
|
||||
data,
|
||||
) => {
|
||||
const selectedView = data?.find((view) => view.id === viewKey);
|
||||
if (selectedView) {
|
||||
const { compositeQuery, name, id, extraData } = selectedView;
|
||||
const query = mapQueryDataFromApi(compositeQuery);
|
||||
return { query, name, id, panelType: compositeQuery.panelType, extraData };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const omitIdFromQuery = (query: Query | null): any => ({
|
||||
...query,
|
||||
builder: {
|
||||
...query?.builder,
|
||||
queryData: query?.builder.queryData.map((queryData) => {
|
||||
const { id: _aggregateAttributeId, ...rest } =
|
||||
queryData.aggregateAttribute || {};
|
||||
const newAggregateAttribute = rest;
|
||||
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
||||
const { id: _groupByAttributeId, ...rest } = groupByAttribute;
|
||||
return rest;
|
||||
});
|
||||
const newItems = queryData.filters?.items?.map((item) => {
|
||||
const { id: _itemId, ...newItem } = item;
|
||||
if (item.key) {
|
||||
const { id: _keyId, ...rest } = item.key;
|
||||
return {
|
||||
...newItem,
|
||||
key: rest,
|
||||
};
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
return {
|
||||
...queryData,
|
||||
aggregateAttribute: newAggregateAttribute,
|
||||
groupBy: newGroupByAttributes,
|
||||
filters: {
|
||||
...queryData.filters,
|
||||
items: newItems,
|
||||
},
|
||||
limit: queryData.limit ? queryData.limit : 0,
|
||||
offset: queryData.offset ? queryData.offset : 0,
|
||||
pageSize: queryData.pageSize ? queryData.pageSize : 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const isQueryUpdatedInView = ({
|
||||
viewKey,
|
||||
data,
|
||||
stagedQuery,
|
||||
currentPanelType,
|
||||
options,
|
||||
}: IsQueryUpdatedInViewProps): boolean => {
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
|
||||
if (!currentViewDetails) {
|
||||
return false;
|
||||
}
|
||||
const { query, panelType, extraData } = currentViewDetails;
|
||||
|
||||
// Omitting id from aggregateAttribute and groupBy
|
||||
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
|
||||
|
||||
if (
|
||||
updatedCurrentQuery?.builder === undefined ||
|
||||
updatedCurrentQuery.clickhouse_sql === undefined ||
|
||||
updatedCurrentQuery.promql === undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
panelType !== currentPanelType ||
|
||||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
|
||||
!isEqual(query.clickhouse_sql, updatedCurrentQuery?.clickhouse_sql) ||
|
||||
!isEqual(query.promql, updatedCurrentQuery?.promql) ||
|
||||
!isEqual(
|
||||
options?.selectColumns,
|
||||
extraData && JSON.parse(extraData)?.selectColumns,
|
||||
) ||
|
||||
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
|
||||
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
|
||||
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
|
||||
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
|
||||
);
|
||||
};
|
||||
|
||||
export const saveViewHandler = ({
|
||||
saveViewAsync,
|
||||
refetchAllView,
|
||||
notifications,
|
||||
handlePopOverClose,
|
||||
viewName,
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
extraData,
|
||||
redirectWithQueryBuilderData,
|
||||
panelType,
|
||||
form,
|
||||
}: SaveViewHandlerProps): void => {
|
||||
saveViewAsync(
|
||||
{
|
||||
viewName,
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
extraData,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
refetchAllView();
|
||||
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
|
||||
[QueryParams.panelTypes]: panelType,
|
||||
[QueryParams.viewName]: viewName,
|
||||
[QueryParams.viewKey]: data.data.data,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'View Saved Successfully',
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorNotification(notifications, err);
|
||||
},
|
||||
onSettled: () => {
|
||||
handlePopOverClose();
|
||||
form.resetFields();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteViewHandler = ({
|
||||
deleteViewAsync,
|
||||
refetchAllView,
|
||||
redirectWithQueryBuilderData,
|
||||
notifications,
|
||||
panelType,
|
||||
viewKey,
|
||||
viewId,
|
||||
updateAllQueriesOperators,
|
||||
sourcePage,
|
||||
}: DeleteViewHandlerProps): void => {
|
||||
deleteViewAsync(viewKey, {
|
||||
onSuccess: () => {
|
||||
if (viewId === viewKey) {
|
||||
redirectWithQueryBuilderData(
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap[sourcePage],
|
||||
panelType || PANEL_TYPES.LIST,
|
||||
sourcePage,
|
||||
),
|
||||
{
|
||||
[QueryParams.viewName]: '',
|
||||
[QueryParams.panelTypes]: panelType,
|
||||
[QueryParams.viewKey]: '',
|
||||
},
|
||||
);
|
||||
}
|
||||
notifications.success({
|
||||
message: 'View Deleted Successfully',
|
||||
});
|
||||
refetchAllView();
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorNotification(notifications, err);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const trimViewName = (viewName: string): string => {
|
||||
if (viewName.length > 20) {
|
||||
return `${viewName.substring(0, 20)}...`;
|
||||
}
|
||||
return viewName;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DrawerProps } from 'antd';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getBuilderVariant } from 'lib/query/queryMode';
|
||||
import {
|
||||
BuilderVariant,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderField } from './queryBuilderFields.types';
|
||||
@@ -64,20 +59,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
[config],
|
||||
);
|
||||
|
||||
const isAIQuery =
|
||||
getBuilderVariant(currentQuery.builder) === BuilderVariant.AI;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
|
||||
// AI queries are traces-only; BAR must not force metrics on them.
|
||||
if (isAIQuery) {
|
||||
handleSetConfig(newPanelType, DataSource.TRACES);
|
||||
return;
|
||||
}
|
||||
if (newPanelType === PANEL_TYPES.BAR) {
|
||||
handleSetConfig(PANEL_TYPES.BAR, DataSource.METRICS);
|
||||
return;
|
||||
}
|
||||
handleSetConfig(newPanelType, currentDataSource);
|
||||
}
|
||||
}, [
|
||||
@@ -86,7 +69,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
initialDataSource,
|
||||
currentDataSource,
|
||||
newPanelType,
|
||||
isAIQuery,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -130,7 +130,6 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
updateAllQueriesOperators,
|
||||
updateQueriesData,
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
} as unknown as QueryBuilderContextType;
|
||||
|
||||
|
||||
@@ -136,7 +136,6 @@ describe('MetricsSelect - signal source switching (standalone)', () => {
|
||||
updateAllQueriesOperators: ((q: any) => q) as any,
|
||||
updateQueriesData: ((q: any) => q) as any,
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
});
|
||||
});
|
||||
@@ -293,7 +292,6 @@ describe('DataSource change - Logs to Traces', () => {
|
||||
updateAllQueriesOperators: ((q: any) => q) as any,
|
||||
updateQueriesData: ((q: any) => q) as any,
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('../QueryV2/QueryV2', () => ({ QueryV2: (): null => null }));
|
||||
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
|
||||
|
||||
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
|
||||
const aiQuery: Query = {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: initialQueriesMap.traces.builder.queryData.map((query) => ({
|
||||
...query,
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
function mockContext({
|
||||
panelType = null,
|
||||
initialDataSource = null,
|
||||
currentQuery = initialQueriesMap.metrics,
|
||||
}: {
|
||||
panelType?: PANEL_TYPES | null;
|
||||
initialDataSource?: DataSource | null;
|
||||
currentQuery?: Query;
|
||||
}): jest.Mock {
|
||||
const handleSetConfig = jest.fn();
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
currentQuery,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
handleSetConfig,
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
handleRunQuery: jest.fn(),
|
||||
} as unknown as ReturnType<typeof useQueryBuilder>);
|
||||
return handleSetConfig;
|
||||
}
|
||||
|
||||
describe('QueryBuilderV2 provider config sync', () => {
|
||||
it('leaves the data source unset for a bar panel without a static config', () => {
|
||||
const handleSetConfig = mockContext({});
|
||||
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.BAR} version="v3" />);
|
||||
|
||||
expect(handleSetConfig).toHaveBeenCalledWith(PANEL_TYPES.BAR, null);
|
||||
});
|
||||
|
||||
it('syncs the static data source', () => {
|
||||
const handleSetConfig = mockContext({});
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
version="v3"
|
||||
panelType={PANEL_TYPES.BAR}
|
||||
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(handleSetConfig).toHaveBeenCalledWith(
|
||||
PANEL_TYPES.BAR,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps an AI query on traces for a bar panel', () => {
|
||||
const handleSetConfig = mockContext({ currentQuery: aiQuery });
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
version="v3"
|
||||
panelType={PANEL_TYPES.BAR}
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(handleSetConfig).toHaveBeenCalledWith(
|
||||
PANEL_TYPES.BAR,
|
||||
DataSource.TRACES,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not reset the config when the provider already matches', () => {
|
||||
const handleSetConfig = mockContext({
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
initialDataSource: DataSource.TRACES,
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
version="v3"
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(handleSetConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -97,7 +97,7 @@ interface ToggleAction {
|
||||
isOnlyOrAllClicked?: boolean;
|
||||
previousState?: CheckedState;
|
||||
sectionType?: SectionType;
|
||||
source?: QuickFiltersSource;
|
||||
quickFilterSource?: QuickFiltersSource;
|
||||
attributeValues?: string[];
|
||||
}
|
||||
|
||||
@@ -117,7 +117,8 @@ function runToggle(c: ToggleCase): { items: SimpleItem[]; expression: string } {
|
||||
currentQuery: buildQuery(initialItems, initialExpression),
|
||||
activeQueryIndex: 0,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
source: c.action.source ?? QuickFiltersSource.LOGS_EXPLORER,
|
||||
quickFilterSource:
|
||||
c.action.quickFilterSource ?? QuickFiltersSource.LOGS_EXPLORER,
|
||||
attributeValues: c.action.attributeValues ?? ['a', 'b', 'c'],
|
||||
value: c.action.value,
|
||||
checked: c.action.checked,
|
||||
@@ -162,7 +163,7 @@ const TOGGLE_CASES: ToggleCase[] = [
|
||||
action: {
|
||||
value: 'a',
|
||||
checked: false,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
quickFilterSource: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
// `nin` is what the source asks for, but re-deriving the expression
|
||||
// normalises it. Nothing observes the difference: both infra pages send
|
||||
@@ -313,7 +314,7 @@ const TOGGLE_CASES: ToggleCase[] = [
|
||||
action: {
|
||||
value: 'b',
|
||||
checked: false,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
quickFilterSource: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
|
||||
@@ -46,14 +46,18 @@ export function removeManagedClauses(expression: string, key: string): string {
|
||||
}
|
||||
|
||||
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
|
||||
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
|
||||
const SOURCES_WITH_SHORT_OPERATORS: QuickFiltersSource[] = [
|
||||
QuickFiltersSource.INFRA_MONITORING,
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the correct NOT_IN operator value based on source.
|
||||
* InfraMonitoring backend expects 'nin', others expect 'not in'.
|
||||
*/
|
||||
export function getNotInOperator(source: QuickFiltersSource): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(source)) {
|
||||
export function getNotInOperator(
|
||||
quickFilterSource: QuickFiltersSource,
|
||||
): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(quickFilterSource)) {
|
||||
return 'nin';
|
||||
}
|
||||
return getOperatorValue('NOT_IN');
|
||||
@@ -134,7 +138,7 @@ export function applyCheckboxToggle({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
source,
|
||||
quickFilterSource,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
@@ -145,7 +149,7 @@ export function applyCheckboxToggle({
|
||||
currentQuery: Query;
|
||||
activeQueryIndex: number;
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
value: string;
|
||||
checked: boolean;
|
||||
@@ -240,7 +244,7 @@ export function applyCheckboxToggle({
|
||||
if (sectionType === SectionType.RELATED) {
|
||||
const newFilter: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getNotInOperator(source),
|
||||
op: getNotInOperator(quickFilterSource),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
@@ -380,7 +384,7 @@ export function applyCheckboxToggle({
|
||||
if (!checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getNotInOperator(source),
|
||||
op: getNotInOperator(quickFilterSource),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
@@ -404,7 +408,9 @@ export function applyCheckboxToggle({
|
||||
// checked=true → user wants to select (IN), checked=false → exclude (NOT IN)
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: checked ? getOperatorValue(OPERATORS.IN) : getNotInOperator(source),
|
||||
op: checked
|
||||
? getOperatorValue(OPERATORS.IN)
|
||||
: getNotInOperator(quickFilterSource),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SectionType } from './v2/itemRules';
|
||||
|
||||
interface UseCheckboxFilterActionsProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
activeQueryIndex: number;
|
||||
onFilterChange?: ((query: Query) => void) | null;
|
||||
@@ -38,7 +38,7 @@ interface UseCheckboxFilterActionsReturn {
|
||||
*/
|
||||
function useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
quickFilterSource,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
@@ -65,7 +65,7 @@ function useCheckboxFilterActions({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
source,
|
||||
quickFilterSource,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { render, RenderResult } from 'tests/test-utils';
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
@@ -21,7 +20,6 @@ export const DEFAULT_FILTER: IQuickFiltersConfig = {
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
};
|
||||
|
||||
@@ -158,7 +156,7 @@ export function renderWithFilter(
|
||||
return render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
|
||||
@@ -33,7 +33,7 @@ import styles from './CheckboxFilterV2.module.scss';
|
||||
|
||||
interface CheckboxFilterV2Props {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
|
||||
useFieldApis: QuickFilterCheckboxUseFieldApis;
|
||||
@@ -42,13 +42,18 @@ interface CheckboxFilterV2Props {
|
||||
export default function CheckboxFilterV2(
|
||||
props: CheckboxFilterV2Props,
|
||||
): JSX.Element {
|
||||
const { source, filter, onFilterChange, onQuickFilterChange, useFieldApis } =
|
||||
props;
|
||||
const {
|
||||
quickFilterSource,
|
||||
filter,
|
||||
onFilterChange,
|
||||
onQuickFilterChange,
|
||||
useFieldApis,
|
||||
} = props;
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@@ -75,7 +80,7 @@ export default function CheckboxFilterV2(
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace: useFieldApis.metricNamespace,
|
||||
source,
|
||||
quickFilterSource,
|
||||
startUnixMilli: useFieldApis.startUnixMilli,
|
||||
endUnixMilli: useFieldApis.endUnixMilli,
|
||||
enabled: isOpen,
|
||||
@@ -104,7 +109,7 @@ export default function CheckboxFilterV2(
|
||||
|
||||
const { onChange, onClear } = useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
quickFilterSource,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
quickFilterSource={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -44,7 +44,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
quickFilterSource={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -59,6 +59,8 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
expect(params.get('endUnixMilli')).toBe(
|
||||
String(DEFAULT_USE_FIELD_APIS.endUnixMilli),
|
||||
);
|
||||
expect(params.get('signal')).toBeNull();
|
||||
expect(params.get('source')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps non-AI sources on the signal-wide endpoint', async () => {
|
||||
@@ -70,7 +72,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'custom.query = "value"',
|
||||
@@ -82,7 +82,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: null,
|
||||
@@ -116,7 +116,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -154,7 +154,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -185,7 +185,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -222,7 +222,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -264,7 +264,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import {
|
||||
TelemetrytypesSignalDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import { QuickFiltersSource } from '../../../../types';
|
||||
|
||||
import CheckboxFilterV2 from '../CheckboxFilterV2';
|
||||
import {
|
||||
DEFAULT_FILTER,
|
||||
DEFAULT_USE_FIELD_APIS,
|
||||
setupServer,
|
||||
} from '../CheckboxFilterV2.testUtils';
|
||||
|
||||
setupServer();
|
||||
|
||||
describe('CheckboxFilterV2 - field APIs', () => {
|
||||
it('sends the signal and source of the quick filter source to the signal-wide endpoint', async () => {
|
||||
const requests: URLSearchParams[] = [];
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
|
||||
requests.push(req.url.searchParams);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { values: { stringValues: ['ingest'] } },
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.METER_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText('ingest')).resolves.toBeInTheDocument();
|
||||
expect(requests[0].get('signal')).toBe(TelemetrytypesSignalDTO.metrics);
|
||||
expect(requests[0].get('source')).toBe(TelemetrytypesSourceDTO.meter);
|
||||
});
|
||||
});
|
||||
@@ -72,7 +72,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -101,7 +101,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -119,7 +119,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -173,7 +173,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -240,7 +240,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -288,7 +288,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -330,7 +330,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -394,7 +394,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -440,7 +440,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -468,7 +468,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -490,7 +490,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -516,7 +516,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -568,7 +568,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -601,7 +601,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -627,7 +627,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -652,7 +652,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -691,7 +691,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -714,7 +714,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
onFilterChange={onFilterChange}
|
||||
/>,
|
||||
@@ -761,7 +761,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
onFilterChange={onFilterChange}
|
||||
/>,
|
||||
@@ -837,7 +837,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
onFilterChange={onFilterChange}
|
||||
/>,
|
||||
@@ -971,7 +971,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, customRendererForValue: customRenderer }}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -55,7 +55,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -106,7 +106,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -140,7 +140,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -185,7 +185,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -221,7 +221,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -273,7 +273,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -321,7 +321,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -373,7 +373,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -424,7 +424,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -466,7 +466,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -521,7 +521,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -47,7 +47,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={closedFilter}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -104,7 +104,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -133,7 +133,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -152,7 +152,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -172,7 +172,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -195,7 +195,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useMemo } from 'react';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldValuesConfig } from 'api/querySuggestions/types';
|
||||
import { QUICK_FILTER_SOURCE_TO_SIGNAL } from 'components/QuickFilters/constants';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { useFieldValuesSuggestion } from 'hooks/querySuggestions/useFieldValuesSuggestion';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseFieldValuesProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
searchText: string;
|
||||
existingQuery?: string;
|
||||
metricNamespace?: string;
|
||||
source?: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
startUnixMilli?: number;
|
||||
endUnixMilli?: number;
|
||||
enabled: boolean;
|
||||
@@ -38,12 +38,13 @@ export function useFieldValues({
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
source,
|
||||
quickFilterSource,
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
enabled,
|
||||
}: UseFieldValuesProps): UseFieldValuesReturn {
|
||||
const isAIObservability = source === QuickFiltersSource.AI_OBSERVABILITY;
|
||||
const isAIObservability =
|
||||
quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY;
|
||||
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
@@ -59,14 +60,12 @@ export function useFieldValues({
|
||||
endUnixMilli,
|
||||
}
|
||||
: {
|
||||
signal: filter.dataSource
|
||||
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
|
||||
: undefined,
|
||||
signal: QUICK_FILTER_SOURCE_TO_SIGNAL[quickFilterSource],
|
||||
name: filter.attributeKey.key,
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
|
||||
source: QUICK_FILTERS_SOURCE_TO_SOURCE[quickFilterSource],
|
||||
startUnixMilli,
|
||||
// This field does not affect the backend but I wanted to keep it here
|
||||
// in case we add the support in the future
|
||||
|
||||
@@ -31,11 +31,11 @@ export type { FilterType };
|
||||
function Duration({
|
||||
filter,
|
||||
onFilterChange,
|
||||
source,
|
||||
quickFilterSource,
|
||||
}: {
|
||||
filter: IQuickFiltersConfig;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
}): JSX.Element {
|
||||
const [selectedFilters, setSelectedFilters] =
|
||||
useState<
|
||||
@@ -52,7 +52,7 @@ function Duration({
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const syncSelectedFilters = useMemo((): FilterType => {
|
||||
|
||||
@@ -49,15 +49,18 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
className,
|
||||
config,
|
||||
handleFilterVisibilityChange,
|
||||
source,
|
||||
quickFilterSource,
|
||||
onFilterChange,
|
||||
onQuickFilterChange,
|
||||
signal,
|
||||
showFilterCollapse = true,
|
||||
showQueryName = true,
|
||||
useFieldApis,
|
||||
savedViewsHeader,
|
||||
} = props;
|
||||
const savedFiltersSource =
|
||||
quickFilterSource === QuickFiltersSource.INFRA_MONITORING
|
||||
? undefined
|
||||
: quickFilterSource;
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [params, setParams] = useApiMonitoringParams();
|
||||
const showIP = params.showIP ?? true;
|
||||
@@ -68,7 +71,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
customFilters,
|
||||
refetchCustomFilters,
|
||||
isCustomFiltersLoading,
|
||||
} = useFilterConfig({ signal, config });
|
||||
} = useFilterConfig({ savedFiltersSource, config });
|
||||
const {
|
||||
deniedPermissions: deniedSettingsPermissions,
|
||||
isLoading: isSettingsChecking,
|
||||
@@ -112,15 +115,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
// Show dropdown in ListView only for TRACES_EXPLORER source
|
||||
const shouldShowDropdownInListView =
|
||||
isListView && source === QuickFiltersSource.TRACES_EXPLORER;
|
||||
isListView && quickFilterSource === QuickFiltersSource.TRACES_EXPLORER;
|
||||
|
||||
// AI observability builds a single query in the row-level views, so there is
|
||||
// no query for the selector to switch between.
|
||||
const isAIObservabilityRowView =
|
||||
source === QuickFiltersSource.AI_OBSERVABILITY &&
|
||||
quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY &&
|
||||
(isListView || panelType === PANEL_TYPES.TRACE);
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
|
||||
|
||||
// clear all the filters for the query which is in sync with filters
|
||||
const handleReset = (): void => {
|
||||
@@ -290,7 +293,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
const renderContent = (): JSX.Element => (
|
||||
<>
|
||||
{source === QuickFiltersSource.API_MONITORING && (
|
||||
{quickFilterSource === QuickFiltersSource.API_MONITORING && (
|
||||
<div className="api-quick-filters-header">
|
||||
<Typography.Text>Show IP addresses</Typography.Text>
|
||||
<Switch
|
||||
@@ -313,7 +316,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
return (
|
||||
<Duration
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
quickFilterSource={quickFilterSource}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
@@ -324,7 +327,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
return (
|
||||
<CheckboxV2
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
quickFilterSource={quickFilterSource}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
onQuickFilterChange={onQuickFilterChange}
|
||||
@@ -363,14 +366,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<div className="quick-filters-container">
|
||||
<div className="quick-filters">
|
||||
{savedViewsHeader}
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
{quickFilterSource !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
<section className="header">
|
||||
{renderLeftActions()}
|
||||
{renderRightActions()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{signal ? (
|
||||
{savedFiltersSource ? (
|
||||
<AuthZGuardContent
|
||||
checks={[QuickFilterReadPermission]}
|
||||
fallbackOnLoading={filtersSkeleton}
|
||||
@@ -391,9 +394,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isSettingsOpen && (
|
||||
{isSettingsOpen && savedFiltersSource && (
|
||||
<QuickFiltersSettings
|
||||
signal={signal}
|
||||
savedFiltersSource={savedFiltersSource}
|
||||
setIsSettingsOpen={setIsSettingsOpen}
|
||||
customFilters={customFilters}
|
||||
refetchCustomFilters={refetchCustomFilters}
|
||||
@@ -407,7 +410,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
QuickFilters.defaultProps = {
|
||||
onFilterChange: null,
|
||||
signal: '',
|
||||
config: [],
|
||||
showFilterCollapse: true,
|
||||
showQueryName: true,
|
||||
|
||||
@@ -5,7 +5,6 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import SavedViewsHeader from 'container/SavedViews/SavedViewsHeader';
|
||||
import SavedViewsPanel from 'container/SavedViews/SavedViewsPanel';
|
||||
import SavedViewsRestore from 'container/SavedViews/SavedViewsRestore';
|
||||
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { SIDEBAR_TOOLTIP_DELAY_MS } from './constants';
|
||||
@@ -50,10 +49,9 @@ function QuickFiltersLayout({
|
||||
children,
|
||||
}: QuickFiltersLayoutProps): JSX.Element {
|
||||
const [isViewsListOpen, setIsViewsListOpen] = useState(false);
|
||||
const isSavedViewEnabled = useSavedViewEnabled();
|
||||
|
||||
const hasQuickFilters = !!quickFilterProps;
|
||||
const hasSavedViews = !!savedViewProps && isSavedViewEnabled;
|
||||
const hasSavedViews = !!savedViewProps;
|
||||
|
||||
const showSidebar = showFilters && (hasQuickFilters || hasSavedViews);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import { QuickFiltersSource } from '../../types';
|
||||
@@ -20,10 +19,6 @@ jest.mock('../QuickFiltersLayout.module.scss', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSavedViewEnabled', () => ({
|
||||
useSavedViewEnabled: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
jest.mock('container/SavedViews/SavedViewsRestore', () => ({
|
||||
__esModule: true,
|
||||
default: ({ source }: { source: string }): JSX.Element => (
|
||||
@@ -34,17 +29,17 @@ jest.mock('container/SavedViews/SavedViewsRestore', () => ({
|
||||
jest.mock('../../QuickFilters', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
source,
|
||||
quickFilterSource,
|
||||
savedViewsHeader,
|
||||
handleFilterVisibilityChange,
|
||||
}: {
|
||||
source: string;
|
||||
quickFilterSource: string;
|
||||
savedViewsHeader?: React.ReactNode;
|
||||
handleFilterVisibilityChange?: () => void;
|
||||
}): JSX.Element => (
|
||||
<div data-testid="quick-filters">
|
||||
{savedViewsHeader}
|
||||
{source}
|
||||
{quickFilterSource}
|
||||
{handleFilterVisibilityChange && (
|
||||
<Button
|
||||
size="md"
|
||||
@@ -62,7 +57,7 @@ jest.mock('../../QuickFilters', () => ({
|
||||
}));
|
||||
|
||||
const quickFilterProps = {
|
||||
source: QuickFiltersSource.TRACES_EXPLORER,
|
||||
quickFilterSource: QuickFiltersSource.TRACES_EXPLORER,
|
||||
useFieldApis: { startUnixMilli: 0, endUnixMilli: 0 },
|
||||
};
|
||||
const savedViewProps = { source: SavedviewtypesSourceDTO.traces };
|
||||
@@ -71,10 +66,6 @@ const drawer = (): HTMLElement =>
|
||||
screen.getByTestId('quick-filters-layout-drawer');
|
||||
|
||||
describe('QuickFiltersLayout', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(useSavedViewEnabled).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('renders QuickFilters with the given props inside the filters pane', () => {
|
||||
render(
|
||||
<QuickFiltersLayout showFilters quickFilterProps={quickFilterProps}>
|
||||
@@ -149,35 +140,6 @@ describe('QuickFiltersLayout', () => {
|
||||
});
|
||||
|
||||
describe('saved views', () => {
|
||||
it('ignores savedViewProps while the flag is off', () => {
|
||||
jest.mocked(useSavedViewEnabled).mockReturnValue(false);
|
||||
const { unmount } = render(
|
||||
<QuickFiltersLayout
|
||||
showFilters
|
||||
quickFilterProps={quickFilterProps}
|
||||
savedViewProps={savedViewProps}
|
||||
>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('saved-views-header')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('saved-views-panel')).not.toBeInTheDocument();
|
||||
|
||||
// Saved views only, so nothing is left to show.
|
||||
unmount();
|
||||
render(
|
||||
<QuickFiltersLayout showFilters savedViewProps={savedViewProps}>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('quick-filters-layout-filters'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('saved-views-restore')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the panel from the header and slides the quick filters drawer, keeping the same QuickFilters node', async () => {
|
||||
render(
|
||||
<QuickFiltersLayout
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
QuickfiltertypesSourceDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldKeysConfig } from 'api/querySuggestions/types';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { QUICK_FILTER_SOURCE_TO_SIGNAL } from 'components/QuickFilters/constants';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import {
|
||||
@@ -13,7 +15,6 @@ import {
|
||||
FieldDataType,
|
||||
TelemetryFieldKey,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
function OtherFiltersSkeleton(): JSX.Element {
|
||||
return (
|
||||
@@ -32,18 +33,20 @@ function OtherFiltersSkeleton(): JSX.Element {
|
||||
}
|
||||
|
||||
function OtherFilters({
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
inputValue,
|
||||
addedFilters,
|
||||
setAddedFilters,
|
||||
}: {
|
||||
signal: SignalType | undefined;
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO;
|
||||
inputValue: string;
|
||||
addedFilters: TelemetryFieldKey[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
}): JSX.Element {
|
||||
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
|
||||
const isAIObservability = signal === SignalType.AI_OBSERVABILITY;
|
||||
const isMeterDataSource =
|
||||
savedFiltersSource === QuickfiltertypesSourceDTO.meter;
|
||||
const isAIObservability =
|
||||
savedFiltersSource === QuickfiltertypesSourceDTO.ai_observability;
|
||||
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
@@ -53,9 +56,7 @@ function OtherFilters({
|
||||
? { searchText: inputValue }
|
||||
: {
|
||||
searchText: inputValue,
|
||||
signal: signal
|
||||
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
|
||||
: undefined,
|
||||
signal: QUICK_FILTER_SOURCE_TO_SIGNAL[savedFiltersSource],
|
||||
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Button as SignozButton } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { SignalType } from '../types';
|
||||
import AddedFilters from './AddedFilters';
|
||||
import useQuickFilterSettings from './hooks/useQuickFilterSettings';
|
||||
import OtherFilters from './OtherFilters';
|
||||
@@ -12,12 +12,12 @@ import OtherFilters from './OtherFilters';
|
||||
import './QuickFiltersSettings.styles.scss';
|
||||
|
||||
function QuickFiltersSettings({
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
setIsSettingsOpen,
|
||||
customFilters,
|
||||
refetchCustomFilters,
|
||||
}: {
|
||||
signal: SignalType | undefined;
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO;
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
@@ -37,7 +37,7 @@ function QuickFiltersSettings({
|
||||
setIsSettingsOpen,
|
||||
customFilters,
|
||||
refetchCustomFilters,
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -73,7 +73,7 @@ function QuickFiltersSettings({
|
||||
setAddedFilters={setAddedFilters}
|
||||
/>
|
||||
<OtherFilters
|
||||
signal={signal}
|
||||
savedFiltersSource={savedFiltersSource}
|
||||
inputValue={debouncedInputValue}
|
||||
addedFilters={addedFilters}
|
||||
setAddedFilters={setAddedFilters}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { SignalType } from '../../types';
|
||||
import OtherFilters from '../OtherFilters';
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
@@ -43,10 +43,12 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
);
|
||||
});
|
||||
|
||||
function renderOtherFilters(signal: SignalType): void {
|
||||
function renderOtherFilters(
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO,
|
||||
): void {
|
||||
render(
|
||||
<OtherFilters
|
||||
signal={signal}
|
||||
savedFiltersSource={savedFiltersSource}
|
||||
inputValue=""
|
||||
addedFilters={[]}
|
||||
setAddedFilters={jest.fn()}
|
||||
@@ -55,7 +57,7 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
}
|
||||
|
||||
it('reads AI observability keys from their own endpoint', async () => {
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
renderOtherFilters(QuickfiltertypesSourceDTO.ai_observability);
|
||||
|
||||
await expect(
|
||||
screen.findByText('gen_ai.request.model'),
|
||||
@@ -64,7 +66,7 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
});
|
||||
|
||||
it('does not narrow the AI keys by fieldContext', async () => {
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
renderOtherFilters(QuickfiltertypesSourceDTO.ai_observability);
|
||||
|
||||
// A `trace` context would return only the computed per-trace aggregates,
|
||||
// which cannot be filtered on.
|
||||
@@ -73,7 +75,7 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
});
|
||||
|
||||
it('keeps other signals on the signal-wide keys endpoint', async () => {
|
||||
renderOtherFilters(SignalType.TRACES);
|
||||
renderOtherFilters(QuickfiltertypesSourceDTO.traces);
|
||||
|
||||
await expect(screen.findByText('http.route')).resolves.toBeInTheDocument();
|
||||
await waitFor(() => expect(aiKeysParams).toBeUndefined());
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const SIGNAL_DATA_SOURCE_MAP = {
|
||||
[SignalType.LOGS]: DataSource.LOGS,
|
||||
[SignalType.TRACES]: DataSource.TRACES,
|
||||
[SignalType.EXCEPTIONS]: DataSource.TRACES,
|
||||
[SignalType.API_MONITORING]: DataSource.TRACES,
|
||||
[SignalType.METER_EXPLORER]: DataSource.METRICS,
|
||||
[SignalType.AI_OBSERVABILITY]: DataSource.TRACES,
|
||||
};
|
||||
@@ -4,8 +4,8 @@ import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesFieldDataTypeDTO,
|
||||
QuickfiltertypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
@@ -16,7 +16,7 @@ interface UseQuickFilterSettingsProps {
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
signal?: SignalType;
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO;
|
||||
}
|
||||
|
||||
interface UseQuickFilterSettingsReturn {
|
||||
@@ -37,7 +37,7 @@ const useQuickFilterSettings = ({
|
||||
customFilters,
|
||||
setIsSettingsOpen,
|
||||
refetchCustomFilters,
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => {
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const [debouncedInputValue, setDebouncedInputValue] = useState<string>('');
|
||||
@@ -113,21 +113,19 @@ const useQuickFilterSettings = ({
|
||||
);
|
||||
|
||||
const handleSaveChanges = useCallback((): void => {
|
||||
if (signal) {
|
||||
updateCustomFilters({
|
||||
pathParams: { source: signal },
|
||||
data: {
|
||||
// Send only the stored TelemetryFieldKey fields; the composite `key`
|
||||
// is UI-only.
|
||||
filters: addedFilters.map((filter) => ({
|
||||
name: filter.name,
|
||||
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
|
||||
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [addedFilters, signal, updateCustomFilters]);
|
||||
updateCustomFilters({
|
||||
pathParams: { source: savedFiltersSource },
|
||||
data: {
|
||||
// Send only the stored TelemetryFieldKey fields; the composite `key`
|
||||
// is UI-only.
|
||||
filters: addedFilters.map((filter) => ({
|
||||
name: filter.name,
|
||||
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
|
||||
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}, [addedFilters, savedFiltersSource, updateCustomFilters]);
|
||||
|
||||
return {
|
||||
handleSettingsClose,
|
||||
|
||||
16
frontend/src/components/QuickFilters/constants.ts
Normal file
16
frontend/src/components/QuickFilters/constants.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { QuickFiltersSource } from './types';
|
||||
|
||||
export const QUICK_FILTER_SOURCE_TO_SIGNAL: Record<
|
||||
QuickFiltersSource,
|
||||
TelemetrytypesSignalDTO
|
||||
> = {
|
||||
[QuickFiltersSource.LOGS_EXPLORER]: TelemetrytypesSignalDTO.logs,
|
||||
[QuickFiltersSource.TRACES_EXPLORER]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.EXCEPTIONS]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.API_MONITORING]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.AI_OBSERVABILITY]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.METER_EXPLORER]: TelemetrytypesSignalDTO.metrics,
|
||||
[QuickFiltersSource.INFRA_MONITORING]: TelemetrytypesSignalDTO.metrics,
|
||||
};
|
||||
@@ -10,26 +10,26 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
* In ListView most sources use index 0; TRACES_EXPLORER and every non-ListView
|
||||
* mode track the last focused query.
|
||||
*/
|
||||
function useActiveQueryIndex(source: QuickFiltersSource): number {
|
||||
function useActiveQueryIndex(quickFilterSource: QuickFiltersSource): number {
|
||||
const { lastUsedQuery, panelType } = useQueryBuilder();
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
|
||||
return useMemo(() => {
|
||||
// AI observability builds a single query in the row-level views, so its
|
||||
// filters always drive the first one there.
|
||||
if (source === QuickFiltersSource.AI_OBSERVABILITY) {
|
||||
if (quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY) {
|
||||
return isListView || panelType === PANEL_TYPES.TRACE
|
||||
? 0
|
||||
: lastUsedQuery || 0;
|
||||
}
|
||||
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
return quickFilterSource === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, panelType, source, lastUsedQuery]);
|
||||
}, [isListView, panelType, quickFilterSource, lastUsedQuery]);
|
||||
}
|
||||
|
||||
export default useActiveQueryIndex;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGetQuickFilters } from 'api/generated/services/quick-filter';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { IQuickFiltersConfig, SignalType } from '../types';
|
||||
import { IQuickFiltersConfig } from '../types';
|
||||
import { getFilterConfig } from '../utils';
|
||||
|
||||
interface UseFilterConfigProps {
|
||||
signal?: SignalType;
|
||||
savedFiltersSource?: QuickfiltertypesSourceDTO;
|
||||
config: IQuickFiltersConfig[];
|
||||
}
|
||||
interface UseFilterConfigReturn {
|
||||
@@ -18,7 +19,7 @@ interface UseFilterConfigReturn {
|
||||
}
|
||||
|
||||
const useFilterConfig = ({
|
||||
signal,
|
||||
savedFiltersSource,
|
||||
config,
|
||||
}: UseFilterConfigProps): UseFilterConfigReturn => {
|
||||
const {
|
||||
@@ -26,8 +27,8 @@ const useFilterConfig = ({
|
||||
data,
|
||||
refetch,
|
||||
} = useGetQuickFilters(
|
||||
{ source: signal ?? '' },
|
||||
{ query: { enabled: !!signal } },
|
||||
{ source: savedFiltersSource ?? '' },
|
||||
{ query: { enabled: !!savedFiltersSource } },
|
||||
);
|
||||
|
||||
const customFilters = useMemo<TelemetryFieldKey[]>(
|
||||
@@ -35,11 +36,11 @@ const useFilterConfig = ({
|
||||
[data],
|
||||
);
|
||||
|
||||
const isDynamicFilters = !!signal;
|
||||
const isDynamicFilters = !!savedFiltersSource;
|
||||
|
||||
const filterConfig = useMemo(
|
||||
() => getFilterConfig(signal, customFilters, config),
|
||||
[config, customFilters, signal],
|
||||
() => getFilterConfig(customFilters, config),
|
||||
[config, customFilters],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -89,6 +89,15 @@ export const handlers = [
|
||||
fieldValuesHandler(['checkout', 'frontend', 'payments']),
|
||||
];
|
||||
|
||||
export const noSavedFiltersHandler = rest.get(
|
||||
'http://localhost/api/v2/quick_filters/logs',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json(quickFiltersResponse(QuickfiltertypesSourceDTO.logs, [])),
|
||||
),
|
||||
);
|
||||
|
||||
export const loadingFiltersHandlers = [
|
||||
rest.get('http://localhost/api/v2/quick_filters/logs', (_req, res, ctx) =>
|
||||
res(ctx.delay('infinite')),
|
||||
|
||||
@@ -15,8 +15,9 @@ import {
|
||||
loadingFiltersHandlers,
|
||||
queryBuilder,
|
||||
selectedServiceQueryBuilder,
|
||||
noSavedFiltersHandler,
|
||||
} from './QuickFilters.stories.mocks';
|
||||
import { QuickFiltersSource, SignalType } from '../types';
|
||||
import { QuickFiltersSource } from '../types';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Quick Filters',
|
||||
@@ -33,8 +34,7 @@ const meta = {
|
||||
args: {
|
||||
config: checkboxConfig,
|
||||
handleFilterVisibilityChange: (): void => undefined,
|
||||
signal: SignalType.LOGS,
|
||||
source: QuickFiltersSource.LOGS_EXPLORER,
|
||||
quickFilterSource: QuickFiltersSource.LOGS_EXPLORER,
|
||||
useFieldApis: fieldApis,
|
||||
},
|
||||
parameters: {
|
||||
@@ -94,13 +94,16 @@ export const LoadingFilters: Story = {
|
||||
|
||||
/** Empty: a loaded quick-filter configuration with no filters has no result rows. */
|
||||
export const NoResults: Story = {
|
||||
args: { config: [], signal: undefined },
|
||||
args: { config: [] },
|
||||
parameters: {
|
||||
msw: { handlers: [noSavedFiltersHandler, ...handlers] },
|
||||
},
|
||||
};
|
||||
|
||||
/** Selection: an expanded checkbox shows the actual selected service values. */
|
||||
export const SelectedExpandedCheckbox: Story = {
|
||||
args: { signal: undefined },
|
||||
parameters: {
|
||||
msw: { handlers: [noSavedFiltersHandler, ...handlers] },
|
||||
signoz: {
|
||||
queryBuilder: selectedServiceQueryBuilder,
|
||||
},
|
||||
@@ -111,12 +114,18 @@ export const SelectedExpandedCheckbox: Story = {
|
||||
* Every tooltip the panel renders, held open: the reveal on each truncated
|
||||
* filter value. Nothing bounds those values, so the panel is answered with
|
||||
* service names long enough to be cut. The Service name filter carries them, so
|
||||
* the signal that would add the workspace's own dynamic filters is left off.
|
||||
* the workspace's own saved filter list is answered empty.
|
||||
*/
|
||||
export const Tooltips: TooltipsStory = {
|
||||
args: { signal: undefined, tooltipsOpen: true },
|
||||
args: { tooltipsOpen: true },
|
||||
parameters: {
|
||||
msw: { handlers: [fieldValuesHandler(LONG_FILTER_VALUES), ...handlers] },
|
||||
msw: {
|
||||
handlers: [
|
||||
noSavedFiltersHandler,
|
||||
fieldValuesHandler(LONG_FILTER_VALUES),
|
||||
...handlers,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import { rest } from 'msw';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from '../types';
|
||||
import { QuickFiltersSource } from '../types';
|
||||
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -35,8 +35,7 @@ jest.mock('container/ApiMonitoring/queryParams');
|
||||
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/logs`;
|
||||
const fieldsKeysURL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
|
||||
|
||||
@@ -81,11 +80,10 @@ afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function renderWithSignal(): void {
|
||||
function renderWithSavedList(): void {
|
||||
render(
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
signal={SIGNAL}
|
||||
quickFilterSource={QuickFiltersSource.LOGS_EXPLORER}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>,
|
||||
@@ -95,7 +93,7 @@ function renderWithSignal(): void {
|
||||
function renderStaticConfig(): void {
|
||||
render(
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={QuickFiltersConfig}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
@@ -108,7 +106,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
it('shows the inline denial instead of the filters, header stays', async () => {
|
||||
server.use(setupAuthzDeny(QuickFilterReadPermission));
|
||||
|
||||
renderWithSignal();
|
||||
renderWithSavedList();
|
||||
|
||||
await expect(
|
||||
screen.findByText(NOT_AUTHORIZED_TEXT),
|
||||
@@ -133,7 +131,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
it('renders the filters but disables the settings trigger', async () => {
|
||||
server.use(setupAuthzDeny(QuickFilterUpdatePermission));
|
||||
|
||||
renderWithSignal();
|
||||
renderWithSavedList();
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -158,7 +156,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
it('renders the filters and opens settings from the trigger', async () => {
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
renderWithSignal();
|
||||
renderWithSavedList();
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -176,7 +174,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('static config pages (no signal)', () => {
|
||||
describe('static config pages (no saved list)', () => {
|
||||
it('is not gated even when every permission is denied', async () => {
|
||||
server.use(setupAuthzDenyAll());
|
||||
|
||||
@@ -195,7 +193,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => res(ctx.delay('infinite'))),
|
||||
);
|
||||
|
||||
renderWithSignal();
|
||||
renderWithSavedList();
|
||||
|
||||
await waitFor(() =>
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import {
|
||||
ApiMonitoringParams,
|
||||
useApiMonitoringParams,
|
||||
} from 'container/ApiMonitoring/queryParams';
|
||||
import { getHostsQuickFiltersConfig } from 'container/InfraMonitoringHostsV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
otherFiltersResponse,
|
||||
@@ -16,7 +19,7 @@ import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
|
||||
import { IQuickFiltersConfig, QuickFiltersSource } from '../types';
|
||||
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -33,9 +36,9 @@ const mockSetApiMonitoringParams = jest.fn() as jest.MockedFunction<
|
||||
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/logs`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/logs`;
|
||||
const anyQuickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/:source`;
|
||||
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
|
||||
|
||||
@@ -54,6 +57,9 @@ const setupServer = (): void => {
|
||||
rest.get(quickFiltersListURL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(quickFiltersListResponse)),
|
||||
),
|
||||
rest.get(anyQuickFiltersListURL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: { filters: [] } })),
|
||||
),
|
||||
rest.get(quickFiltersSuggestionsURL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(otherFiltersResponse)),
|
||||
),
|
||||
@@ -68,48 +74,24 @@ const setupServer = (): void => {
|
||||
};
|
||||
|
||||
function TestQuickFilters({
|
||||
signal = SignalType.LOGS,
|
||||
quickFilterSource = QuickFiltersSource.EXCEPTIONS,
|
||||
config = QuickFiltersConfig,
|
||||
}: {
|
||||
signal?: SignalType;
|
||||
quickFilterSource?: QuickFiltersSource;
|
||||
config?: IQuickFiltersConfig[];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
quickFilterSource={quickFilterSource}
|
||||
config={config}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
signal={signal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TestQuickFilters.defaultProps = {
|
||||
signal: '',
|
||||
config: QuickFiltersConfig,
|
||||
};
|
||||
|
||||
function TestQuickFiltersApiMonitoring({
|
||||
signal = SignalType.LOGS,
|
||||
config = QuickFiltersConfig,
|
||||
}: {
|
||||
signal?: SignalType;
|
||||
config?: IQuickFiltersConfig[];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
config={config}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
signal={signal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TestQuickFiltersApiMonitoring.defaultProps = {
|
||||
signal: '',
|
||||
quickFilterSource: QuickFiltersSource.EXCEPTIONS,
|
||||
config: QuickFiltersConfig,
|
||||
};
|
||||
|
||||
@@ -287,10 +269,14 @@ describe('Quick Filters', () => {
|
||||
it('toggles Show IP addresses and updates API Monitoring params', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFiltersApiMonitoring />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.API_MONITORING} />,
|
||||
);
|
||||
|
||||
// Switch should be rendered and initially checked
|
||||
expect(screen.getByText('Show IP addresses')).toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText('Show IP addresses'),
|
||||
).resolves.toBeInTheDocument();
|
||||
const toggle = screen.getByRole('switch');
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
|
||||
@@ -308,7 +294,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('loads the custom filters correctly', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Filters for')).toBeInTheDocument();
|
||||
expect(screen.getByText(QUERY_NAME)).toBeInTheDocument();
|
||||
@@ -368,7 +356,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
),
|
||||
);
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -396,7 +386,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -417,7 +409,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('removes a filter from ADDED FILTERS and moves it to OTHER FILTERS', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -446,7 +440,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('restores original filter state on Discard', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -488,7 +484,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('saves the updated filters by calling PUT with correct payload', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -525,7 +523,9 @@ describe('Quick Filters with custom filters', () => {
|
||||
pointerEventsCheck: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<TestQuickFilters signal={SIGNAL} />);
|
||||
const { getByTestId } = render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
expect(screen.getByText('Duration')).toBeInTheDocument();
|
||||
|
||||
@@ -579,7 +579,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
});
|
||||
|
||||
describe('Quick Filters refetch behavior', () => {
|
||||
it('fetches custom filters on every mount when signal is provided', async () => {
|
||||
it('fetches custom filters on every mount for a page with a saved list', async () => {
|
||||
let getCalls = 0;
|
||||
|
||||
server.use(
|
||||
@@ -589,14 +589,18 @@ describe('Quick Filters refetch behavior', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { unmount } = render(<TestQuickFilters signal={SIGNAL} />);
|
||||
const { unmount } = render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
).resolves.toBeInTheDocument();
|
||||
@@ -604,19 +608,24 @@ describe('Quick Filters refetch behavior', () => {
|
||||
expect(getCalls).toBe(2);
|
||||
});
|
||||
|
||||
it('does not fetch custom filters when signal is undefined', async () => {
|
||||
it('does not fetch custom filters for a page without a saved list', async () => {
|
||||
let getCalls = 0;
|
||||
|
||||
server.use(
|
||||
rest.get(quickFiltersListURL, (_req, res, ctx) => {
|
||||
rest.get(anyQuickFiltersListURL, (_req, res, ctx) => {
|
||||
getCalls += 1;
|
||||
return res(ctx.status(200), ctx.json(quickFiltersListResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestQuickFilters signal={undefined} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.INFRA_MONITORING} />,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getCalls).toBe(0));
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(getCalls).toBe(0);
|
||||
});
|
||||
|
||||
it('refetches custom filters after saving settings', async () => {
|
||||
@@ -635,7 +644,9 @@ describe('Quick Filters refetch behavior', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -687,7 +698,9 @@ describe('Quick Filters refetch behavior', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -718,10 +731,47 @@ describe('Quick Filters refetch behavior', () => {
|
||||
),
|
||||
);
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} config={[]} />);
|
||||
render(
|
||||
<TestQuickFilters
|
||||
quickFilterSource={QuickFiltersSource.LOGS_EXPLORER}
|
||||
config={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText('No filters found'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Quick Filters field APIs', () => {
|
||||
it('sends the page signal for every filter, the infra Environment filter included', async () => {
|
||||
const requests: URLSearchParams[] = [];
|
||||
server.use(
|
||||
rest.get(fieldsValuesURL, (req, res, ctx) => {
|
||||
requests.push(req.url.searchParams);
|
||||
return res(ctx.status(200), ctx.json(quickFiltersFieldValuesResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<QuickFilters
|
||||
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={getHostsQuickFiltersConfig()}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(requests.map((params) => params.get('name'))).toContain(
|
||||
INFRA_MONITORING_ATTR_KEYS.DEPLOYMENT_ENVIRONMENT,
|
||||
),
|
||||
);
|
||||
const environment = requests.find(
|
||||
(params) =>
|
||||
params.get('name') === INFRA_MONITORING_ATTR_KEYS.DEPLOYMENT_ENVIRONMENT,
|
||||
);
|
||||
expect(environment?.get('signal')).toBe(TelemetrytypesSignalDTO.metrics);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export enum FiltersType {
|
||||
SLIDER = 'SLIDER',
|
||||
@@ -19,15 +19,6 @@ export enum SpecficFilterOperations {
|
||||
ONLY = 'ONLY',
|
||||
}
|
||||
|
||||
export enum SignalType {
|
||||
TRACES = 'traces',
|
||||
LOGS = 'logs',
|
||||
API_MONITORING = 'api_monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai_observability',
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing export from signozhq/ui/checkbox, TODO(H4ad): Add and remove this type definition
|
||||
*/
|
||||
@@ -39,7 +30,6 @@ export interface IQuickFiltersConfig {
|
||||
attributeKey: BaseAutocompleteData;
|
||||
aggregateOperator?: string;
|
||||
aggregateAttribute?: string;
|
||||
dataSource?: DataSource;
|
||||
customRendererForValue?: (value: string) => JSX.Element;
|
||||
defaultOpen: boolean;
|
||||
}
|
||||
@@ -53,11 +43,9 @@ export interface QuickFilterChangeEventData {
|
||||
export interface IQuickFiltersProps {
|
||||
config: IQuickFiltersConfig[];
|
||||
handleFilterVisibilityChange?: () => void;
|
||||
source: QuickFiltersSource;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
|
||||
/** Pass to fetch quick filters for this signal; omit to use `config` as-is */
|
||||
signal?: SignalType;
|
||||
className?: string;
|
||||
showFilterCollapse?: boolean;
|
||||
showQueryName?: boolean;
|
||||
@@ -65,15 +53,19 @@ export interface IQuickFiltersProps {
|
||||
savedViewsHeader?: ReactNode;
|
||||
}
|
||||
|
||||
export enum QuickFiltersSource {
|
||||
LOGS_EXPLORER = 'logs-explorer',
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai-observability',
|
||||
}
|
||||
export const QuickFiltersSource = {
|
||||
LOGS_EXPLORER: QuickfiltertypesSourceDTO.logs,
|
||||
TRACES_EXPLORER: QuickfiltertypesSourceDTO.traces,
|
||||
EXCEPTIONS: QuickfiltertypesSourceDTO.exceptions,
|
||||
API_MONITORING: QuickfiltertypesSourceDTO.api_monitoring,
|
||||
METER_EXPLORER: QuickfiltertypesSourceDTO.meter,
|
||||
AI_OBSERVABILITY: QuickfiltertypesSourceDTO.ai_observability,
|
||||
/** No saved filter list: renders `config` as given. */
|
||||
INFRA_MONITORING: 'infra-monitoring',
|
||||
} as const;
|
||||
|
||||
export type QuickFiltersSource =
|
||||
(typeof QuickFiltersSource)[keyof typeof QuickFiltersSource];
|
||||
|
||||
export type QuickFilterCheckboxUseFieldApis = {
|
||||
startUnixMilli: number;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
|
||||
|
||||
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
|
||||
import { FiltersType, IQuickFiltersConfig } from './types';
|
||||
|
||||
const FILTER_TITLE_MAP: Record<string, string> = {
|
||||
duration_nano: 'Duration',
|
||||
@@ -50,11 +49,10 @@ const getFilterType = (att: TelemetryFieldKey): FiltersType => {
|
||||
};
|
||||
|
||||
export const getFilterConfig = (
|
||||
signal?: SignalType,
|
||||
customFilters?: TelemetryFieldKey[],
|
||||
config?: IQuickFiltersConfig[],
|
||||
): IQuickFiltersConfig[] => {
|
||||
if (!customFilters?.length || !signal) {
|
||||
if (!customFilters?.length) {
|
||||
return config || [];
|
||||
}
|
||||
|
||||
@@ -63,7 +61,6 @@ export const getFilterConfig = (
|
||||
({
|
||||
type: getFilterType(att),
|
||||
title: getFilterName(att.name),
|
||||
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
|
||||
attributeKey: {
|
||||
id: att.name,
|
||||
key: att.name,
|
||||
|
||||
@@ -21,7 +21,6 @@ export enum LOCALSTORAGE {
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
|
||||
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
|
||||
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
||||
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
|
||||
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
||||
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
|
||||
@@ -47,5 +46,4 @@ export enum LOCALSTORAGE {
|
||||
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
|
||||
DASHBOARD_V2_PANEL_COLUMN_WIDTHS = 'DASHBOARD_V2_PANEL_COLUMN_WIDTHS',
|
||||
LLM_ATTRIBUTE_MAPPING_TEST_SPAN = 'LLM_ATTRIBUTE_MAPPING_TEST_SPAN',
|
||||
SAVED_VIEW_ENABLED = 'SAVED_VIEW_ENABLED',
|
||||
}
|
||||
|
||||
@@ -50,8 +50,6 @@ const ROUTES = {
|
||||
ROLE_EDIT: '/settings/roles/:roleId/edit',
|
||||
MEMBERS_SETTINGS: '/settings/members',
|
||||
SUPPORT: '/support',
|
||||
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
||||
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
||||
TRACES_FUNNELS: '/traces/funnels',
|
||||
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
|
||||
WORKSPACE_LOCKED: '/workspace-locked',
|
||||
@@ -69,7 +67,6 @@ const ROUTES = {
|
||||
MESSAGING_QUEUES_OVERVIEW: '/messaging-queues/overview',
|
||||
METRICS_EXPLORER: '/metrics-explorer/summary',
|
||||
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
||||
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
||||
METRICS_EXPLORER_VOLUME_CONTROL: '/metrics-explorer/volume-control',
|
||||
API_MONITORING_BASE: '/api-monitoring',
|
||||
API_MONITORING: '/api-monitoring/explorer',
|
||||
@@ -77,7 +74,6 @@ const ROUTES = {
|
||||
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
||||
METER: '/meter',
|
||||
METER_EXPLORER: '/meter/explorer',
|
||||
METER_EXPLORER_VIEWS: '/meter/explorer/views',
|
||||
HOME_PAGE: '/',
|
||||
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
|
||||
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
.trigger {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 10;
|
||||
transition: transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import Noz from 'components/Noz/Noz';
|
||||
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
|
||||
|
||||
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
|
||||
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import {
|
||||
openAIAssistant,
|
||||
useAIAssistantStore,
|
||||
} from '../store/useAIAssistantStore';
|
||||
|
||||
import styles from './AIAssistantTrigger.module.scss';
|
||||
|
||||
/**
|
||||
* Floating action button anchored to the bottom-right of the content area.
|
||||
* Hidden when the panel is already open or when on the full-screen AI Assistant page.
|
||||
*/
|
||||
export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
const { pathname } = useLocation();
|
||||
const isDrawerOpen = useAIAssistantStore((s) => s.isDrawerOpen);
|
||||
const isModalOpen = useAIAssistantStore((s) => s.isModalOpen);
|
||||
|
||||
const isFullScreenPage = !!matchPath(pathname, {
|
||||
path: ROUTES.AI_ASSISTANT,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
const handleOpen = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: AIAssistantOpenSource.Icon,
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
openAIAssistant();
|
||||
}, [pathname]);
|
||||
|
||||
if (isDrawerOpen || isModalOpen || isFullScreenPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.trigger} noz-wave`}>
|
||||
<Tooltip title={NOZ_TOOLTIP_TITLE}>
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
icon
|
||||
onClick={handleOpen}
|
||||
aria-label="Open Noz"
|
||||
>
|
||||
<Noz size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './AIAssistantTrigger';
|
||||
export { default } from './AIAssistantTrigger';
|
||||
@@ -102,7 +102,7 @@ describe('resolvePageType', () => {
|
||||
expect(resolvePageType(ROUTES.METRICS_EXPLORER_EXPLORER, '')).toBe(
|
||||
PageTypeDTO.metrics_explorer,
|
||||
);
|
||||
expect(resolvePageType(ROUTES.METRICS_EXPLORER_VIEWS, '')).toBe(
|
||||
expect(resolvePageType('/metrics-explorer/views', '')).toBe(
|
||||
PageTypeDTO.metrics_explorer,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -235,7 +235,6 @@ describe('buildExplorerNavigationUrl', () => {
|
||||
{ queryType: 'builder' } as never,
|
||||
{
|
||||
[QueryParams.panelTypes]: PANEL_TYPES.LIST,
|
||||
[QueryParams.viewName]: 'My view',
|
||||
[QueryParams.viewKey]: 'view-1',
|
||||
},
|
||||
);
|
||||
@@ -259,7 +258,7 @@ describe('openSavedView', () => {
|
||||
expect(pushedUrl).toContain(ROUTES.LOGS_EXPLORER);
|
||||
const params = new URLSearchParams(pushedUrl.split('?')[1]);
|
||||
expect(params.get(QueryParams.viewKey)).toBe('"view-logs"');
|
||||
expect(params.get(QueryParams.viewName)).toBe('"View view-logs"');
|
||||
expect(params.get(QueryParams.viewName)).toBeNull();
|
||||
expect(params.get(QueryParams.panelTypes)).toBe('"list"');
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SavedViewSourcePage } from 'container/SavedViews/types';
|
||||
import { findSavedView } from 'container/SavedViews/utils/findSavedView';
|
||||
import { getSavedViewQuery } from 'container/SavedViews/utils/getSavedViewQuery';
|
||||
import { toSavedViewSource } from 'container/SavedViews/utils/toSavedViewSource';
|
||||
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
|
||||
import { SOURCEPAGE_VS_ROUTES } from 'container/SavedViews/constants';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { History } from 'history';
|
||||
@@ -108,7 +108,6 @@ export function openSavedView(
|
||||
const query = getSavedViewQuery(view);
|
||||
const url = buildExplorerNavigationUrl(route, query, {
|
||||
[QueryParams.panelTypes]: view.spec.panelType as unknown as PANEL_TYPES,
|
||||
[QueryParams.viewName]: view.spec.displayName,
|
||||
[QueryParams.viewKey]: view.id,
|
||||
});
|
||||
history.push(url);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { matchPath } from 'react-router-dom';
|
||||
*
|
||||
* **URL-only.** All inputs come from `pathname` + `search`; we never reach
|
||||
* into Redux / context. SigNoz already encodes most page state into the URL
|
||||
* (`compositeQuery`, `startTime`/`endTime`, `viewName`/`viewKey`,
|
||||
* (`compositeQuery`, `startTime`/`endTime`, `viewKey`,
|
||||
* `variables`, `expandedWidgetId`, `activeLogId`, …), so URL parsing is
|
||||
* sufficient. Anything not in the URL is simply omitted from `metadata` —
|
||||
* the server applies its own defaults.
|
||||
@@ -288,7 +288,7 @@ export function getAutoContexts(
|
||||
|
||||
// ── Metrics ───────────────────────────────────────────────────────────────
|
||||
|
||||
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer, views).
|
||||
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer).
|
||||
if (
|
||||
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_BASE, exact: false })
|
||||
) {
|
||||
@@ -361,10 +361,6 @@ function collectSharedMetadata(
|
||||
if (viewKey) {
|
||||
out.savedViewId = viewKey;
|
||||
}
|
||||
const viewName = params.get(QueryParams.viewName);
|
||||
if (viewName) {
|
||||
out.savedViewName = viewName;
|
||||
}
|
||||
const variablesRaw = params.get(QueryParams.variables);
|
||||
if (variablesRaw) {
|
||||
try {
|
||||
|
||||
@@ -334,6 +334,7 @@ function AnomalyAlertEvaluationView({
|
||||
<Checkbox
|
||||
color="primary"
|
||||
key={seriesKey}
|
||||
testId="anomaly-series-option"
|
||||
name="series"
|
||||
value={selectedSeries === seriesKey}
|
||||
onChange={(): void => handleSeriesChange(seriesKey)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
|
||||
import DomainList from './Domains/DomainList';
|
||||
@@ -24,8 +24,7 @@ function Explorer(): JSX.Element {
|
||||
showFilters
|
||||
quickFilterProps={{
|
||||
className: 'qf-api-monitoring',
|
||||
source: QuickFiltersSource.API_MONITORING,
|
||||
signal: SignalType.API_MONITORING,
|
||||
quickFilterSource: QuickFiltersSource.API_MONITORING,
|
||||
showQueryName: false,
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
|
||||
@@ -91,7 +91,6 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -102,7 +101,6 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -113,7 +111,6 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -72,40 +72,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chat-support-gateway {
|
||||
position: fixed;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: calc(20px + var(--bottom-strip-height, 0px));
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
|
||||
.chat-support-gateway-btn {
|
||||
max-width: 48px;
|
||||
width: 48px;
|
||||
max-height: 48px;
|
||||
height: 48px;
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
background-color: #f25733;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
color: white !important;
|
||||
border-color: white !important;
|
||||
}
|
||||
|
||||
.chat-support-gateway-btn-icon {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-credit-card-btn,
|
||||
.cancel-btn {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -28,7 +28,6 @@ import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
|
||||
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
@@ -51,8 +50,6 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
|
||||
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import history from 'lib/history';
|
||||
import { isNull } from 'lodash-es';
|
||||
@@ -401,7 +398,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
}, [pathname]);
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
const isSavedViewEnabled = useSavedViewEnabled();
|
||||
|
||||
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
||||
const pageTitle = t(routeKey);
|
||||
@@ -489,8 +485,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
}
|
||||
}, [isDarkMode]);
|
||||
|
||||
const chatSupport = useChatSupport();
|
||||
|
||||
// Listen for API warnings
|
||||
const handleWarning = (
|
||||
isSlow: boolean,
|
||||
@@ -834,9 +828,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
</LayoutContent>
|
||||
</Sentry.ErrorBoundary>
|
||||
|
||||
{isSavedViewEnabled && isToDisplayLayout && !renderFullScreen && (
|
||||
<BottomStrip />
|
||||
)}
|
||||
{isToDisplayLayout && !renderFullScreen && <BottomStrip />}
|
||||
</div>
|
||||
|
||||
{isLoggedIn && isAIAssistantEnabled && (
|
||||
@@ -847,9 +839,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{chatSupport === ChatSupportState.NeedsCard && !isSavedViewEnabled && (
|
||||
<ChatSupportGateway />
|
||||
)}
|
||||
{showChangelogModal && changelog && (
|
||||
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AlertmanagertypesGettableNotificationChannelDTO } from 'api/generated/s
|
||||
|
||||
import { toChannelConfig, toPostableChannel } from './channelConfig';
|
||||
import { toChannelFormState } from './channelFormValues';
|
||||
import { ChannelFormValues, ChannelKind } from './types';
|
||||
import { ChannelFormValues, ChannelKind, WebhookAuth } from './types';
|
||||
|
||||
const slackValues: ChannelFormValues = {
|
||||
name: 'prod alerts',
|
||||
@@ -56,6 +56,22 @@ describe('toChannelConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[WebhookAuth.none, {}],
|
||||
[WebhookAuth.basic, { username: 'u', password: 'p' }],
|
||||
[WebhookAuth.bearer, { bearerToken: 't' }],
|
||||
])('sends only the %s webhook credentials', (webhookAuth, credentials) => {
|
||||
expect(
|
||||
toChannelConfig(ChannelKind.webhook, {
|
||||
url: 'https://example.com/hook',
|
||||
webhookAuth,
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
bearerToken: 't',
|
||||
}).spec,
|
||||
).toStrictEqual({ url: 'https://example.com/hook', ...credentials });
|
||||
});
|
||||
|
||||
it('drops an attachment list the user emptied instead of sending []', () => {
|
||||
expect(
|
||||
toChannelConfig(ChannelKind.slack, {
|
||||
|
||||
@@ -18,8 +18,17 @@ import {
|
||||
import { omit, pick } from 'lodash-es';
|
||||
import { omitBlank } from 'utils/omitBlank';
|
||||
|
||||
import { ChannelFormValues, ChannelKind, ChannelSpecFormValues } from './types';
|
||||
import { canonicalJiraReopenDuration, filledRows } from './utils';
|
||||
import {
|
||||
ChannelFormValues,
|
||||
ChannelKind,
|
||||
ChannelSpecFormValues,
|
||||
WebhookAuth,
|
||||
} from './types';
|
||||
import {
|
||||
canonicalJiraReopenDuration,
|
||||
filledRows,
|
||||
webhookAuthOf,
|
||||
} from './utils';
|
||||
|
||||
const SPEC_FIELDS = {
|
||||
[ChannelKind.slack]: [
|
||||
@@ -210,6 +219,17 @@ export function toChannelConfig(
|
||||
);
|
||||
}
|
||||
|
||||
if (kind === ChannelKind.webhook) {
|
||||
const auth = webhookAuthOf(values);
|
||||
if (auth !== WebhookAuth.basic) {
|
||||
delete picked.username;
|
||||
delete picked.password;
|
||||
}
|
||||
if (auth !== WebhookAuth.bearer) {
|
||||
delete picked.bearerToken;
|
||||
}
|
||||
}
|
||||
|
||||
const spec: Record<string, unknown> = omitBlank(picked);
|
||||
|
||||
// the API answers a read with the duration prometheus would print, and
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AlertmanagertypesGettableNotificationChannelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { ChannelFormValues, ChannelKind } from './types';
|
||||
import { webhookAuthOf } from './utils';
|
||||
|
||||
export interface ChannelFormState {
|
||||
kind: ChannelKind;
|
||||
@@ -22,11 +23,15 @@ function toChannelKind(kind: string): ChannelKind {
|
||||
export function toChannelFormState(
|
||||
channel: AlertmanagertypesGettableNotificationChannelDTO,
|
||||
): ChannelFormState {
|
||||
return {
|
||||
kind: toChannelKind(channel.config.kind),
|
||||
values: {
|
||||
...channel.config.spec,
|
||||
name: channel.displayName,
|
||||
},
|
||||
const kind = toChannelKind(channel.config.kind);
|
||||
const values: ChannelFormValues = {
|
||||
...channel.config.spec,
|
||||
name: channel.displayName,
|
||||
};
|
||||
|
||||
if (kind === ChannelKind.webhook) {
|
||||
values.webhookAuth = webhookAuthOf(values);
|
||||
}
|
||||
|
||||
return { kind, values };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TFunction } from 'i18next';
|
||||
import { get, set } from 'lodash-es';
|
||||
import type { FieldErrors, Resolver } from 'react-hook-form';
|
||||
import type { $ZodIssue } from 'zod/v4/core';
|
||||
|
||||
@@ -10,12 +11,12 @@ const toFieldErrors = (
|
||||
issues: $ZodIssue[],
|
||||
t: TFunction,
|
||||
): FieldErrors<ChannelFormValues> => {
|
||||
const errors: Record<string, { type: string; message: string }> = {};
|
||||
const errors = {};
|
||||
|
||||
issues.forEach((issue) => {
|
||||
const field = issue.path.join('.') || 'name';
|
||||
if (!errors[field]) {
|
||||
errors[field] = { type: 'validation', message: t(issue.message) };
|
||||
const path = issue.path.length ? issue.path : ['name'];
|
||||
if (!get(errors, path)) {
|
||||
set(errors, path, { type: 'validation', message: t(issue.message) });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ const SPECS: Record<string, Record<string, unknown>> = {
|
||||
url: 'https://example.com/hook',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
bearerToken: 't',
|
||||
sendResolved: true,
|
||||
},
|
||||
email: {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isValidGoogleChatWebhookURL } from '../utils';
|
||||
import { requiredString } from './shared';
|
||||
|
||||
export const googleChatSchema = z.object({
|
||||
webhookUrl: requiredString('webhook_url_required').refine(
|
||||
isValidGoogleChatWebhookURL,
|
||||
{ message: 'google_chat_webhook_url_invalid' },
|
||||
),
|
||||
webhookUrl: requiredString('webhook_url_required'),
|
||||
});
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isValidIncidentIOURL } from '../utils';
|
||||
import { requiredString } from './shared';
|
||||
|
||||
const required = requiredString('incidentio_required_fields');
|
||||
|
||||
export const incidentIOSchema = z.object({
|
||||
url: required.refine(isValidIncidentIOURL, {
|
||||
message: 'incidentio_url_invalid',
|
||||
}),
|
||||
token: required,
|
||||
url: required,
|
||||
token: z
|
||||
.string({ error: 'incidentio_required_fields' })
|
||||
.refine((token) => token === token.trim(), {
|
||||
message: 'incidentio_token_whitespace',
|
||||
})
|
||||
.pipe(
|
||||
required.refine((token) => !/^bearer( |$)/i.test(token), {
|
||||
message: 'incidentio_token_bearer_prefix',
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isValidJiraReopenDuration, isValidJiraSiteURL } from '../utils';
|
||||
import { isValidJiraReopenDuration } from '../utils';
|
||||
import { requiredString } from './shared';
|
||||
|
||||
const required = requiredString('jira_required_fields');
|
||||
|
||||
export const jiraSchema = z.object({
|
||||
site: required.refine(isValidJiraSiteURL, { message: 'jira_site_invalid' }),
|
||||
site: required,
|
||||
email: required,
|
||||
apiToken: required,
|
||||
project: required,
|
||||
|
||||
@@ -2,6 +2,30 @@ import { z } from 'zod';
|
||||
|
||||
import { requiredString } from './shared';
|
||||
|
||||
// rows with a blank title or text are dropped on save, so only the rest are checked
|
||||
const fieldRow = z
|
||||
.object({ title: z.string().optional(), value: z.string().optional() })
|
||||
.refine((row) => !row.title?.trim() || !!row.value?.trim(), {
|
||||
message: 'slack_field_value_required',
|
||||
path: ['value'],
|
||||
});
|
||||
|
||||
const actionRow = z
|
||||
.object({
|
||||
text: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
.refine(
|
||||
(row) => !row.text?.trim() || !!row.url?.trim() || !!row.name?.trim(),
|
||||
{
|
||||
message: 'slack_action_url_or_name_required',
|
||||
path: ['url'],
|
||||
},
|
||||
);
|
||||
|
||||
export const slackSchema = z.object({
|
||||
apiUrl: requiredString('webhook_url_required'),
|
||||
fields: z.array(fieldRow).nullish(),
|
||||
actions: z.array(actionRow).nullish(),
|
||||
});
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WebhookAuth } from '../types';
|
||||
import { webhookAuthOf } from '../utils';
|
||||
import { requiredString } from './shared';
|
||||
|
||||
export const webhookSchema = z
|
||||
.object({
|
||||
url: requiredString('webhook_url_required'),
|
||||
webhookAuth: z.enum(WebhookAuth).optional(),
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
bearerToken: z.string().optional(),
|
||||
})
|
||||
// the API allows bearer-only and no-auth webhooks, but a username without its
|
||||
// password is still an incomplete basic auth pair
|
||||
.refine((v) => !v.username || !!v.password, {
|
||||
message: 'username_no_password',
|
||||
path: ['password'],
|
||||
});
|
||||
.refine(
|
||||
(values) =>
|
||||
webhookAuthOf(values) !== WebhookAuth.basic ||
|
||||
!!values.username?.trim() ||
|
||||
!!values.password?.trim(),
|
||||
{ message: 'webhook_basic_auth_required', path: ['username'] },
|
||||
)
|
||||
.refine(
|
||||
(values) =>
|
||||
webhookAuthOf(values) !== WebhookAuth.bearer || !!values.bearerToken?.trim(),
|
||||
{ message: 'webhook_bearer_token_required', path: ['bearerToken'] },
|
||||
);
|
||||
|
||||
@@ -31,6 +31,15 @@ export type ChannelSpecFormValues = Partial<
|
||||
AlertmanagertypesChannelTelegramConfigDTO
|
||||
>;
|
||||
|
||||
export const WebhookAuth = {
|
||||
none: 'none',
|
||||
basic: 'basic',
|
||||
bearer: 'bearer',
|
||||
} as const;
|
||||
export type WebhookAuth = (typeof WebhookAuth)[keyof typeof WebhookAuth];
|
||||
|
||||
export type ChannelFormValues = ChannelSpecFormValues & {
|
||||
name?: string;
|
||||
/** Never sent: the API reads the mode from which credentials are set. */
|
||||
webhookAuth?: WebhookAuth;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user