mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-28 20:10:43 +00:00
Compare commits
38 Commits
fix/error-
...
refactor/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d01a06794d | ||
|
|
f16dfd6c09 | ||
|
|
18eb8ca849 | ||
|
|
3b34a0d951 | ||
|
|
0036cda4f1 | ||
|
|
665e70b17a | ||
|
|
450ceed5fd | ||
|
|
63d0470894 | ||
|
|
c69b77e030 | ||
|
|
4daf425a19 | ||
|
|
6f2ee41dcb | ||
|
|
8ab8c63bea | ||
|
|
0513ab7f1f | ||
|
|
83b4754eaa | ||
|
|
120da85b49 | ||
|
|
b57e5e8e09 | ||
|
|
ba155067e7 | ||
|
|
b2c0b50e92 | ||
|
|
62ba753c64 | ||
|
|
706c233958 | ||
|
|
8cac5d3ed4 | ||
|
|
e28ae2acb5 | ||
|
|
5e970c0d3c | ||
|
|
14a792f26e | ||
|
|
17c84c1ac1 | ||
|
|
74f92acd0e | ||
|
|
d6eddcd7f3 | ||
|
|
bf77b5cb5b | ||
|
|
9c85e798be | ||
|
|
ed901c884e | ||
|
|
e8c4f12a21 | ||
|
|
f8149cf42b | ||
|
|
967bed31ca | ||
|
|
12c5e4fd87 | ||
|
|
48aecdd2fe | ||
|
|
476a163c5e | ||
|
|
5a463a25a0 | ||
|
|
55541e810d |
@@ -14,7 +14,7 @@ Grep recipes, run against the page's directories:
|
||||
| endpoint URLs | the api module's `axios.get\|post` |
|
||||
| endpoint URLs behind a generated hook | the hook lives in `src/api/generated/services/<name>/index.ts` and the URL only appears in the fetcher body: `rg 'url: \`' src/api/generated/services/<name>/` |
|
||||
| url state | `useUrlQuery\|useUrlQueryData\|useUrlSearchState\|useQueryState\|QueryParams\.` |
|
||||
| navigation | `useSafeNavigate\|history.push\|<Link` |
|
||||
| navigation | `useSafeNavigate\|useAppNavigate\|navigate\|<AppLink` |
|
||||
| permissions | `useAuthZ\|AuthZGuard\|AuthZButton\|hasEditPermission\|routePermission` |
|
||||
| flags and prefs | `useFeatureFlag\|FeatureKeys\.\|USER_PREFERENCES\.\|userPreferences` |
|
||||
| empty and error branches | `isLoading\|isError\|isFetching\|length === 0\|!data` |
|
||||
|
||||
@@ -31970,6 +31970,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.
|
||||
@@ -187,7 +187,7 @@ Each spec follows these principles:
|
||||
});
|
||||
```
|
||||
This is faster and more reliable than a multi-step UI seed. Reach for the UI flow only when the test's *purpose* is asserting that flow.
|
||||
6. **Reusable static data lives in `tests/e2e/testdata/`.** For example, `apm-metrics.json` is a real dashboard payload that `importApmMetricsDashboardViaUI` (in `helpers/dashboards.ts`) uploads through the actual Import JSON UI flow to seed a richly-tagged dashboard for search/list tests.
|
||||
6. **Reusable static data lives in `tests/e2e/testdata/`.** For example, `traces/large-trace.json` is a real trace that `loadLargeTrace` (in `helpers/trace-details.ts`) seeds for the trace-details tests.
|
||||
|
||||
## How to write an E2E test?
|
||||
|
||||
|
||||
@@ -294,6 +294,8 @@
|
||||
// Forces useDashboardFetchRequired() outside the root V2 pages (allowlisted in overrides below)
|
||||
"signoz/no-msw-in-story-file": "error",
|
||||
// Bans msw imports in *.stories.tsx; handlers/mock data belong in the sibling .stories.mocks.tsx
|
||||
"signoz/no-direct-react-router-import": "error",
|
||||
// Steers call sites to the src/lib/router facade (allowlisted in overrides below)
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
@@ -589,6 +591,23 @@
|
||||
"rules": {
|
||||
"no-redeclare": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
// The react-router allowlist: the facade itself, the route table, `useSafeNavigate`,
|
||||
// the history singleton and the test and story router mounts. Test files are
|
||||
// deliberately absent: they render at a route or use tests/router-mocks.
|
||||
"files": [
|
||||
"src/lib/router/**",
|
||||
"src/AppRoutes/**",
|
||||
"src/hooks/useSafeNavigate.ts",
|
||||
"src/lib/history.ts",
|
||||
"src/tests/router.tsx",
|
||||
"src/storybook/providers/StorybookProviders.tsx",
|
||||
"src/storybook/renderAtRoute.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"signoz/no-direct-react-router-import": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ const preview: Preview = {
|
||||
'Detail',
|
||||
[
|
||||
'Overview',
|
||||
'Panels',
|
||||
'Settings',
|
||||
'Variables',
|
||||
'New Variable',
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { ruleTester } from './rule-tester.mjs';
|
||||
|
||||
const FACADE = 'src/lib/router facade';
|
||||
const HISTORY = 'lib/history singleton';
|
||||
const MOCK = 'tests/router-mocks helpers';
|
||||
|
||||
await ruleTester({
|
||||
rule: 'no-direct-react-router-import',
|
||||
valid: [
|
||||
{
|
||||
name: 'facade import',
|
||||
code: "import { useAppNavigate } from 'lib/router/useAppNavigate';",
|
||||
},
|
||||
{
|
||||
name:
|
||||
'the history package itself is a version-bump concern, not a facade one',
|
||||
code: "import { createBrowserHistory } from 'history';",
|
||||
},
|
||||
{
|
||||
name: 'unrelated module whose name contains history',
|
||||
code: "import { useHistoryPanel } from 'container/HistoryPanel';",
|
||||
},
|
||||
{
|
||||
name: 'mocking a facade module',
|
||||
code: "jest.mock('lib/router/navigation');",
|
||||
},
|
||||
{
|
||||
name: 'jest call that does not name a module',
|
||||
code: 'jest.fn();',
|
||||
},
|
||||
{
|
||||
name: 'require of an unrelated module',
|
||||
code: "const x = require('lib/dashboardVariables');",
|
||||
},
|
||||
{
|
||||
name: 'export without a source',
|
||||
code: 'const a = 1;\nexport { a };',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'jest.mock of react-router-dom',
|
||||
code: "jest.mock('react-router-dom', () => ({}));",
|
||||
errors: [{ message: MOCK, line: 1, column: 11 }],
|
||||
},
|
||||
{
|
||||
name: 'jest.requireActual of react-router-dom',
|
||||
code: "const actual = jest.requireActual('react-router-dom');",
|
||||
errors: [{ message: MOCK }],
|
||||
},
|
||||
{
|
||||
name: 'jest.mock of lib/history',
|
||||
code: "jest.mock('lib/history');",
|
||||
errors: [{ message: MOCK }],
|
||||
},
|
||||
{
|
||||
name: 'jest.doMock of react-router-dom-v5-compat',
|
||||
code: "jest.doMock('react-router-dom-v5-compat');",
|
||||
errors: [{ message: MOCK }],
|
||||
},
|
||||
{
|
||||
name: 'react-router-dom named import',
|
||||
code: "import { useHistory } from 'react-router-dom';",
|
||||
errors: [{ message: FACADE, line: 1, column: 28 }],
|
||||
},
|
||||
{
|
||||
name: 'react-router named import',
|
||||
code: "import { useLocation } from 'react-router';",
|
||||
errors: [{ message: FACADE }],
|
||||
},
|
||||
{
|
||||
name: 'react-router-dom type-only import',
|
||||
code: "import type { RouteProps } from 'react-router-dom';",
|
||||
errors: [{ message: FACADE }],
|
||||
},
|
||||
{
|
||||
name: 'react-router-dom-v5-compat',
|
||||
code: "import { useNavigate } from 'react-router-dom-v5-compat';",
|
||||
errors: [{ message: 'compat package is an implementation detail' }],
|
||||
},
|
||||
{
|
||||
name: 'lib/history default import',
|
||||
code: "import history from 'lib/history';",
|
||||
errors: [{ message: HISTORY }],
|
||||
},
|
||||
{
|
||||
name: 'lib/history through the @/ alias',
|
||||
code: "import history from '@/lib/history';",
|
||||
errors: [{ message: HISTORY }],
|
||||
},
|
||||
{
|
||||
name: 'jest.mock of lib/history through the @/ alias',
|
||||
code: "jest.mock('@/lib/history');",
|
||||
errors: [{ message: MOCK }],
|
||||
},
|
||||
{
|
||||
name: 'lib/history require',
|
||||
code: "const history = require('lib/history').default;",
|
||||
errors: [{ message: HISTORY }],
|
||||
},
|
||||
{
|
||||
name: 'dynamic import',
|
||||
code: "const mod = await import('react-router-dom');",
|
||||
errors: [{ message: FACADE }],
|
||||
},
|
||||
{
|
||||
name: 're-export',
|
||||
code: "export { Link } from 'react-router-dom';",
|
||||
errors: [{ message: FACADE }],
|
||||
},
|
||||
{
|
||||
name: 'export all',
|
||||
code: "export * from 'react-router-dom';",
|
||||
errors: [{ message: FACADE }],
|
||||
},
|
||||
{
|
||||
name: 'one report per import statement',
|
||||
code:
|
||||
"import { Link } from 'react-router-dom';\nimport history from 'lib/history';",
|
||||
errors: [
|
||||
{ message: FACADE, line: 1 },
|
||||
{ message: HISTORY, line: 2 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
123
frontend/plugins/rules/no-direct-react-router-import.mjs
Normal file
123
frontend/plugins/rules/no-direct-react-router-import.mjs
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Rule: no-direct-react-router-import
|
||||
*
|
||||
* Every router concern goes through the `src/lib/router/*` facade, so a router
|
||||
* upgrade is a change to one directory instead of ~300 call sites. This rule
|
||||
* keeps new call sites, and test mocks, from reaching past it.
|
||||
*
|
||||
* `history` (the package) is deliberately not flagged: it is a transitive concern of
|
||||
* the version bump, not something the facade replaces.
|
||||
*
|
||||
* The allowlist (the facade itself, the route table, `useSafeNavigate`, the history
|
||||
* singleton and the test and story router mounts) is applied via overrides in
|
||||
* .oxlintrc.json, not here, so the file list stays visible next to the severity.
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
const HISTORY_MODULE_SUFFIX = path.join('src', 'lib', 'history');
|
||||
|
||||
const JEST_MODULE_METHODS = new Set([
|
||||
'mock',
|
||||
'doMock',
|
||||
'unmock',
|
||||
'dontMock',
|
||||
'setMock',
|
||||
'requireActual',
|
||||
'requireMock',
|
||||
'createMockFromModule',
|
||||
]);
|
||||
|
||||
const MESSAGE_IDS = {
|
||||
'react-router': 'v5Router',
|
||||
'react-router-dom': 'v5Router',
|
||||
'react-router-dom-v5-compat': 'v6Compat',
|
||||
'lib/history': 'historySingleton',
|
||||
'@/lib/history': 'historySingleton',
|
||||
};
|
||||
|
||||
/** Resolves `./history` / `../history` so files inside src/lib count too. */
|
||||
function isHistoryModule(specifier, filename) {
|
||||
if (!specifier.startsWith('.') || !filename) {
|
||||
return false;
|
||||
}
|
||||
const resolved = path.resolve(path.dirname(filename), specifier);
|
||||
return (
|
||||
resolved.endsWith(HISTORY_MODULE_SUFFIX) ||
|
||||
resolved.endsWith(`${HISTORY_MODULE_SUFFIX}.ts`)
|
||||
);
|
||||
}
|
||||
|
||||
function messageIdFor(specifier, filename) {
|
||||
if (MESSAGE_IDS[specifier] !== undefined) {
|
||||
return MESSAGE_IDS[specifier];
|
||||
}
|
||||
return isHistoryModule(specifier, filename) ? 'historySingleton' : null;
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow direct react-router / lib/history imports; import from src/lib/router instead',
|
||||
category: 'React Router migration',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
v5Router:
|
||||
'Do not import react-router-dom directly. Use the src/lib/router facade (useAppNavigate, useAppLocation, useAppParams, AppLink, Redirect, matchRoute) so a router upgrade stays contained.',
|
||||
v6Compat:
|
||||
'Do not import react-router-dom-v5-compat directly. Use the src/lib/router facade instead; the compat package is an implementation detail of the migration and disappears with it.',
|
||||
historySingleton:
|
||||
'Do not import the lib/history singleton. Use the helpers in src/lib/router/navigation.ts, or useAppNavigate() to follow the nearest router. history loses basename handling once the router owns it.',
|
||||
mock:
|
||||
'Do not mock the router directly. Render at a route with tests/test-utils, or spy on the facade with the tests/router-mocks helpers.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const report = (sourceNode, overrideMessageId) => {
|
||||
if (
|
||||
sourceNode === null ||
|
||||
sourceNode === undefined ||
|
||||
typeof sourceNode.value !== 'string'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const messageId = messageIdFor(sourceNode.value, context.filename);
|
||||
if (messageId !== null) {
|
||||
context.report({
|
||||
node: sourceNode,
|
||||
messageId: overrideMessageId ?? messageId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
ImportDeclaration: (node) => report(node.source),
|
||||
ImportExpression: (node) => report(node.source),
|
||||
ExportAllDeclaration: (node) => report(node.source),
|
||||
ExportNamedDeclaration: (node) => report(node.source),
|
||||
CallExpression(node) {
|
||||
const { callee } = node;
|
||||
if (
|
||||
(callee.type === 'Identifier' && callee.name === 'require') ||
|
||||
callee.type === 'Import'
|
||||
) {
|
||||
report(node.arguments[0]);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.object.type === 'Identifier' &&
|
||||
callee.object.name === 'jest' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
JEST_MODULE_METHODS.has(callee.property.name)
|
||||
) {
|
||||
report(node.arguments[0], 'mock');
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root
|
||||
import noConditionalTextNodesWithSiblings from './rules/no-conditional-text-nodes-with-siblings.mjs';
|
||||
import noReturnTextNodes from './rules/no-return-text-nodes.mjs';
|
||||
import noMswInStoryFile from './rules/no-msw-in-story-file.mjs';
|
||||
import noDirectReactRouterImport from './rules/no-direct-react-router-import.mjs';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
@@ -33,5 +34,6 @@ export default {
|
||||
'no-conditional-text-nodes-with-siblings': noConditionalTextNodesWithSiblings,
|
||||
'no-return-text-nodes': noReturnTextNodes,
|
||||
'no-msw-in-story-file': noMswInStoryFile,
|
||||
'no-direct-react-router-import': noDirectReactRouterImport,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
import deleteLocalStorageKey from 'api/browser/localstorage/remove';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
|
||||
import deleteSession from './v2/sessions/delete';
|
||||
|
||||
@@ -21,5 +21,5 @@ export const Logout = async (): Promise<void> => {
|
||||
deleteLocalStorageKey(LOCALSTORAGE.CHAT_SUPPORT);
|
||||
deleteLocalStorageKey(LOCALSTORAGE.USER_ID);
|
||||
window.dispatchEvent(new CustomEvent('LOGOUT'));
|
||||
history.push(ROUTES.LOGIN);
|
||||
navigate(ROUTES.LOGIN);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Row, Select, Spin } from 'antd';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import {
|
||||
getValuesFromQueryParams,
|
||||
setQueryParamsFromOptions,
|
||||
@@ -35,8 +36,7 @@ export function FilterSelect({
|
||||
useCeleryFilterOptions(filterType);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const location = useAppLocation();
|
||||
|
||||
// Add state to track the current search input
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
@@ -66,7 +66,7 @@ export function FilterSelect({
|
||||
setQueryParamsFromOptions(
|
||||
value as string[],
|
||||
urlQuery,
|
||||
history,
|
||||
navigate,
|
||||
location,
|
||||
queryParam,
|
||||
);
|
||||
@@ -77,7 +77,6 @@ export function FilterSelect({
|
||||
handleSearch,
|
||||
shouldSetQueryParams,
|
||||
urlQuery,
|
||||
history,
|
||||
location,
|
||||
queryParam,
|
||||
onChange,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Select, Spin } from 'antd';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -16,8 +17,7 @@ import './CeleryTaskConfigOptions.styles.scss';
|
||||
function CeleryTaskConfigOptions(): JSX.Element {
|
||||
const { handleSearch, isFetching, options } =
|
||||
useCeleryFilterOptions('celery.task_name');
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const location = useAppLocation();
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
@@ -52,7 +52,7 @@ function CeleryTaskConfigOptions(): JSX.Element {
|
||||
setQueryParamsFromOptions(
|
||||
value,
|
||||
urlQuery,
|
||||
history,
|
||||
navigate,
|
||||
location,
|
||||
QueryParams.taskName,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
@@ -50,8 +51,7 @@ function CeleryTaskBar({
|
||||
queryEnabled: boolean;
|
||||
checkIfDataExists?: (isDataAvailable: boolean) => void;
|
||||
}): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const dispatch = useDispatch();
|
||||
const urlQuery = useUrlQuery();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -68,13 +68,13 @@ function CeleryTaskBar({
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
navigate(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch, history, pathname, urlQuery],
|
||||
[dispatch, pathname, urlQuery],
|
||||
);
|
||||
|
||||
const [barState, setBarState] = useState<CeleryTaskState>(CeleryTaskState.All);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
@@ -54,8 +55,7 @@ function CeleryTaskGraph({
|
||||
checkIfDataExists?: (isDataAvailable: boolean) => void;
|
||||
analyticsEvent?: string;
|
||||
}): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const dispatch = useDispatch();
|
||||
const urlQuery = useUrlQuery();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -83,13 +83,13 @@ function CeleryTaskGraph({
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
navigate(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch, history, pathname, urlQuery],
|
||||
[dispatch, pathname, urlQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Col, Row } from 'antd';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -48,8 +49,7 @@ function CeleryTaskLatencyGraph({
|
||||
queryEnabled: boolean;
|
||||
checkIfDataExists?: (isDataAvailable: boolean) => void;
|
||||
}): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const dispatch = useDispatch();
|
||||
const urlQuery = useUrlQuery();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -80,13 +80,13 @@ function CeleryTaskLatencyGraph({
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
navigate(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch, history, pathname, urlQuery],
|
||||
[dispatch, pathname, urlQuery],
|
||||
);
|
||||
|
||||
const selectedFilters = useMemo(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { History, Location } from 'history';
|
||||
import type { AppLocation, NavigateFn } from 'lib/router/types';
|
||||
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
@@ -17,13 +17,13 @@ export function getValuesFromQueryParams(
|
||||
export function setQueryParamsFromOptions(
|
||||
value: string[],
|
||||
urlQuery: URLSearchParams,
|
||||
history: History<unknown>,
|
||||
location: Location<unknown>,
|
||||
navigate: NavigateFn,
|
||||
location: AppLocation,
|
||||
queryParams: QueryParams,
|
||||
): void {
|
||||
urlQuery.set(queryParams, value.join(','));
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
navigate(generatedUrl, { replace: true });
|
||||
}
|
||||
|
||||
export function getFiltersFromQueryParams(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Button } from 'antd';
|
||||
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -8,7 +8,7 @@ import { MessageSquareText } from '@signozhq/icons';
|
||||
export default function ChatSupportGateway(): JSX.Element {
|
||||
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
|
||||
useState(false);
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import dayjs from 'dayjs';
|
||||
import { mockAppLocation } from 'tests/router-mocks';
|
||||
import * as timeUtils from 'utils/timeUtils';
|
||||
|
||||
import CustomTimePicker from './CustomTimePicker';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useLocation: jest.fn().mockReturnValue({
|
||||
pathname: '/test-path',
|
||||
}),
|
||||
};
|
||||
});
|
||||
mockAppLocation({ pathname: '/test-path' });
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input, InputRef, Popover, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
@@ -109,7 +109,7 @@ function CustomTimePicker({
|
||||
const [inputErrorDetails, setInputErrorDetails] = useState<
|
||||
TimeRangeValidationResult['errorDetails'] | null
|
||||
>(null);
|
||||
const location = useLocation();
|
||||
const location = useAppLocation();
|
||||
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const initialInputValueOnOpenRef = useRef<string>('');
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -114,7 +114,7 @@ function CustomTimePickerPopoverContent({
|
||||
customDateTimeInputStatus = CustomTimePickerInputStatus.UNSET,
|
||||
inputErrorDetails,
|
||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
|
||||
const isLogsExplorerPage = useMemo(
|
||||
() => pathname === ROUTES.LOGS_EXPLORER,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { mockAppLocation } from 'tests/router-mocks';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import CustomTimePicker from '../CustomTimePicker';
|
||||
@@ -59,10 +60,6 @@ jest.mock('providers/Timezone', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useLocation: (): { pathname: string } => ({ pathname: '/logs-explorer' }),
|
||||
}));
|
||||
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
const defaultProps = {
|
||||
@@ -85,6 +82,7 @@ describe('CustomTimePicker - zoom out button', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
|
||||
mockAppLocation({ pathname: '/logs-explorer' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -2,23 +2,15 @@ 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 { mockAppLocation } from 'tests/router-mocks';
|
||||
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,
|
||||
}),
|
||||
}));
|
||||
mockAppLocation({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
|
||||
});
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { mockAppLocation } from 'tests/router-mocks';
|
||||
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}/`,
|
||||
}),
|
||||
}));
|
||||
mockAppLocation({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
|
||||
});
|
||||
|
||||
jest.mock('antd', () => ({
|
||||
...jest.requireActual('antd'),
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
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: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Button, Input } from 'antd';
|
||||
import { ToggleGroup } from '@signozhq/ui/toggle-group';
|
||||
@@ -11,7 +11,7 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState('feedback');
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const location = useLocation();
|
||||
const location = useAppLocation();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Dot } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
@@ -39,7 +39,7 @@ function HeaderRightSection({
|
||||
enableFeedback,
|
||||
shareModalExtraOption,
|
||||
}: HeaderRightSectionProps): JSX.Element | null {
|
||||
const location = useLocation();
|
||||
const location = useAppLocation();
|
||||
|
||||
const [openFeedbackModal, setOpenFeedbackModal] = useState(false);
|
||||
const [openShareURLModal, setOpenShareURLModal] = useState(false);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
@@ -41,7 +42,7 @@ interface ShareURLModalProps {
|
||||
|
||||
function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
const location = useAppLocation();
|
||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
@@ -75,7 +76,7 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
|
||||
const isRouteToBeSharedWithTime = useMemo(
|
||||
() =>
|
||||
routesToBeSharedWithTime.some((route) =>
|
||||
matchPath(location.pathname, { path: route, exact: true }),
|
||||
matchRoute(location.pathname, route, { exact: true }),
|
||||
),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mock dependencies before imports
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -9,9 +9,8 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
|
||||
import FeedbackModal from '../FeedbackModal';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
jest.mock('lib/router/useAppLocation', () => ({
|
||||
useAppLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
@@ -30,7 +29,7 @@ jest.mock('container/Integrations/utils', () => ({
|
||||
handleContactSupport: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseLocation = useLocation as jest.Mock;
|
||||
const mockUseAppLocation = useAppLocation as jest.Mock;
|
||||
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
|
||||
const mockHandleContactSupport = handleContactSupport as jest.Mock;
|
||||
const mockToast = toast as jest.Mocked<typeof toast>;
|
||||
@@ -45,7 +44,7 @@ describe('FeedbackModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
logEventMock.mockReturnValue(Promise.resolve() as never);
|
||||
mockUseLocation.mockReturnValue(mockLocation);
|
||||
mockUseAppLocation.mockReturnValue(mockLocation);
|
||||
mockUseGetTenantLicense.mockReturnValue({
|
||||
isCloudUser: false,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mock dependencies before imports
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { logEventMock } from '__tests__/logEventMock';
|
||||
@@ -7,9 +7,8 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
|
||||
import HeaderRightSection from '../HeaderRightSection';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
jest.mock('lib/router/useAppLocation', () => ({
|
||||
useAppLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../FeedbackModal', () => ({
|
||||
@@ -45,7 +44,7 @@ jest.mock('hooks/useIsAIAssistantEnabled', () => ({
|
||||
useIsAIAssistantEnabled: (): boolean => false,
|
||||
}));
|
||||
|
||||
const mockUseLocation = useLocation as jest.Mock;
|
||||
const mockUseAppLocation = useAppLocation as jest.Mock;
|
||||
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
|
||||
|
||||
const defaultProps = {
|
||||
@@ -61,7 +60,7 @@ const mockLocation = {
|
||||
describe('HeaderRightSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseLocation.mockReturnValue(mockLocation);
|
||||
mockUseAppLocation.mockReturnValue(mockLocation);
|
||||
// Default to licensed user (Enterprise or Cloud)
|
||||
mockUseGetTenantLicense.mockReturnValue({
|
||||
isCloudUser: true,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Mock dependencies before imports
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -12,10 +13,12 @@ import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
import ShareURLModal from '../ShareURLModal';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
matchPath: jest.fn(),
|
||||
jest.mock('lib/router/useAppLocation', () => ({
|
||||
useAppLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('lib/router/matchRoute', () => ({
|
||||
matchRoute: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useUrlQuery', () => ({
|
||||
@@ -48,12 +51,12 @@ Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const mockUseLocation = useLocation as jest.Mock;
|
||||
const mockUseAppLocation = useAppLocation as jest.Mock;
|
||||
const mockUseUrlQuery = useUrlQuery as jest.Mock;
|
||||
const mockUseSelector = useSelector as jest.Mock;
|
||||
const mockGetMinMax = GetMinMax as jest.Mock;
|
||||
const mockUseCopyToClipboard = useCopyToClipboard as jest.Mock;
|
||||
const mockMatchPath = matchPath as jest.Mock;
|
||||
const mockMatchRoute = matchRoute as jest.Mock;
|
||||
|
||||
const mockUrlQuery = {
|
||||
get: jest.fn(),
|
||||
@@ -71,7 +74,7 @@ describe('ShareURLModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseLocation.mockReturnValue({
|
||||
mockUseAppLocation.mockReturnValue({
|
||||
pathname: TEST_PATH,
|
||||
});
|
||||
|
||||
@@ -88,7 +91,7 @@ describe('ShareURLModal', () => {
|
||||
|
||||
mockUseCopyToClipboard.mockReturnValue([null, mockHandleCopyToClipboard]);
|
||||
|
||||
mockMatchPath.mockReturnValue(false);
|
||||
mockMatchRoute.mockReturnValue(null);
|
||||
|
||||
// Reset URL query mocks - all return null by default
|
||||
mockUrlQuery.get.mockReturnValue(null);
|
||||
@@ -126,7 +129,7 @@ describe('ShareURLModal', () => {
|
||||
});
|
||||
|
||||
it('should show absolute time toggle when on time-enabled route', () => {
|
||||
mockMatchPath.mockReturnValue(true); // Simulate being on a route that supports time
|
||||
mockMatchRoute.mockReturnValue({}); // Simulate being on a route that supports time
|
||||
|
||||
render(<ShareURLModal />);
|
||||
|
||||
@@ -146,7 +149,7 @@ describe('ShareURLModal', () => {
|
||||
|
||||
it('should toggle absolute time switch', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMatchPath.mockReturnValue(true);
|
||||
mockMatchRoute.mockReturnValue({});
|
||||
mockUseSelector.mockReturnValue({
|
||||
selectedTime: '5min', // Non-custom time should enable absolute time by default
|
||||
});
|
||||
@@ -169,7 +172,7 @@ describe('ShareURLModal', () => {
|
||||
// Invalid - missing start and end time for custom
|
||||
mockUrlQuery.get.mockReturnValue(null);
|
||||
|
||||
mockMatchPath.mockReturnValue(true);
|
||||
mockMatchRoute.mockReturnValue({});
|
||||
|
||||
render(<ShareURLModal />);
|
||||
|
||||
@@ -181,7 +184,7 @@ describe('ShareURLModal', () => {
|
||||
|
||||
it('should process URL with absolute time for non-custom time', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMatchPath.mockReturnValue(true);
|
||||
mockMatchRoute.mockReturnValue({});
|
||||
mockUseSelector.mockReturnValue({
|
||||
selectedTime: '5min',
|
||||
});
|
||||
@@ -200,7 +203,7 @@ describe('ShareURLModal', () => {
|
||||
|
||||
it('should process URL with custom time parameters', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMatchPath.mockReturnValue(true);
|
||||
mockMatchRoute.mockReturnValue({});
|
||||
mockUseSelector.mockReturnValue({
|
||||
selectedTime: 'custom',
|
||||
});
|
||||
@@ -228,7 +231,7 @@ describe('ShareURLModal', () => {
|
||||
|
||||
it('should process URL with relative time when absolute time is disabled', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMatchPath.mockReturnValue(true);
|
||||
mockMatchRoute.mockReturnValue({});
|
||||
mockUseSelector.mockReturnValue({
|
||||
selectedTime: '5min',
|
||||
});
|
||||
@@ -249,12 +252,12 @@ describe('ShareURLModal', () => {
|
||||
|
||||
it('should handle routes that should be shared with time', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockUseLocation.mockReturnValue({
|
||||
mockUseAppLocation.mockReturnValue({
|
||||
pathname: ROUTES.LOGS_EXPLORER,
|
||||
});
|
||||
|
||||
mockMatchPath.mockImplementation(
|
||||
(pathname: string, options: any) => options.path === ROUTES.LOGS_EXPLORER,
|
||||
mockMatchRoute.mockImplementation((pathname: string, route: string) =>
|
||||
route === ROUTES.LOGS_EXPLORER ? {} : null,
|
||||
);
|
||||
|
||||
render(<ShareURLModal />);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -43,7 +43,7 @@ function LaunchChatSupport({
|
||||
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
|
||||
const isChatSupportEnabled = useMemo(() => {
|
||||
if (!isFetchingFeatureFlags && (featureFlags || featureFlagsFetchError)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AppLink } from 'lib/router/AppLink';
|
||||
|
||||
import styles from './LogHighlights.module.scss';
|
||||
|
||||
@@ -8,7 +8,7 @@ interface TraceIdFieldProps {
|
||||
|
||||
function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
|
||||
return (
|
||||
<Link
|
||||
<AppLink
|
||||
to={{ pathname: `/trace/${traceId}` }}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -16,7 +16,7 @@ function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
|
||||
title={traceId}
|
||||
>
|
||||
{traceId}
|
||||
</Link>
|
||||
</AppLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
OctagonAlert,
|
||||
Bolt,
|
||||
@@ -14,7 +13,7 @@ import { Modal, Select, Spin, Tooltip, Tree, TreeDataNode } from 'antd';
|
||||
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History } from 'history';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
@@ -45,7 +44,6 @@ export enum AttributesFilters {
|
||||
function ErrorTitleAndKey({
|
||||
title,
|
||||
parentTitle,
|
||||
history,
|
||||
isCloudUserVal,
|
||||
errorMsg,
|
||||
isLeaf,
|
||||
@@ -53,7 +51,6 @@ function ErrorTitleAndKey({
|
||||
title: string;
|
||||
parentTitle: string;
|
||||
isCloudUserVal: boolean;
|
||||
history: History<unknown>;
|
||||
errorMsg?: string;
|
||||
isLeaf?: boolean;
|
||||
}): TreeDataNode {
|
||||
@@ -75,7 +72,7 @@ function ErrorTitleAndKey({
|
||||
}
|
||||
|
||||
if (isCloudUserVal && !!link) {
|
||||
history.push(link);
|
||||
navigate(link);
|
||||
} else {
|
||||
openInNewTab(KAFKA_SETUP_DOC_LINK);
|
||||
}
|
||||
@@ -149,7 +146,6 @@ function generateTreeDataNodes(
|
||||
response: OnboardingStatusResponse['data'],
|
||||
parentTitle: string,
|
||||
isCloudUserVal: boolean,
|
||||
history: History<unknown>,
|
||||
): TreeDataNode[] {
|
||||
return response
|
||||
.map((item) => {
|
||||
@@ -162,7 +158,6 @@ function generateTreeDataNodes(
|
||||
title: item.attribute,
|
||||
errorMsg: item.error_message || '',
|
||||
parentTitle,
|
||||
history,
|
||||
isCloudUserVal,
|
||||
});
|
||||
}
|
||||
@@ -185,7 +180,6 @@ function AttributeCheckList({
|
||||
setFilter(value);
|
||||
};
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
const filteredData = onboardingStatusResponses.map((response) => {
|
||||
@@ -195,7 +189,6 @@ function AttributeCheckList({
|
||||
errorMsg: response.errorMsg,
|
||||
isLeaf: true,
|
||||
parentTitle: response.title,
|
||||
history,
|
||||
isCloudUserVal,
|
||||
});
|
||||
}
|
||||
@@ -213,7 +206,6 @@ function AttributeCheckList({
|
||||
filteredData,
|
||||
response.title,
|
||||
isCloudUserVal,
|
||||
history,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
import store from 'store';
|
||||
import { TestRouter } from 'tests/router';
|
||||
|
||||
import NotFound from './index';
|
||||
|
||||
describe('Not Found page test', () => {
|
||||
it('should render Not Found page without errors', () => {
|
||||
const { asFragment } = render(
|
||||
<MemoryRouter>
|
||||
<TestRouter>
|
||||
<Provider store={store}>
|
||||
<NotFound />
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
</TestRouter>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AppLink } from 'lib/router/AppLink';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Button = styled(Link)`
|
||||
export const Button = styled(AppLink)`
|
||||
border: 2px solid #2f80ed;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -34,17 +34,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 +62,7 @@ jest.mock('../../QuickFilters', () => ({
|
||||
}));
|
||||
|
||||
const quickFilterProps = {
|
||||
source: QuickFiltersSource.TRACES_EXPLORER,
|
||||
quickFilterSource: QuickFiltersSource.TRACES_EXPLORER,
|
||||
useFieldApis: { startUnixMilli: 0, endUnixMilli: 0 },
|
||||
};
|
||||
const savedViewProps = { source: SavedviewtypesSourceDTO.traces };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
|
||||
import RouteTab from './index';
|
||||
@@ -10,6 +9,11 @@ jest.mock('./RouteTab.module.scss', () => ({
|
||||
default: { routeTab: 'routeTab' },
|
||||
}));
|
||||
|
||||
jest.mock('lib/router/navigation', () => ({
|
||||
...jest.requireActual('lib/router/navigation'),
|
||||
navigate: jest.fn(),
|
||||
}));
|
||||
|
||||
function DummyComponent1(): JSX.Element {
|
||||
return <div>Dummy Component 1</div>;
|
||||
}
|
||||
@@ -33,63 +37,39 @@ const testRoutes: RouteTabProps['routes'] = [
|
||||
];
|
||||
|
||||
describe('RouteTab component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const history = createMemoryHistory();
|
||||
render(
|
||||
<Router history={history}>
|
||||
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
|
||||
</Router>,
|
||||
);
|
||||
render(<RouteTab routes={testRoutes} activeKey="Tab1" />);
|
||||
expect(screen.getByRole('tab', { name: 'Tab1' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Tab2' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correct number of tabs', () => {
|
||||
const history = createMemoryHistory();
|
||||
render(
|
||||
<Router history={history}>
|
||||
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
|
||||
</Router>,
|
||||
);
|
||||
render(<RouteTab routes={testRoutes} activeKey="Tab1" />);
|
||||
const tabs = screen.getAllByRole('tab');
|
||||
expect(tabs).toHaveLength(testRoutes.length);
|
||||
});
|
||||
|
||||
it('sets provided activeKey as active tab', () => {
|
||||
const history = createMemoryHistory();
|
||||
render(
|
||||
<Router history={history}>
|
||||
<RouteTab history={history} routes={testRoutes} activeKey="Tab2" />
|
||||
</Router>,
|
||||
);
|
||||
render(<RouteTab routes={testRoutes} activeKey="Tab2" />);
|
||||
expect(
|
||||
screen.getByRole('tab', { name: 'Tab2', selected: true }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to correct route on tab click', () => {
|
||||
const history = createMemoryHistory();
|
||||
render(
|
||||
<Router history={history}>
|
||||
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
|
||||
</Router>,
|
||||
);
|
||||
expect(history.location.pathname).toBe('/');
|
||||
render(<RouteTab routes={testRoutes} activeKey="Tab1" />);
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
|
||||
expect(history.location.pathname).toBe('/tab2');
|
||||
expect(navigate).toHaveBeenCalledWith('/tab2');
|
||||
});
|
||||
|
||||
it('applies the layout class alongside a custom className', () => {
|
||||
const history = createMemoryHistory();
|
||||
const { container } = render(
|
||||
<Router history={history}>
|
||||
<RouteTab
|
||||
history={history}
|
||||
routes={testRoutes}
|
||||
activeKey="Tab1"
|
||||
className="custom-tabs"
|
||||
/>
|
||||
</Router>,
|
||||
<RouteTab routes={testRoutes} activeKey="Tab1" className="custom-tabs" />,
|
||||
);
|
||||
expect(container.querySelector('.ant-tabs')).toHaveClass(
|
||||
'routeTab',
|
||||
@@ -98,11 +78,8 @@ describe('RouteTab component', () => {
|
||||
});
|
||||
|
||||
it('renders the active tab content inside an overlay scrollbar', () => {
|
||||
const history = createMemoryHistory();
|
||||
const { container } = render(
|
||||
<Router history={history}>
|
||||
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
|
||||
</Router>,
|
||||
<RouteTab routes={testRoutes} activeKey="Tab1" />,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('.ant-tabs-tabpane-active > .overlay-scrollbar'),
|
||||
@@ -111,16 +88,12 @@ describe('RouteTab component', () => {
|
||||
|
||||
it('calls onChangeHandler on tab change', () => {
|
||||
const onChangeHandler = jest.fn();
|
||||
const history = createMemoryHistory();
|
||||
render(
|
||||
<Router history={history}>
|
||||
<RouteTab
|
||||
routes={testRoutes}
|
||||
activeKey="Tab1"
|
||||
onChangeHandler={onChangeHandler}
|
||||
history={history}
|
||||
/>
|
||||
</Router>,
|
||||
<RouteTab
|
||||
routes={testRoutes}
|
||||
activeKey="Tab1"
|
||||
onChangeHandler={onChangeHandler}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
|
||||
expect(onChangeHandler).toHaveBeenCalled();
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
generatePath,
|
||||
matchPath,
|
||||
useLocation,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { buildRoutePath } from 'lib/router/buildRoutePath';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { useAppParams } from 'lib/router/useAppParams';
|
||||
|
||||
import { RouteTabProps } from './types';
|
||||
|
||||
@@ -28,21 +27,17 @@ function RouteTab({
|
||||
routes,
|
||||
activeKey,
|
||||
onChangeHandler,
|
||||
history,
|
||||
showRightSection,
|
||||
className,
|
||||
...rest
|
||||
}: RouteTabProps & TabsProps): JSX.Element {
|
||||
const params = useParams<Params>();
|
||||
const location = useLocation();
|
||||
const params = useAppParams<Params>();
|
||||
const location = useAppLocation();
|
||||
|
||||
// Find the matching route for the current pathname
|
||||
const currentRoute = routes.find((route) => {
|
||||
const routePath = route.route.split('?')[0];
|
||||
return matchPath(location.pathname, {
|
||||
path: routePath,
|
||||
exact: true,
|
||||
});
|
||||
return matchRoute(location.pathname, routePath, { exact: true });
|
||||
});
|
||||
|
||||
const onChange = (activeRoute: string): void => {
|
||||
@@ -53,8 +48,7 @@ function RouteTab({
|
||||
const selectedRoute = routes.find((e) => e.key === activeRoute);
|
||||
|
||||
if (selectedRoute) {
|
||||
const resolvedRoute = generatePath(selectedRoute.route, params);
|
||||
history.push(resolvedRoute);
|
||||
navigate(buildRoutePath(selectedRoute.route, params));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { TabsProps } from 'antd';
|
||||
import { History } from 'history';
|
||||
|
||||
export type TabRoutes = {
|
||||
name: React.ReactNode;
|
||||
@@ -13,6 +12,5 @@ export interface RouteTabProps {
|
||||
routes: TabRoutes[];
|
||||
activeKey: TabsProps['activeKey'];
|
||||
onChangeHandler?: (key: string) => void;
|
||||
history: History<unknown>;
|
||||
showRightSection: boolean;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
// ---- Mocks (must run BEFORE importing the component) ----
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
@@ -24,61 +24,12 @@ afterAll(() => {
|
||||
delete (HTMLElement.prototype as any).scrollIntoView;
|
||||
});
|
||||
|
||||
// mock history.push / replace / go / location
|
||||
jest.mock('lib/history', () => {
|
||||
const location = { pathname: '/', search: '', hash: '' };
|
||||
jest.mock('lib/router/navigation', () => ({
|
||||
...jest.requireActual('lib/router/navigation'),
|
||||
navigate: jest.fn(),
|
||||
}));
|
||||
|
||||
const stack: { pathname: string; search: string }[] = [
|
||||
{ pathname: '/', search: '' },
|
||||
];
|
||||
|
||||
const push = jest.fn((path: string) => {
|
||||
const [rawPath, rawQuery] = path.split('?');
|
||||
const pathname = rawPath || '/';
|
||||
const search = path.includes('?') ? `?${rawQuery || ''}` : '';
|
||||
|
||||
location.pathname = pathname;
|
||||
location.search = search;
|
||||
|
||||
stack.push({ pathname, search });
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const replace = jest.fn((path: string) => {
|
||||
const [rawPath, rawQuery] = path.split('?');
|
||||
const pathname = rawPath || '/';
|
||||
const search = path.includes('?') ? `?${rawQuery || ''}` : '';
|
||||
|
||||
location.pathname = pathname;
|
||||
location.search = search;
|
||||
|
||||
if (stack.length > 0) {
|
||||
stack[stack.length - 1] = { pathname, search };
|
||||
} else {
|
||||
stack.push({ pathname, search });
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const listen = jest.fn();
|
||||
const go = jest.fn((n: number) => {
|
||||
if (n < 0 && stack.length > 1) {
|
||||
stack.pop();
|
||||
}
|
||||
const top = stack[stack.length - 1] || { pathname: '/', search: '' };
|
||||
location.pathname = top.pathname;
|
||||
location.search = top.search;
|
||||
});
|
||||
|
||||
return {
|
||||
push,
|
||||
replace,
|
||||
listen,
|
||||
go,
|
||||
location,
|
||||
__stack: stack,
|
||||
};
|
||||
});
|
||||
const mockNavigate = navigate as jest.MockedFunction<typeof navigate>;
|
||||
|
||||
// Mock ResizeObserver for Jest/jsdom
|
||||
class ResizeObserver {
|
||||
@@ -159,14 +110,14 @@ describe('CmdKPalette', () => {
|
||||
expect(screen.getByText('Switch to Dark Mode')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking a navigation item calls history.push with correct route', async () => {
|
||||
it('clicking a navigation item navigates to the correct route', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CmdKPalette userRole="ADMIN" />);
|
||||
|
||||
const homeItem = screen.getByText(HOME_LABEL);
|
||||
await user.click(homeItem);
|
||||
|
||||
expect(history.push).toHaveBeenCalledWith(ROUTES.HOME);
|
||||
expect(mockNavigate).toHaveBeenCalledWith(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('role-based filtering (basic smoke)', () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
@@ -23,7 +22,8 @@ import {
|
||||
import { useThemeMode } from 'hooks/useDarkMode';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { IS_DEV } from 'lib/env';
|
||||
import history from 'lib/history';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { ROLES as UserRole } from 'types/roles';
|
||||
|
||||
import { createShortcutActions } from '../../constants/shortcutActions';
|
||||
@@ -77,7 +77,7 @@ export function CmdKPalette({
|
||||
const { open, setOpen } = useCmdK();
|
||||
|
||||
const { setAutoSwitch, setTheme, theme } = useThemeMode();
|
||||
const location = useLocation();
|
||||
const location = useAppLocation();
|
||||
const isAIAssistantEnabled = useIsAIAssistantEnabled();
|
||||
const startNewConversation = useAIAssistantStore(
|
||||
(s) => s.startNewConversation,
|
||||
@@ -120,7 +120,7 @@ export function CmdKPalette({
|
||||
}
|
||||
|
||||
function onClickHandler(key: string): void {
|
||||
history.push(key);
|
||||
navigate(key);
|
||||
}
|
||||
|
||||
const handleOpenAIAssistant = (): void => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Drawer } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -12,8 +12,6 @@ import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { VariantContext } from '../VariantContext';
|
||||
|
||||
export default function AIAssistantDrawer(): JSX.Element {
|
||||
const history = useHistory();
|
||||
|
||||
const isDrawerOpen = useAIAssistantStore((s) => s.isDrawerOpen);
|
||||
const activeConversationId = useAIAssistantStore(
|
||||
(s) => s.activeConversationId,
|
||||
@@ -28,10 +26,10 @@ export default function AIAssistantDrawer(): JSX.Element {
|
||||
return;
|
||||
}
|
||||
closeDrawer();
|
||||
history.push(
|
||||
navigate(
|
||||
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
|
||||
);
|
||||
}, [activeConversationId, closeDrawer, history]);
|
||||
}, [activeConversationId, closeDrawer]);
|
||||
|
||||
const handleNewConversation = useCallback(() => {
|
||||
startNewConversation();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Minus, Plus, X } from '@signozhq/icons';
|
||||
@@ -31,8 +32,7 @@ import styles from './AIAssistantModal.module.scss';
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function AIAssistantModal(): JSX.Element | null {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
|
||||
const isOpen = useAIAssistantStore((s) => s.isModalOpen);
|
||||
@@ -94,11 +94,11 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
|
||||
// the assistant was already open in the modal, so this is a surface
|
||||
// switch, not a new open.
|
||||
history.push(
|
||||
navigate(
|
||||
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
|
||||
{ fromInApp: true },
|
||||
{ state: { fromInApp: true } },
|
||||
);
|
||||
}, [activeConversationId, closeModal, history]);
|
||||
}, [activeConversationId, closeModal]);
|
||||
|
||||
const handleNew = useCallback(() => {
|
||||
void logEvent(AIAssistantEvents.NewChatClicked, {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { matchPath, useHistory, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Plus, X } from '@signozhq/icons';
|
||||
@@ -21,13 +23,11 @@ const AI_ASSISTANT_PANEL_OPEN_CLASS = 'ai-assistant-panel-open';
|
||||
const AI_ASSISTANT_PANEL_WIDTH_VAR = '--ai-assistant-panel-width';
|
||||
|
||||
export default function AIAssistantPanel(): JSX.Element | null {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
|
||||
const isOpen = useAIAssistantStore((s) => s.isDrawerOpen);
|
||||
const isFullScreenPage = !!matchPath(pathname, {
|
||||
path: ROUTES.AI_ASSISTANT,
|
||||
const isFullScreenPage = !!matchRoute(pathname, ROUTES.AI_ASSISTANT, {
|
||||
exact: true,
|
||||
});
|
||||
const activeConversationId = useAIAssistantStore(
|
||||
@@ -47,11 +47,11 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
|
||||
// the assistant was already open in the drawer, so this is a surface
|
||||
// switch, not a new open.
|
||||
history.push(
|
||||
navigate(
|
||||
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
|
||||
{ fromInApp: true },
|
||||
{ state: { fromInApp: true } },
|
||||
);
|
||||
}, [activeConversationId, closeDrawer, history]);
|
||||
}, [activeConversationId, closeDrawer]);
|
||||
|
||||
const handleNew = useCallback(() => {
|
||||
void logEvent(AIAssistantEvents.NewChatClicked, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -21,12 +22,11 @@ import styles from './AIAssistantTrigger.module.scss';
|
||||
* 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 { pathname } = useAppLocation();
|
||||
const isDrawerOpen = useAIAssistantStore((s) => s.isDrawerOpen);
|
||||
const isModalOpen = useAIAssistantStore((s) => s.isModalOpen);
|
||||
|
||||
const isFullScreenPage = !!matchPath(pathname, {
|
||||
path: ROUTES.AI_ASSISTANT,
|
||||
const isFullScreenPage = !!matchRoute(pathname, ROUTES.AI_ASSISTANT, {
|
||||
exact: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import cx from 'classnames';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function ConversationView({
|
||||
}: ConversationViewProps): JSX.Element {
|
||||
const variant = useVariant();
|
||||
const isCompact = variant === 'panel';
|
||||
const location = useLocation();
|
||||
const location = useAppLocation();
|
||||
|
||||
const conversation = useAIAssistantStore(
|
||||
(s) => s.conversations[conversationId],
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { MessageContext } from 'api/ai-assistant/chat';
|
||||
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import { VariantContext } from 'container/AIAssistant/VariantContext';
|
||||
import { TestRouter } from 'tests/router';
|
||||
|
||||
const CHIP_ID = 'recent-errors';
|
||||
const CHIP_TEXT = 'Show me recent errors';
|
||||
@@ -87,11 +86,11 @@ function renderView(variant: 'panel' | 'page' | 'modal'): {
|
||||
getByTestId: (id: string) => HTMLElement;
|
||||
} {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={['/dashboard/dashboard-123']}>
|
||||
<TestRouter initialRoute="/dashboard/dashboard-123">
|
||||
<VariantContext.Provider value={variant}>
|
||||
<ConversationView conversationId={CONVERSATION_ID} />
|
||||
</VariantContext.Provider>
|
||||
</MemoryRouter>,
|
||||
</TestRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { matchPath, useHistory, useLocation } from 'react-router-dom';
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
@@ -213,20 +215,11 @@ function signalMatchesPathname(
|
||||
): boolean {
|
||||
switch (signal) {
|
||||
case ApplyFilterSignalDTO.logs:
|
||||
return Boolean(
|
||||
matchPath(pathname, { path: ROUTES.LOGS_EXPLORER, exact: false }),
|
||||
);
|
||||
return Boolean(matchRoute(pathname, ROUTES.LOGS_EXPLORER));
|
||||
case ApplyFilterSignalDTO.traces:
|
||||
return Boolean(
|
||||
matchPath(pathname, { path: ROUTES.TRACES_EXPLORER, exact: false }),
|
||||
);
|
||||
return Boolean(matchRoute(pathname, ROUTES.TRACES_EXPLORER));
|
||||
case ApplyFilterSignalDTO.metrics:
|
||||
return Boolean(
|
||||
matchPath(pathname, {
|
||||
path: ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||
exact: false,
|
||||
}),
|
||||
);
|
||||
return Boolean(matchRoute(pathname, ROUTES.METRICS_EXPLORER_EXPLORER));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -275,7 +268,6 @@ function explorerRouteForSignal(signal: ApplyFilterSignalDTO): string | null {
|
||||
}
|
||||
|
||||
interface ApplyFilterDeps {
|
||||
history: ReturnType<typeof useHistory>;
|
||||
pathname: string;
|
||||
redirectWithQueryBuilderData: ReturnType<
|
||||
typeof useQueryBuilder
|
||||
@@ -317,8 +309,8 @@ function withDerivedFilterExpressions(query: Query): Query {
|
||||
* page-action recipe — calling redirect alone is not sufficient because
|
||||
* the URL→state effect runs after the next render and the editor binds
|
||||
* to `filter.expression`, not `filters.items`.
|
||||
* - Off-page: use `history.push` so the landing explorer initializes from
|
||||
* the new URL on mount.
|
||||
* - Off-page: `navigate()` so the landing explorer initializes from the
|
||||
* new URL on mount.
|
||||
*/
|
||||
function applyFilter(action: MessageActionDTO, deps: ApplyFilterDeps): void {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -360,9 +352,9 @@ function applyFilter(action: MessageActionDTO, deps: ApplyFilterDeps): void {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[apply_filter] off-page → history.push', base);
|
||||
console.log('[apply_filter] off-page → navigate', base);
|
||||
const encoded = encodeURIComponent(JSON.stringify(normalized));
|
||||
deps.history.push(`${base}?${QueryParams.compositeQuery}=${encoded}`);
|
||||
navigate(`${base}?${QueryParams.compositeQuery}=${encoded}`);
|
||||
}
|
||||
|
||||
/** Picks the right rollback API call for a given action kind. */
|
||||
@@ -391,8 +383,7 @@ export default function ActionsSection({
|
||||
actions,
|
||||
messageId,
|
||||
}: ActionsSectionProps): JSX.Element | null {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
|
||||
const { redirectWithQueryBuilderData, handleSetQueryData } = useQueryBuilder();
|
||||
@@ -424,19 +415,12 @@ export default function ActionsSection({
|
||||
}
|
||||
autoAppliedFilterKeys.add(key);
|
||||
applyFilter(action, {
|
||||
history,
|
||||
pathname,
|
||||
redirectWithQueryBuilderData,
|
||||
handleSetQueryData,
|
||||
});
|
||||
});
|
||||
}, [
|
||||
actions,
|
||||
pathname,
|
||||
history,
|
||||
redirectWithQueryBuilderData,
|
||||
handleSetQueryData,
|
||||
]);
|
||||
}, [actions, pathname, redirectWithQueryBuilderData, handleSetQueryData]);
|
||||
|
||||
if (actions.length === 0) {
|
||||
return null;
|
||||
@@ -456,11 +440,7 @@ export default function ActionsSection({
|
||||
}
|
||||
setResult(key, { state: 'loading' });
|
||||
try {
|
||||
await openSavedViewByKey(
|
||||
resourceId,
|
||||
resolveSavedViewSourceHint(action),
|
||||
history,
|
||||
);
|
||||
await openSavedViewByKey(resourceId, resolveSavedViewSourceHint(action));
|
||||
void logEvent(AIAssistantEvents.ResourceOpened, {
|
||||
threadId,
|
||||
messageId,
|
||||
@@ -515,7 +495,7 @@ export default function ActionsSection({
|
||||
targetModule: targetModuleForResource(resourceType),
|
||||
resourceId,
|
||||
});
|
||||
history.push(path);
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
const handleClick = (key: string, action: MessageActionDTO): void => {
|
||||
@@ -576,7 +556,6 @@ export default function ActionsSection({
|
||||
});
|
||||
}
|
||||
applyFilter(action, {
|
||||
history,
|
||||
pathname,
|
||||
redirectWithQueryBuilderData,
|
||||
handleSetQueryData,
|
||||
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import type { History } from 'history';
|
||||
|
||||
import {
|
||||
buildExplorerNavigationUrl,
|
||||
@@ -39,6 +39,9 @@ import {
|
||||
import { resourceRoute, ResourceType } from '../resourceRoute';
|
||||
|
||||
jest.mock('api/generated/services/saved-view');
|
||||
jest.mock('lib/router/navigation', () => ({
|
||||
navigate: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi',
|
||||
@@ -60,6 +63,7 @@ const mockedListSavedViews = listSavedViews as jest.MockedFunction<
|
||||
const mockedGetSavedView = getSavedView as jest.MockedFunction<
|
||||
typeof getSavedView
|
||||
>;
|
||||
const mockedNavigate = navigate as jest.MockedFunction<typeof navigate>;
|
||||
|
||||
function makeView(
|
||||
id: string,
|
||||
@@ -247,15 +251,17 @@ describe('buildExplorerNavigationUrl', () => {
|
||||
});
|
||||
|
||||
describe('openSavedView', () => {
|
||||
it('navigates with history.push and view query params', () => {
|
||||
const push = jest.fn();
|
||||
const history = { push } as unknown as History;
|
||||
beforeEach(() => {
|
||||
mockedNavigate.mockClear();
|
||||
});
|
||||
|
||||
it('navigates with the view query params', () => {
|
||||
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
|
||||
|
||||
openSavedView(view, history);
|
||||
openSavedView(view);
|
||||
|
||||
expect(push).toHaveBeenCalledTimes(1);
|
||||
const pushedUrl = push.mock.calls[0][0] as string;
|
||||
expect(mockedNavigate).toHaveBeenCalledTimes(1);
|
||||
const pushedUrl = mockedNavigate.mock.calls[0][0] as string;
|
||||
expect(pushedUrl).toContain(ROUTES.LOGS_EXPLORER);
|
||||
const params = new URLSearchParams(pushedUrl.split('?')[1]);
|
||||
expect(params.get(QueryParams.viewKey)).toBe('"view-logs"');
|
||||
@@ -267,18 +273,14 @@ describe('openSavedView', () => {
|
||||
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
|
||||
delete view.source;
|
||||
|
||||
expect(() =>
|
||||
openSavedView(view, { push: jest.fn() } as unknown as History),
|
||||
).toThrow('Unsupported saved view source');
|
||||
expect(() => openSavedView(view)).toThrow('Unsupported saved view source');
|
||||
});
|
||||
|
||||
it('throws when the view has no queries', () => {
|
||||
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
|
||||
view.spec.queries = [];
|
||||
|
||||
expect(() =>
|
||||
openSavedView(view, { push: jest.fn() } as unknown as History),
|
||||
).toThrow('Saved view is missing query data');
|
||||
expect(() => openSavedView(view)).toThrow('Saved view is missing query data');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,44 +288,39 @@ describe('openSavedViewByKey', () => {
|
||||
beforeEach(() => {
|
||||
mockedListSavedViews.mockReset();
|
||||
mockedGetSavedView.mockReset();
|
||||
mockedNavigate.mockClear();
|
||||
});
|
||||
|
||||
it('prefers the direct view lookup endpoint', async () => {
|
||||
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
|
||||
mockedGetSavedView.mockResolvedValueOnce(mockViewByIdResponse(view));
|
||||
const push = jest.fn();
|
||||
const history = { push } as unknown as History;
|
||||
|
||||
await openSavedViewByKey('view-logs', DataSource.LOGS, history);
|
||||
await openSavedViewByKey('view-logs', DataSource.LOGS);
|
||||
|
||||
expect(mockedGetSavedView).toHaveBeenCalledWith({ id: 'view-logs' });
|
||||
expect(mockedListSavedViews).not.toHaveBeenCalled();
|
||||
expect(push).toHaveBeenCalled();
|
||||
expect(mockedNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to list probing when direct lookup fails', async () => {
|
||||
const view = makeView('view-traces', SavedviewtypesSourceDTO.traces);
|
||||
mockedGetSavedView.mockRejectedValueOnce(new Error('not found'));
|
||||
mockedListSavedViews.mockResolvedValueOnce(mockViewsResponse([view]));
|
||||
const push = jest.fn();
|
||||
const history = { push } as unknown as History;
|
||||
|
||||
await openSavedViewByKey('view-traces', DataSource.TRACES, history);
|
||||
await openSavedViewByKey('view-traces', DataSource.TRACES);
|
||||
|
||||
expect(mockedListSavedViews).toHaveBeenCalledWith({
|
||||
source: SavedviewtypesSourceDTO.traces,
|
||||
});
|
||||
expect(push).toHaveBeenCalled();
|
||||
expect(mockedNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws when the saved view does not exist', async () => {
|
||||
mockedGetSavedView.mockRejectedValueOnce(new Error('not found'));
|
||||
mockedListSavedViews.mockResolvedValue(mockViewsResponse([]));
|
||||
|
||||
await expect(
|
||||
openSavedViewByKey('missing', DataSource.LOGS, {
|
||||
push: jest.fn(),
|
||||
} as unknown as History),
|
||||
).rejects.toThrow('Saved view not found');
|
||||
await expect(openSavedViewByKey('missing', DataSource.LOGS)).rejects.toThrow(
|
||||
'Saved view not found',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,10 +9,10 @@ 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 { navigate } from 'lib/router/navigation';
|
||||
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { History } from 'history';
|
||||
|
||||
type SavedViewSourceHint = SavedViewSourcePage;
|
||||
|
||||
@@ -92,10 +92,7 @@ export function buildExplorerNavigationUrl(
|
||||
return `${route}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function openSavedView(
|
||||
view: SavedviewtypesSavedViewDTO,
|
||||
history: History,
|
||||
): void {
|
||||
export function openSavedView(view: SavedviewtypesSavedViewDTO): void {
|
||||
const route = view.source ? explorerRouteForSourcePage(view.source) : null;
|
||||
if (!route) {
|
||||
throw new Error('Unsupported saved view source');
|
||||
@@ -111,14 +108,13 @@ export function openSavedView(
|
||||
[QueryParams.viewName]: view.spec.displayName,
|
||||
[QueryParams.viewKey]: view.id,
|
||||
});
|
||||
history.push(url);
|
||||
navigate(url);
|
||||
}
|
||||
|
||||
export async function openSavedViewByKey(
|
||||
viewKey: string,
|
||||
sourceHint: SavedViewSourceHint | null | undefined,
|
||||
history: History,
|
||||
): Promise<void> {
|
||||
const view = await loadSavedView(viewKey, sourceHint);
|
||||
openSavedView(view, history);
|
||||
openSavedView(view);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MessageContext } from 'api/ai-assistant/chat';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { AlertListTabs } from 'pages/AlertList/types';
|
||||
import { NEW_PANEL_ID } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Resolves the page the user is currently on into structured `MessageContext`
|
||||
@@ -33,9 +33,10 @@ export function getAutoContexts(
|
||||
|
||||
// Panel editor (V2). `panel/new` has no widget id yet and the schema requires
|
||||
// a non-empty `panel_edit.widgetId`, so it reports `panel_create` instead.
|
||||
const panelEditorMatch = matchPath<{ dashboardId: string; panelId: string }>(
|
||||
const panelEditorMatch = matchRoute<'dashboardId' | 'panelId'>(
|
||||
pathname,
|
||||
{ path: ROUTES.DASHBOARD_PANEL_EDITOR, exact: true },
|
||||
ROUTES.DASHBOARD_PANEL_EDITOR,
|
||||
{ exact: true },
|
||||
);
|
||||
if (panelEditorMatch) {
|
||||
const { dashboardId, panelId } = panelEditorMatch.params;
|
||||
@@ -55,8 +56,7 @@ export function getAutoContexts(
|
||||
// Dashboard detail — `/dashboard/:dashboardId`. The `expandedWidgetId`
|
||||
// query param signals the panel-fullscreen overlay; otherwise it's the
|
||||
// plain dashboard view.
|
||||
const dashboardMatch = matchPath<{ dashboardId: string }>(pathname, {
|
||||
path: ROUTES.DASHBOARD,
|
||||
const dashboardMatch = matchRoute<'dashboardId'>(pathname, ROUTES.DASHBOARD, {
|
||||
exact: true,
|
||||
});
|
||||
if (dashboardMatch) {
|
||||
@@ -89,7 +89,7 @@ export function getAutoContexts(
|
||||
}
|
||||
|
||||
// Dashboard list — `/dashboard`.
|
||||
if (matchPath(pathname, { path: ROUTES.ALL_DASHBOARD, exact: true })) {
|
||||
if (matchRoute(pathname, ROUTES.ALL_DASHBOARD, { exact: true })) {
|
||||
return [
|
||||
{
|
||||
source: 'auto',
|
||||
@@ -106,8 +106,8 @@ export function getAutoContexts(
|
||||
// or `/alerts/history?ruleId=…`. Mirrors dashboard_detail: resourceId is the
|
||||
// rule id and shared metadata carries the URL time range when present.
|
||||
if (
|
||||
matchPath(pathname, { path: ROUTES.ALERT_OVERVIEW, exact: true }) ||
|
||||
matchPath(pathname, { path: ROUTES.ALERT_HISTORY, exact: true })
|
||||
matchRoute(pathname, ROUTES.ALERT_OVERVIEW, { exact: true }) ||
|
||||
matchRoute(pathname, ROUTES.ALERT_HISTORY, { exact: true })
|
||||
) {
|
||||
const ruleId = params.get(QueryParams.ruleId);
|
||||
if (ruleId) {
|
||||
@@ -129,7 +129,7 @@ export function getAutoContexts(
|
||||
// Alert edit — `/alerts/edit?ruleId=…`. The form syncs its query-builder
|
||||
// state to the URL (`useShareBuilderUrl`), so shared metadata carries the
|
||||
// alert's query + time range, mirroring the dashboard panel editor.
|
||||
if (matchPath(pathname, { path: ROUTES.EDIT_ALERTS, exact: true })) {
|
||||
if (matchRoute(pathname, ROUTES.EDIT_ALERTS, { exact: true })) {
|
||||
const ruleId = params.get(QueryParams.ruleId);
|
||||
if (ruleId) {
|
||||
return [
|
||||
@@ -145,7 +145,7 @@ export function getAutoContexts(
|
||||
|
||||
// Alert new — `/alerts/new`. No rule id yet (draft), but the query-builder
|
||||
// state is on the URL, so shared metadata carries the in-progress query.
|
||||
if (matchPath(pathname, { path: ROUTES.ALERTS_NEW, exact: true })) {
|
||||
if (matchRoute(pathname, ROUTES.ALERTS_NEW, { exact: true })) {
|
||||
return [
|
||||
{
|
||||
source: 'auto',
|
||||
@@ -157,7 +157,7 @@ export function getAutoContexts(
|
||||
}
|
||||
|
||||
// Triggered-alerts index — `/alerts/history` without a rule id.
|
||||
if (matchPath(pathname, { path: ROUTES.ALERT_HISTORY, exact: true })) {
|
||||
if (matchRoute(pathname, ROUTES.ALERT_HISTORY, { exact: true })) {
|
||||
return [
|
||||
{
|
||||
source: 'auto',
|
||||
@@ -172,7 +172,7 @@ export function getAutoContexts(
|
||||
}
|
||||
|
||||
// Alerts index — `/alerts` with tab query param (defaults to Alert Rules).
|
||||
if (matchPath(pathname, { path: ROUTES.LIST_ALL_ALERT, exact: true })) {
|
||||
if (matchRoute(pathname, ROUTES.LIST_ALL_ALERT, { exact: true })) {
|
||||
const page = resolveAlertsIndexPage(params.get(QueryParams.tab));
|
||||
return [
|
||||
{
|
||||
@@ -191,10 +191,10 @@ export function getAutoContexts(
|
||||
|
||||
// Service detail (covers sub-routes like top-level-operations) —
|
||||
// `/services/:servicename[/...]`.
|
||||
const serviceMatch = matchPath<{ servicename: string }>(pathname, {
|
||||
path: ROUTES.SERVICE_METRICS,
|
||||
exact: false,
|
||||
});
|
||||
const serviceMatch = matchRoute<'servicename'>(
|
||||
pathname,
|
||||
ROUTES.SERVICE_METRICS,
|
||||
);
|
||||
if (serviceMatch?.params.servicename) {
|
||||
return [
|
||||
{
|
||||
@@ -210,7 +210,7 @@ export function getAutoContexts(
|
||||
}
|
||||
|
||||
// Services list — `/services`.
|
||||
if (matchPath(pathname, { path: ROUTES.APPLICATION, exact: true })) {
|
||||
if (matchRoute(pathname, ROUTES.APPLICATION, { exact: true })) {
|
||||
return [
|
||||
{
|
||||
source: 'auto',
|
||||
@@ -226,7 +226,7 @@ export function getAutoContexts(
|
||||
|
||||
// ── Logs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
if (matchPath(pathname, { path: ROUTES.LOGS_EXPLORER, exact: false })) {
|
||||
if (matchRoute(pathname, ROUTES.LOGS_EXPLORER)) {
|
||||
const activeLogId = params.get(QueryParams.activeLogId);
|
||||
// `?activeLogId=…` indicates a log-detail panel is open. Per the
|
||||
// schema, log_detail requires payload fields (timestamp, service,
|
||||
@@ -251,8 +251,7 @@ export function getAutoContexts(
|
||||
|
||||
// Trace detail — `/trace/:id`. Treated as a detail-as-metadata page
|
||||
// (resourceId null, `traceId` lives in metadata).
|
||||
const traceMatch = matchPath<{ id: string }>(pathname, {
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
const traceMatch = matchRoute<'id'>(pathname, ROUTES.TRACE_DETAIL, {
|
||||
exact: true,
|
||||
});
|
||||
if (traceMatch?.params.id) {
|
||||
@@ -272,7 +271,7 @@ export function getAutoContexts(
|
||||
];
|
||||
}
|
||||
|
||||
if (matchPath(pathname, { path: ROUTES.TRACES_EXPLORER, exact: false })) {
|
||||
if (matchRoute(pathname, ROUTES.TRACES_EXPLORER)) {
|
||||
return [
|
||||
{
|
||||
source: 'auto',
|
||||
@@ -289,9 +288,7 @@ export function getAutoContexts(
|
||||
// ── Metrics ───────────────────────────────────────────────────────────────
|
||||
|
||||
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer, views).
|
||||
if (
|
||||
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_BASE, exact: false })
|
||||
) {
|
||||
if (matchRoute(pathname, ROUTES.METRICS_EXPLORER_BASE)) {
|
||||
return [
|
||||
{
|
||||
source: 'auto',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
|
||||
import ROUTES from 'constants/routes';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
|
||||
import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { useVariant } from '../VariantContext';
|
||||
@@ -28,7 +28,7 @@ const ROUTE_TEMPLATES = Object.values(ROUTES).sort(
|
||||
|
||||
export function normalizePage(pathname: string): string {
|
||||
for (const template of ROUTE_TEMPLATES) {
|
||||
if (matchPath(pathname, { path: template, exact: true })) {
|
||||
if (matchRoute(pathname, template, { exact: true })) {
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export function normalizePage(pathname: string): string {
|
||||
export function useAIAssistantAnalyticsContext(
|
||||
conversationId?: string,
|
||||
): AIAssistantAnalyticsContext {
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const variant = useVariant();
|
||||
const threadId = useAIAssistantStore((s) => {
|
||||
const id = conversationId ?? s.activeConversationId;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
|
||||
import { getAutoContexts } from './getAutoContexts';
|
||||
|
||||
@@ -45,15 +45,10 @@ export function resolvePageType(
|
||||
// Pseudo-pages with no attachable resource: resolved straight from the
|
||||
// route. They deliberately emit no auto-context chip (see `getAutoContexts`),
|
||||
// so they can't be derived from `metadata.page` like the pages below.
|
||||
if (matchPath(pathname, { path: ROUTES.HOME, exact: true })) {
|
||||
if (matchRoute(pathname, ROUTES.HOME, { exact: true })) {
|
||||
return PageTypeDTO.homepage;
|
||||
}
|
||||
if (
|
||||
matchPath(pathname, {
|
||||
path: ROUTES.INFRASTRUCTURE_MONITORING_BASE,
|
||||
exact: false,
|
||||
})
|
||||
) {
|
||||
if (matchRoute(pathname, ROUTES.INFRASTRUCTURE_MONITORING_BASE)) {
|
||||
return PageTypeDTO.infra_entity_detail;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AppLink } from 'lib/router/AppLink';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Popover } from 'antd';
|
||||
import LogsIcon from 'assets/AlertHistory/LogsIcon';
|
||||
@@ -26,7 +26,7 @@ function PopoverContent({
|
||||
return (
|
||||
<div className="contributor-row-popover-buttons">
|
||||
{!!relatedLogsLink && (
|
||||
<Link
|
||||
<AppLink
|
||||
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
|
||||
className="contributor-row-popover-buttons__button"
|
||||
data-testid="alert-popover-view-logs"
|
||||
@@ -35,10 +35,10 @@ function PopoverContent({
|
||||
<LogsIcon />
|
||||
</div>
|
||||
<div className="text">View Logs</div>
|
||||
</Link>
|
||||
</AppLink>
|
||||
)}
|
||||
{!!relatedTracesLink && (
|
||||
<Link
|
||||
<AppLink
|
||||
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
|
||||
className="contributor-row-popover-buttons__button"
|
||||
data-testid="alert-popover-view-traces"
|
||||
@@ -50,7 +50,7 @@ function PopoverContent({
|
||||
/>
|
||||
</div>
|
||||
<div className="text">View Traces</div>
|
||||
</Link>
|
||||
</AppLink>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import history from 'lib/history';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { ArrowRight } from '@signozhq/icons';
|
||||
|
||||
import TopContributorsContent from './TopContributorsContent';
|
||||
@@ -16,7 +16,7 @@ function TopContributorsCard({
|
||||
topContributorsData,
|
||||
totalCurrentTriggers,
|
||||
}: TopContributorsCardProps): JSX.Element {
|
||||
const { search } = useLocation();
|
||||
const { search } = useAppLocation();
|
||||
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const viewAllTopContributorsParam = searchParams.get('viewAllTopContributors');
|
||||
@@ -43,7 +43,7 @@ function TopContributorsCard({
|
||||
|
||||
return newState;
|
||||
});
|
||||
history.push({ search: searchParams.toString() });
|
||||
navigate({ search: searchParams.toString() });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { QueryParams } from 'constants/query';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
|
||||
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
|
||||
import { uPlotXAxisValuesFormat } from 'lib/uPlotLib/utils/constants';
|
||||
@@ -103,7 +103,7 @@ function HorizontalTimelineGraph({
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
|
||||
history.push({
|
||||
navigate({
|
||||
search: urlQuery.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TimelineFilter, TimelineTab } from 'container/AlertHistory/types';
|
||||
import history from 'lib/history';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { Info } from '@signozhq/icons';
|
||||
import Tabs2 from 'periscope/components/Tabs2';
|
||||
|
||||
@@ -42,7 +42,7 @@ function TimelineTabs(): JSX.Element {
|
||||
}
|
||||
|
||||
function TimelineFilters(): JSX.Element {
|
||||
const { search } = useLocation();
|
||||
const { search } = useAppLocation();
|
||||
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const initialSelectedTab = useMemo(
|
||||
@@ -52,7 +52,7 @@ function TimelineFilters(): JSX.Element {
|
||||
|
||||
const handleFilter = (value: TimelineFilter): void => {
|
||||
searchParams.set('timelineFilter', value);
|
||||
history.push({ search: searchParams.toString() });
|
||||
navigate({ search: searchParams.toString() });
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { AppLink } from 'lib/router/AppLink';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { Button, Card, Input, Space, TableProps, Tooltip, Flex } from 'antd';
|
||||
import { Search } from '@signozhq/icons';
|
||||
import type { ColumnType, TablePaginationConfig } from 'antd/es/table';
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { useAllErrorsQueryState } from 'pages/AllErrors/QueryStateContext';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -67,7 +68,7 @@ function AllErrors(): JSX.Element {
|
||||
const { maxTime, minTime, loading } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const params = useUrlQuery();
|
||||
const { t } = useTranslation(['common']);
|
||||
const {
|
||||
@@ -228,7 +229,9 @@ function AllErrors(): JSX.Element {
|
||||
queryParams.serviceName = serviceFilterValue;
|
||||
}
|
||||
|
||||
history.replace(`${pathname}?${createQueryParams(queryParams)}`);
|
||||
navigate(`${pathname}?${createQueryParams(queryParams)}`, {
|
||||
replace: true,
|
||||
});
|
||||
confirm();
|
||||
},
|
||||
[
|
||||
@@ -336,13 +339,13 @@ function AllErrors(): JSX.Element {
|
||||
...getFilter(onExceptionTypeFilter, 'Search By Exception', 'exceptionType'),
|
||||
render: (value, record): JSX.Element => (
|
||||
<Tooltip overlay={(): JSX.Element => value}>
|
||||
<Link
|
||||
<AppLink
|
||||
to={`${ROUTES.ERROR_DETAIL}?groupId=${
|
||||
record.groupID
|
||||
}×tamp=${getNanoSeconds(record.lastSeen)}`}
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
</AppLink>
|
||||
</Tooltip>
|
||||
),
|
||||
sorter: true,
|
||||
@@ -438,7 +441,7 @@ function AllErrors(): JSX.Element {
|
||||
exceptionType: getFilterString(params.get(urlKey.exceptionType)),
|
||||
});
|
||||
const compositeQuery = params.get(urlKey.compositeQuery) || '';
|
||||
history.replace(
|
||||
navigate(
|
||||
`${pathname}?${createQueryParams({
|
||||
order: updatedOrder,
|
||||
offset: (current - 1) * pageSize,
|
||||
@@ -448,6 +451,7 @@ function AllErrors(): JSX.Element {
|
||||
serviceName,
|
||||
compositeQuery,
|
||||
})}`,
|
||||
{ replace: true },
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider, useSelector } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
@@ -8,6 +7,7 @@ import { rest } from 'msw';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
import store from 'store';
|
||||
import { TestRouter } from 'tests/router';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
@@ -55,9 +55,9 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
},
|
||||
} as any);
|
||||
|
||||
function Exceptions({ initUrl }: { initUrl?: string[] }): JSX.Element {
|
||||
function Exceptions({ initUrl }: { initUrl?: string }): JSX.Element {
|
||||
return (
|
||||
<MemoryRouter initialEntries={initUrl ?? ['/exceptions']}>
|
||||
<TestRouter initialRoute={initUrl ?? '/exceptions'}>
|
||||
<TimezoneProvider>
|
||||
<Provider store={store}>
|
||||
<MockQueryClientProvider>
|
||||
@@ -65,12 +65,12 @@ function Exceptions({ initUrl }: { initUrl?: string[] }): JSX.Element {
|
||||
</MockQueryClientProvider>
|
||||
</Provider>
|
||||
</TimezoneProvider>
|
||||
</MemoryRouter>
|
||||
</TestRouter>
|
||||
);
|
||||
}
|
||||
|
||||
Exceptions.defaultProps = {
|
||||
initUrl: ['/exceptions'],
|
||||
initUrl: '/exceptions',
|
||||
};
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
@@ -130,7 +130,7 @@ describe('Exceptions - All Errors', () => {
|
||||
});
|
||||
|
||||
it('should call useQueries with exact composite query object', async () => {
|
||||
render(<Exceptions initUrl={[INIT_URL_WITH_COMMON_QUERY]} />);
|
||||
render(<Exceptions initUrl={INIT_URL_WITH_COMMON_QUERY} />);
|
||||
await screen.findByText(/redis timeout/i);
|
||||
expect(postListErrorsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -143,11 +143,7 @@ describe('Exceptions - All Errors', () => {
|
||||
it('should navigate to page 2 when pageSize=100 and clicking next', async () => {
|
||||
// Arrange: start with pageSize=100 and offset=0
|
||||
render(
|
||||
<Exceptions
|
||||
initUrl={[
|
||||
`/exceptions?pageSize=100&offset=0&order=ascending&orderParam=serviceName`,
|
||||
]}
|
||||
/>,
|
||||
<Exceptions initUrl="/exceptions?pageSize=100&offset=0&order=ascending&orderParam=serviceName" />,
|
||||
);
|
||||
|
||||
// Wait for initial load
|
||||
@@ -171,11 +167,7 @@ describe('Exceptions - All Errors', () => {
|
||||
it('initializes current page from URL (offset/pageSize)', async () => {
|
||||
// offset=100, pageSize=100 => current page should be 2
|
||||
render(
|
||||
<Exceptions
|
||||
initUrl={[
|
||||
`/exceptions?pageSize=100&offset=100&order=ascending&orderParam=serviceName`,
|
||||
]}
|
||||
/>,
|
||||
<Exceptions initUrl="/exceptions?pageSize=100&offset=100&order=ascending&orderParam=serviceName" />,
|
||||
);
|
||||
await screen.findByText(/redis timeout/i);
|
||||
const activeItem = document.querySelector('.ant-pagination-item-active');
|
||||
@@ -188,11 +180,7 @@ describe('Exceptions - All Errors', () => {
|
||||
it('clicking a numbered page updates offset correctly', async () => {
|
||||
// pageSize=100, click page 3 => offset = 200
|
||||
render(
|
||||
<Exceptions
|
||||
initUrl={[
|
||||
`/exceptions?pageSize=100&offset=0&order=ascending&orderParam=serviceName`,
|
||||
]}
|
||||
/>,
|
||||
<Exceptions initUrl="/exceptions?pageSize=100&offset=0&order=ascending&orderParam=serviceName" />,
|
||||
);
|
||||
await screen.findByText(/redis timeout/i);
|
||||
const page3Item = screen.getByTitle('3');
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -4,16 +4,14 @@ import {
|
||||
getApiMonitoringParams,
|
||||
setApiMonitoringParams,
|
||||
} from 'container/ApiMonitoring/queryParams';
|
||||
import { mockNavigate } from 'tests/router-mocks';
|
||||
|
||||
// Mock react-router-dom hooks
|
||||
jest.mock('react-router-dom', () => {
|
||||
const originalModule = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
...originalModule,
|
||||
useLocation: jest.fn(),
|
||||
useHistory: jest.fn(),
|
||||
};
|
||||
});
|
||||
const navigateSpy = mockNavigate();
|
||||
|
||||
function navigatedSearch(): string | undefined {
|
||||
const [to] = navigateSpy.mock.calls[0];
|
||||
return typeof to === 'string' ? to : to.search;
|
||||
}
|
||||
|
||||
describe('API Monitoring Query Params', () => {
|
||||
describe('getApiMonitoringParams', () => {
|
||||
@@ -58,25 +56,21 @@ describe('API Monitoring Query Params', () => {
|
||||
|
||||
describe('setApiMonitoringParams', () => {
|
||||
it('updates URL with new params (push mode)', () => {
|
||||
const history = {
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
};
|
||||
const search = '';
|
||||
const newParams: Partial<ApiMonitoringParams> = {
|
||||
showIP: false,
|
||||
selectedDomain: 'updated-domain',
|
||||
};
|
||||
|
||||
setApiMonitoringParams(newParams, search, history as any, false);
|
||||
setApiMonitoringParams(newParams, search, false);
|
||||
|
||||
expect(history.push).toHaveBeenCalledWith({
|
||||
search: expect.stringContaining('apiMonitoringParams'),
|
||||
});
|
||||
expect(history.replace).not.toHaveBeenCalled();
|
||||
expect(navigateSpy).toHaveBeenCalledWith(
|
||||
{ search: expect.stringContaining('apiMonitoringParams') },
|
||||
{ replace: false },
|
||||
);
|
||||
|
||||
// Verify that the search string contains the expected encoded params
|
||||
const searchArg = history.push.mock.calls[0][0].search;
|
||||
const searchArg = navigatedSearch();
|
||||
const params = new URLSearchParams(searchArg);
|
||||
const decoded = JSON.parse(
|
||||
decodeURIComponent(params.get('apiMonitoringParams') || ''),
|
||||
@@ -88,30 +82,21 @@ describe('API Monitoring Query Params', () => {
|
||||
});
|
||||
|
||||
it('updates URL with new params (replace mode)', () => {
|
||||
const history = {
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
};
|
||||
const search = '';
|
||||
const newParams: Partial<ApiMonitoringParams> = {
|
||||
showIP: false,
|
||||
selectedDomain: 'updated-domain',
|
||||
};
|
||||
|
||||
setApiMonitoringParams(newParams, search, history as any, true);
|
||||
setApiMonitoringParams(newParams, search, true);
|
||||
|
||||
expect(history.replace).toHaveBeenCalledWith({
|
||||
search: expect.stringContaining('apiMonitoringParams'),
|
||||
});
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
expect(navigateSpy).toHaveBeenCalledWith(
|
||||
{ search: expect.stringContaining('apiMonitoringParams') },
|
||||
{ replace: true },
|
||||
);
|
||||
});
|
||||
|
||||
it('merges new params with existing params', () => {
|
||||
const history = {
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
};
|
||||
|
||||
// Start with some existing params
|
||||
const existingParams: Partial<ApiMonitoringParams> = {
|
||||
showIP: true,
|
||||
@@ -132,10 +117,10 @@ describe('API Monitoring Query Params', () => {
|
||||
selectedEndPointName: '/api/test',
|
||||
};
|
||||
|
||||
setApiMonitoringParams(newParams, search, history as any, false);
|
||||
setApiMonitoringParams(newParams, search, false);
|
||||
|
||||
// Verify merged params
|
||||
const searchArg = history.push.mock.calls[0][0].search;
|
||||
const searchArg = navigatedSearch();
|
||||
const params = new URLSearchParams(searchArg);
|
||||
const decoded = JSON.parse(
|
||||
decodeURIComponent(params.get('apiMonitoringParams') || ''),
|
||||
@@ -161,27 +146,7 @@ describe('API Monitoring Query Params', () => {
|
||||
state: null,
|
||||
};
|
||||
|
||||
// Create mock history object
|
||||
const history = {
|
||||
push: jest.fn((args) => {
|
||||
// Simulate updating the location search
|
||||
location.search = args.search;
|
||||
}),
|
||||
replace: jest.fn((args) => {
|
||||
location.search = args.search;
|
||||
}),
|
||||
length: 1,
|
||||
location,
|
||||
};
|
||||
|
||||
// Set up mocks for useLocation and useHistory
|
||||
const useLocationMock = jest.requireMock('react-router-dom').useLocation;
|
||||
const useHistoryMock = jest.requireMock('react-router-dom').useHistory;
|
||||
|
||||
useLocationMock.mockReturnValue(location);
|
||||
useHistoryMock.mockReturnValue(history);
|
||||
|
||||
return { location, history };
|
||||
return { location };
|
||||
};
|
||||
|
||||
it('retrieves URL params correctly from location', () => {
|
||||
@@ -207,7 +172,7 @@ describe('API Monitoring Query Params', () => {
|
||||
});
|
||||
|
||||
it('updates URL correctly with new params', () => {
|
||||
const { location, history } = mockUseLocationAndHistory();
|
||||
const { location } = mockUseLocationAndHistory();
|
||||
|
||||
const newParams: Partial<ApiMonitoringParams> = {
|
||||
selectedDomain: 'new-domain',
|
||||
@@ -215,13 +180,14 @@ describe('API Monitoring Query Params', () => {
|
||||
};
|
||||
|
||||
// Manually execute the core logic of the hook's setParams function
|
||||
setApiMonitoringParams(newParams, location.search, history as any);
|
||||
setApiMonitoringParams(newParams, location.search);
|
||||
|
||||
expect(history.push).toHaveBeenCalledWith({
|
||||
search: expect.stringContaining('apiMonitoringParams'),
|
||||
});
|
||||
expect(navigateSpy).toHaveBeenCalledWith(
|
||||
{ search: expect.stringContaining('apiMonitoringParams') },
|
||||
{ replace: false },
|
||||
);
|
||||
|
||||
const searchArg = history.push.mock.calls[0][0].search;
|
||||
const searchArg = navigatedSearch();
|
||||
const params = new URLSearchParams(searchArg);
|
||||
const decoded = JSON.parse(
|
||||
decodeURIComponent(params.get('apiMonitoringParams') || ''),
|
||||
@@ -247,20 +213,16 @@ describe('API Monitoring Query Params', () => {
|
||||
const initialSearch = `?${urlParams.toString()}`;
|
||||
|
||||
// Set up mocks
|
||||
const { location, history } = mockUseLocationAndHistory(initialSearch);
|
||||
const { location } = mockUseLocationAndHistory(initialSearch);
|
||||
|
||||
// Manually execute the core logic
|
||||
setApiMonitoringParams(
|
||||
{ selectedView: 'new-view' },
|
||||
location.search,
|
||||
history as any,
|
||||
);
|
||||
setApiMonitoringParams({ selectedView: 'new-view' }, location.search);
|
||||
|
||||
// Verify history was updated
|
||||
expect(history.push).toHaveBeenCalled();
|
||||
expect(navigateSpy).toHaveBeenCalled();
|
||||
|
||||
// Parse the new query params from the URL
|
||||
const searchArg = history.push.mock.calls[0][0].search;
|
||||
const searchArg = navigatedSearch();
|
||||
const params = new URLSearchParams(searchArg);
|
||||
const decoded = JSON.parse(
|
||||
decodeURIComponent(params.get('apiMonitoringParams') || ''),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
|
||||
// --- Types for all API Monitoring query params ---
|
||||
export interface ApiMonitoringParams {
|
||||
@@ -55,7 +56,6 @@ export function getApiMonitoringParams(search: string): ApiMonitoringParams {
|
||||
export function setApiMonitoringParams(
|
||||
newParams: Partial<ApiMonitoringParams>,
|
||||
search: string,
|
||||
history: ReturnType<typeof useHistory>,
|
||||
replace = false,
|
||||
): void {
|
||||
const urlParams = new URLSearchParams(search);
|
||||
@@ -63,11 +63,7 @@ export function setApiMonitoringParams(
|
||||
const merged = { ...current, ...newParams };
|
||||
urlParams.set(PARAM_KEY, encodeParams(merged));
|
||||
const newSearch = `?${urlParams.toString()}`;
|
||||
if (replace) {
|
||||
history.replace({ search: newSearch });
|
||||
} else {
|
||||
history.push({ search: newSearch });
|
||||
}
|
||||
navigate({ search: newSearch }, { replace });
|
||||
}
|
||||
|
||||
// --- React hook to use query params in a component ---
|
||||
@@ -75,15 +71,14 @@ export function useApiMonitoringParams(): [
|
||||
ApiMonitoringParams,
|
||||
(newParams: Partial<ApiMonitoringParams>, replace?: boolean) => void,
|
||||
] {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const location = useAppLocation();
|
||||
const params = getApiMonitoringParams(location.search);
|
||||
|
||||
const setParams = useCallback(
|
||||
(newParams: Partial<ApiMonitoringParams>, replace = false) => {
|
||||
setApiMonitoringParams(newParams, location.search, history, replace);
|
||||
setApiMonitoringParams(newParams, location.search, replace);
|
||||
},
|
||||
[location.search, history],
|
||||
[location.search],
|
||||
);
|
||||
|
||||
return [params, setParams];
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQueries } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Toaster } from '@signozhq/ui/sonner';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
@@ -54,7 +54,7 @@ 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 { navigate } from 'lib/router/navigation';
|
||||
import { isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -193,7 +193,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const { t } = useTranslation(['titles']);
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
@@ -468,7 +468,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
}, [isLoggedIn]);
|
||||
|
||||
const handleUpgrade = useCallback((): void => {
|
||||
history.push(ROUTES.BILLING);
|
||||
navigate(ROUTES.BILLING);
|
||||
}, []);
|
||||
|
||||
const handleFailedPayment = useCallback((): void => {
|
||||
|
||||
@@ -9,8 +9,7 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import CancelSubscriptionBanner from './CancelSubscriptionBanner';
|
||||
|
||||
jest.mock('utils/basePath', () => ({
|
||||
getBasePath: (): string => '/',
|
||||
withBasePath: (path: string): string => path,
|
||||
...jest.requireActual('utils/basePath'),
|
||||
getAbsoluteUrl: (path: string): string => `https://test.signoz.io${path}`,
|
||||
getBaseUrl: (): string => 'https://test.signoz.io',
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
useAIAssistantStore,
|
||||
} from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Dot } from '@signozhq/icons';
|
||||
|
||||
@@ -30,7 +30,7 @@ import styles from './AskNoz.module.scss';
|
||||
* hiding the header button would remove a notification rather than move it.
|
||||
*/
|
||||
function AskNoz(): JSX.Element | null {
|
||||
const { pathname } = useLocation();
|
||||
const { pathname } = useAppLocation();
|
||||
const isAIAssistantEnabled = useIsAIAssistantEnabled();
|
||||
const isDrawerOpen = useAIAssistantStore((state) => state.isDrawerOpen);
|
||||
const isModalOpen = useAIAssistantStore((state) => state.isModalOpen);
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user