Compare commits

..

2 Commits

Author SHA1 Message Date
Tushar Vats
3c4d9666ce fix(querier): guard reversed windows and keep in-process coercion
A DESC cursor at the window boundary leaves end below start, which the
unsigned bucket arithmetic wrapped into a non-terminating loop.

Callers that build a request in-process never reach UnmarshalJSON, so
QueryRange still normalises to ms for them.
2026-10-30 16:58:21 +05:30
Tushar Vats
7285aeecbe fix(querier): stop zero-start OOM and reject malformed time windows
makeBuckets never terminated on a zero start: curEnd - bucket wrapped
below zero, so the clamp back to start never fired.

Start/end are coerced to ms at decode so validation compares like units.
2026-10-30 16:58:21 +05:30
512 changed files with 4821 additions and 6971 deletions

View File

@@ -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\|useAppNavigate\|navigate\|<AppLink` |
| navigation | `useSafeNavigate\|history.push\|<Link` |
| permissions | `useAuthZ\|AuthZGuard\|AuthZButton\|hasEditPermission\|routePermission` |
| flags and prefs | `useFeatureFlag\|FeatureKeys\.\|USER_PREFERENCES\.\|userPreferences` |
| empty and error branches | `isLoading\|isError\|isFetching\|length === 0\|!data` |

View File

@@ -17,7 +17,7 @@ Condition: `model`, `llm.input_messages` (OpenInference sets `llm.model_name` fr
| `gen_ai.response.model`, last source for the request model | vLLM, which writes only the response model |
| `ai.model.id`, `ai.model.provider`, `ai.response.model`, `ai.response.finishReason`, `ai.prompt.messages`, `ai.response.text`, `ai.response.toolCalls`, `ai.usage.promptTokens`, `ai.usage.completionTokens` | Vercel AI SDK v4, Mastra |
| `ai.usage.inputTokens`, `ai.usage.outputTokens`, `ai.usage.cachedInputTokens`, `ai.usage.reasoningTokens` | Vercel AI SDK v5 |
| `langfuse.observation.model.name`, `langfuse.observation.input`, `langfuse.observation.output`, `langfuse.observation.type`, `langfuse.session.id` | Langfuse SDK v3 (Python) and v4 (JS), LiteLLM `langfuse_otel` callback |
| `langfuse.observation.model.name`, `langfuse.session.id` | Langfuse SDK v3 |
| `session.id` | OpenInference, Langfuse, Claude Code |
| `gen_ai.client.session_id` | opentelemetry-hooks runner (Cursor, Copilot, Claude Code hooks) |
| `model`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_creation_tokens` | Claude Code `claude_code.llm_request` spans ([docs](https://code.claude.com/docs/en/monitoring-usage#span-attributes)) |

View File

@@ -1,3 +1,6 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd frontend && pnpm run commitlint --edit $1
branch="$(git rev-parse --abbrev-ref HEAD)"

View File

@@ -1 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd frontend && pnpm lint-staged

View File

@@ -294,8 +294,6 @@
// 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",
{
@@ -591,23 +589,6 @@
"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"
}
}
]
}

View File

@@ -1,6 +1,6 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const glob = require('glob');
function generateChecksum(str, algorithm, encoding) {
return crypto
@@ -9,17 +9,12 @@ function generateChecksum(str, algorithm, encoding) {
.digest(encoding || 'hex');
}
const localesDir = 'public/locales';
const result = {};
fs
.readdirSync(localesDir, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
.forEach((entry) => {
const file = path.join(entry.parentPath, entry.name);
const content = fs.readFileSync(file, { encoding: 'utf-8' });
const key = path.relative(localesDir, file).replace(/\.json$/, '');
result[`/${key}`] = generateChecksum(content);
});
glob.sync(`public/locales/**/*.json`).forEach((path) => {
const [_, lang] = path.split('public/locales');
const content = fs.readFileSync(path, { encoding: 'utf-8' });
result[lang.replace('.json', '')] = generateChecksum(content);
});
fs.writeFileSync('./i18n-translations-hash.json', JSON.stringify(result));

View File

@@ -24,8 +24,8 @@
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "pnpm i18n:generate-hash && ([ -n \"$CI\" ] || pnpm husky:configure) && node scripts/update-registry.cjs",
"husky:configure": "cd .. && husky frontend/.husky",
"postinstall": "pnpm i18n:generate-hash && (is-ci || pnpm husky:configure) && node scripts/update-registry.cjs",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
@@ -70,7 +70,7 @@
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.21.0",
"axios": "1.18.0",
"babel-jest": "^29.6.4",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
@@ -100,8 +100,10 @@
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
"rc-select": "14.10.0",
"react": "18.2.0",
"react-addons-update": "15.6.3",
"react-beautiful-dnd": "13.1.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
@@ -135,6 +137,7 @@
"uplot": "1.6.31",
"uuid": "14.0.1",
"vite": "npm:rolldown-vite@7.3.1",
"vite-plugin-html": "3.2.2",
"zod": "4.3.6",
"zustand": "5.0.11"
},
@@ -175,10 +178,11 @@
"@types/history": "4.7.11",
"@types/jest": "30.0.0",
"@types/lodash-es": "^4.17.4",
"@types/node": "^22.20.5",
"@types/node": "^16.10.3",
"@types/papaparse": "5.3.7",
"@types/react": "18.0.26",
"@types/react-addons-update": "0.14.21",
"@types/react-beautiful-dnd": "13.1.8",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-redux": "^7.1.11",
@@ -188,9 +192,12 @@
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/uuid": "^8.3.1",
"@typescript/native-preview": "7.0.0-dev.20260430.1",
"eslint-plugin-sonarjs": "4.0.2",
"husky": "^9.1.7",
"glob": "^13.0.6",
"husky": "^7.0.4",
"is-ci": "^3.0.1",
"jest-environment-jsdom": "29.7.0",
"jest-styled-components": "^7.2.0",
"json-schema-to-typescript": "^15.0.4",

View File

@@ -1,126 +0,0 @@
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 },
],
},
],
});

View File

@@ -1,123 +0,0 @@
/**
* 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');
}
},
};
},
};

View File

@@ -16,7 +16,6 @@ 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: {
@@ -34,6 +33,5 @@ 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,
},
};

613
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -86,13 +86,6 @@ overrides:
# typescript-plugin-css-modules > postcss-load-config@3 (^1.10.2)
# remove: blocked — babel-plugin-macros 3.1.0 (latest) still uses cosmiconfig@7
yaml@>=1.0.0 <1.10.3: '>=1.10.3 <2'
# via: jest (@jest/core, @jest/console, jest-config) declares @types/node '*',
# which kept 16.18.25 next to the direct ^22
# remove: once `pnpm dedupe` passes trustPolicy and leaves one @types/node
'@types/node': '$@types/node'
trustPolicy: no-downgrade
trustPolicyExclude:
- 'semver@6.3.1 || 5.7.2'
# via: @types/node@22 (undici-types ~6.21.0, no provenance)
# remove: upgrade @types/node to >=24 (undici-types 7.x has provenance)
- 'undici-types@6.21.0'

View File

@@ -1,7 +1,7 @@
import deleteLocalStorageKey from 'api/browser/localstorage/remove';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
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'));
navigate(ROUTES.LOGIN);
history.push(ROUTES.LOGIN);
};

View File

@@ -1,7 +1,6 @@
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,
@@ -36,7 +35,8 @@ export function FilterSelect({
useCeleryFilterOptions(filterType);
const urlQuery = useUrlQuery();
const location = useAppLocation();
const history = useHistory();
const location = useLocation();
// 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,
navigate,
history,
location,
queryParam,
);
@@ -77,6 +77,7 @@ export function FilterSelect({
handleSearch,
shouldSetQueryParams,
urlQuery,
history,
location,
queryParam,
onChange,

View File

@@ -1,6 +1,5 @@
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';
@@ -17,7 +16,8 @@ import './CeleryTaskConfigOptions.styles.scss';
function CeleryTaskConfigOptions(): JSX.Element {
const { handleSearch, isFetching, options } =
useCeleryFilterOptions('celery.task_name');
const location = useAppLocation();
const history = useHistory();
const location = useLocation();
const urlQuery = useUrlQuery();
@@ -52,7 +52,7 @@ function CeleryTaskConfigOptions(): JSX.Element {
setQueryParamsFromOptions(
value,
urlQuery,
navigate,
history,
location,
QueryParams.taskName,
);

View File

@@ -1,9 +1,8 @@
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';
@@ -51,7 +50,8 @@ function CeleryTaskBar({
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const { pathname } = useAppLocation();
const history = useHistory();
const { pathname } = useLocation();
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()}`;
navigate(generatedUrl);
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, urlQuery],
[dispatch, history, pathname, urlQuery],
);
const [barState, setBarState] = useState<CeleryTaskState>(CeleryTaskState.All);

View File

@@ -1,9 +1,8 @@
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';
@@ -55,7 +54,8 @@ function CeleryTaskGraph({
checkIfDataExists?: (isDataAvailable: boolean) => void;
analyticsEvent?: string;
}): JSX.Element {
const { pathname } = useAppLocation();
const history = useHistory();
const { pathname } = useLocation();
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()}`;
navigate(generatedUrl);
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, urlQuery],
[dispatch, history, pathname, urlQuery],
);
return (

View File

@@ -1,9 +1,8 @@
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';
@@ -49,7 +48,8 @@ function CeleryTaskLatencyGraph({
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const { pathname } = useAppLocation();
const history = useHistory();
const { pathname } = useLocation();
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()}`;
navigate(generatedUrl);
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, urlQuery],
[dispatch, history, pathname, urlQuery],
);
const selectedFilters = useMemo(

View File

@@ -1,5 +1,5 @@
import { QueryParams } from 'constants/query';
import type { AppLocation, NavigateFn } from 'lib/router/types';
import { History, Location } from 'history';
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,
navigate: NavigateFn,
location: AppLocation,
history: History<unknown>,
location: Location<unknown>,
queryParams: QueryParams,
): void {
urlQuery.set(queryParams, value.join(','));
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
navigate(generatedUrl, { replace: true });
history.replace(generatedUrl);
}
export function getFiltersFromQueryParams(

View File

@@ -1,12 +1,20 @@
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';
mockAppLocation({ pathname: '/test-path' });
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useLocation: jest.fn().mockReturnValue({
pathname: '/test-path',
}),
};
});
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),

View File

@@ -6,7 +6,7 @@ import {
useRef,
useState,
} from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
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 = useAppLocation();
const location = useLocation();
const inputRef = useRef<InputRef>(null);
const initialInputValueOnOpenRef = useRef<string>('');

View File

@@ -6,7 +6,7 @@ import {
useMemo,
useState,
} from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
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 } = useAppLocation();
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(
() => pathname === ROUTES.LOGS_EXPLORER,

View File

@@ -1,7 +1,6 @@
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';
@@ -60,6 +59,10 @@ 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 = {
@@ -82,7 +85,6 @@ describe('CustomTimePicker - zoom out button', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
mockAppLocation({ pathname: '/logs-explorer' });
});
afterEach(() => {

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
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 = useAppLocation();
const location = useLocation();
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const [isLoading, setIsLoading] = useState(false);

View File

@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Dot, Globe, Inbox, SquarePen } from '@signozhq/icons';
import { useLocation } from 'react-router-dom';
import { Dot } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import Noz from 'components/Noz/Noz';
@@ -16,6 +16,7 @@ import {
import { selectPendingUserInputStreamCount } from 'container/AIAssistant/store/pendingInputSelectors';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { Globe, Inbox, SquarePen } from '@signozhq/icons';
import AnnouncementsModal from './AnnouncementsModal';
import FeedbackModal from './FeedbackModal';
@@ -38,7 +39,7 @@ function HeaderRightSection({
enableFeedback,
shareModalExtraOption,
}: HeaderRightSectionProps): JSX.Element | null {
const location = useAppLocation();
const location = useLocation();
const [openFeedbackModal, setOpenFeedbackModal] = useState(false);
const [openShareURLModal, setOpenShareURLModal] = useState(false);

View File

@@ -1,8 +1,7 @@
import { useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useAppLocation } from 'lib/router/useAppLocation';
import { matchRoute } from 'lib/router/matchRoute';
import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
@@ -42,7 +41,7 @@ interface ShareURLModalProps {
function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
const urlQuery = useUrlQuery();
const location = useAppLocation();
const location = useLocation();
const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@@ -76,7 +75,7 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
const isRouteToBeSharedWithTime = useMemo(
() =>
routesToBeSharedWithTime.some((route) =>
matchRoute(location.pathname, route, { exact: true }),
matchPath(location.pathname, { path: route, exact: true }),
),
[location.pathname],
);

View File

@@ -1,5 +1,5 @@
// Mock dependencies before imports
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
@@ -9,8 +9,9 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import FeedbackModal from '../FeedbackModal';
jest.mock('lib/router/useAppLocation', () => ({
useAppLocation: jest.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
jest.mock('@signozhq/ui/sonner', () => ({
@@ -29,7 +30,7 @@ jest.mock('container/Integrations/utils', () => ({
handleContactSupport: jest.fn(),
}));
const mockUseAppLocation = useAppLocation as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const mockHandleContactSupport = handleContactSupport as jest.Mock;
const mockToast = toast as jest.Mocked<typeof toast>;
@@ -44,7 +45,7 @@ describe('FeedbackModal', () => {
beforeEach(() => {
jest.clearAllMocks();
logEventMock.mockReturnValue(Promise.resolve() as never);
mockUseAppLocation.mockReturnValue(mockLocation);
mockUseLocation.mockReturnValue(mockLocation);
mockUseGetTenantLicense.mockReturnValue({
isCloudUser: false,
});

View File

@@ -1,5 +1,5 @@
// Mock dependencies before imports
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
@@ -7,8 +7,9 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import HeaderRightSection from '../HeaderRightSection';
jest.mock('lib/router/useAppLocation', () => ({
useAppLocation: jest.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
jest.mock('../FeedbackModal', () => ({
@@ -44,7 +45,7 @@ jest.mock('hooks/useIsAIAssistantEnabled', () => ({
useIsAIAssistantEnabled: (): boolean => false,
}));
const mockUseAppLocation = useAppLocation as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const defaultProps = {
@@ -60,7 +61,7 @@ const mockLocation = {
describe('HeaderRightSection', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseAppLocation.mockReturnValue(mockLocation);
mockUseLocation.mockReturnValue(mockLocation);
// Default to licensed user (Enterprise or Cloud)
mockUseGetTenantLicense.mockReturnValue({
isCloudUser: true,

View File

@@ -1,8 +1,7 @@
// Mock dependencies before imports
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useAppLocation } from 'lib/router/useAppLocation';
import { matchRoute } from 'lib/router/matchRoute';
import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
@@ -13,12 +12,10 @@ import GetMinMax from 'lib/getMinMax';
import ShareURLModal from '../ShareURLModal';
jest.mock('lib/router/useAppLocation', () => ({
useAppLocation: jest.fn(),
}));
jest.mock('lib/router/matchRoute', () => ({
matchRoute: jest.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
matchPath: jest.fn(),
}));
jest.mock('hooks/useUrlQuery', () => ({
@@ -51,12 +48,12 @@ Object.defineProperty(window, 'location', {
writable: true,
});
const mockUseAppLocation = useAppLocation as jest.Mock;
const mockUseLocation = useLocation 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 mockMatchRoute = matchRoute as jest.Mock;
const mockMatchPath = matchPath as jest.Mock;
const mockUrlQuery = {
get: jest.fn(),
@@ -74,7 +71,7 @@ describe('ShareURLModal', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseAppLocation.mockReturnValue({
mockUseLocation.mockReturnValue({
pathname: TEST_PATH,
});
@@ -91,7 +88,7 @@ describe('ShareURLModal', () => {
mockUseCopyToClipboard.mockReturnValue([null, mockHandleCopyToClipboard]);
mockMatchRoute.mockReturnValue(null);
mockMatchPath.mockReturnValue(false);
// Reset URL query mocks - all return null by default
mockUrlQuery.get.mockReturnValue(null);
@@ -129,7 +126,7 @@ describe('ShareURLModal', () => {
});
it('should show absolute time toggle when on time-enabled route', () => {
mockMatchRoute.mockReturnValue({}); // Simulate being on a route that supports time
mockMatchPath.mockReturnValue(true); // Simulate being on a route that supports time
render(<ShareURLModal />);
@@ -149,7 +146,7 @@ describe('ShareURLModal', () => {
it('should toggle absolute time switch', async () => {
const user = userEvent.setup();
mockMatchRoute.mockReturnValue({});
mockMatchPath.mockReturnValue(true);
mockUseSelector.mockReturnValue({
selectedTime: '5min', // Non-custom time should enable absolute time by default
});
@@ -172,7 +169,7 @@ describe('ShareURLModal', () => {
// Invalid - missing start and end time for custom
mockUrlQuery.get.mockReturnValue(null);
mockMatchRoute.mockReturnValue({});
mockMatchPath.mockReturnValue(true);
render(<ShareURLModal />);
@@ -184,7 +181,7 @@ describe('ShareURLModal', () => {
it('should process URL with absolute time for non-custom time', async () => {
const user = userEvent.setup();
mockMatchRoute.mockReturnValue({});
mockMatchPath.mockReturnValue(true);
mockUseSelector.mockReturnValue({
selectedTime: '5min',
});
@@ -203,7 +200,7 @@ describe('ShareURLModal', () => {
it('should process URL with custom time parameters', async () => {
const user = userEvent.setup();
mockMatchRoute.mockReturnValue({});
mockMatchPath.mockReturnValue(true);
mockUseSelector.mockReturnValue({
selectedTime: 'custom',
});
@@ -231,7 +228,7 @@ describe('ShareURLModal', () => {
it('should process URL with relative time when absolute time is disabled', async () => {
const user = userEvent.setup();
mockMatchRoute.mockReturnValue({});
mockMatchPath.mockReturnValue(true);
mockUseSelector.mockReturnValue({
selectedTime: '5min',
});
@@ -252,12 +249,12 @@ describe('ShareURLModal', () => {
it('should handle routes that should be shared with time', async () => {
const user = userEvent.setup();
mockUseAppLocation.mockReturnValue({
mockUseLocation.mockReturnValue({
pathname: ROUTES.LOGS_EXPLORER,
});
mockMatchRoute.mockImplementation((pathname: string, route: string) =>
route === ROUTES.LOGS_EXPLORER ? {} : null,
mockMatchPath.mockImplementation(
(pathname: string, options: any) => options.path === ROUTES.LOGS_EXPLORER,
);
render(<ShareURLModal />);

View File

@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
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 } = useAppLocation();
const { pathname } = useLocation();
const isChatSupportEnabled = useMemo(() => {
if (!isFetchingFeatureFlags && (featureFlags || featureFlagsFetchError)) {

View File

@@ -1,4 +1,4 @@
import { AppLink } from 'lib/router/AppLink';
import { Link } from 'react-router-dom';
import styles from './LogHighlights.module.scss';
@@ -8,7 +8,7 @@ interface TraceIdFieldProps {
function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
return (
<AppLink
<Link
to={{ pathname: `/trace/${traceId}` }}
target="_blank"
rel="noreferrer"
@@ -16,7 +16,7 @@ function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
title={traceId}
>
{traceId}
</AppLink>
</Link>
);
}

View File

@@ -4,7 +4,6 @@ import { Button } from '@signozhq/ui/button';
import { Drawer, Tooltip } from 'antd';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
@@ -148,7 +147,7 @@ function LogDetailInner({
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -1,5 +1,6 @@
import { Typography } from '@signozhq/ui/typography';
import { ReactNode, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
OctagonAlert,
Bolt,
@@ -13,7 +14,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 { navigate } from 'lib/router/navigation';
import { History } from 'history';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import {
KAFKA_SETUP_DOC_LINK,
@@ -44,6 +45,7 @@ export enum AttributesFilters {
function ErrorTitleAndKey({
title,
parentTitle,
history,
isCloudUserVal,
errorMsg,
isLeaf,
@@ -51,6 +53,7 @@ function ErrorTitleAndKey({
title: string;
parentTitle: string;
isCloudUserVal: boolean;
history: History<unknown>;
errorMsg?: string;
isLeaf?: boolean;
}): TreeDataNode {
@@ -72,7 +75,7 @@ function ErrorTitleAndKey({
}
if (isCloudUserVal && !!link) {
navigate(link);
history.push(link);
} else {
openInNewTab(KAFKA_SETUP_DOC_LINK);
}
@@ -146,6 +149,7 @@ function generateTreeDataNodes(
response: OnboardingStatusResponse['data'],
parentTitle: string,
isCloudUserVal: boolean,
history: History<unknown>,
): TreeDataNode[] {
return response
.map((item) => {
@@ -158,6 +162,7 @@ function generateTreeDataNodes(
title: item.attribute,
errorMsg: item.error_message || '',
parentTitle,
history,
isCloudUserVal,
});
}
@@ -180,6 +185,7 @@ function AttributeCheckList({
setFilter(value);
};
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const history = useHistory();
useEffect(() => {
const filteredData = onboardingStatusResponses.map((response) => {
@@ -189,6 +195,7 @@ function AttributeCheckList({
errorMsg: response.errorMsg,
isLeaf: true,
parentTitle: response.title,
history,
isCloudUserVal,
});
}
@@ -206,6 +213,7 @@ function AttributeCheckList({
filteredData,
response.title,
isCloudUserVal,
history,
),
};
});

View File

@@ -18,7 +18,7 @@ import {
RefreshCw,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, type RefSelectProps, Select } from 'antd';
import { Button, Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Tooltip } from '@signozhq/ui/tooltip';
import cx from 'classnames';
@@ -26,6 +26,7 @@ import TextToolTip from 'components/TextToolTip/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { capitalize, isEmpty } from 'lodash-es';
import type { BaseSelectRef } from 'rc-select';
import { popupContainer } from 'utils/selectPopupContainer';
import TruncatedTooltip from './TruncatedTooltip';
@@ -83,7 +84,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// ===== State & Refs =====
const [isOpen, setIsOpen] = useState(false);
const [searchText, setSearchText] = useState('');
const selectRef = useRef<RefSelectProps>(null);
const selectRef = useRef<BaseSelectRef>(null);
const [activeIndex, setActiveIndex] = useState<number>(-1);
const [activeChipIndex, setActiveChipIndex] = useState<number>(-1); // For tracking active chip/tag
const [selectionStart, setSelectionStart] = useState<number>(-1);

View File

@@ -16,12 +16,13 @@ import {
X,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { type RefSelectProps, Select } from 'antd';
import { Select } from 'antd';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { capitalize, isEmpty } from 'lodash-es';
import type { BaseSelectRef } from 'rc-select';
import { popupContainer } from 'utils/selectPopupContainer';
import TruncatedTooltip from './TruncatedTooltip';
@@ -74,7 +75,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const isDarkMode = useIsDarkMode();
// Refs for element access and scroll behavior
const selectRef = useRef<RefSelectProps>(null);
const selectRef = useRef<BaseSelectRef>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
// Flag to track if dropdown just opened

View File

@@ -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(
<TestRouter>
<MemoryRouter>
<Provider store={store}>
<NotFound />
</Provider>
</TestRouter>,
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});

View File

@@ -1,7 +1,7 @@
import { AppLink } from 'lib/router/AppLink';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
export const Button = styled(AppLink)`
export const Button = styled(Link)`
border: 2px solid #2f80ed;
box-sizing: border-box;
border-radius: 10px;

View File

@@ -1,169 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, waitFor } from 'storybook/test';
import { withCanvas } from '@/storybook/decorators/withCanvas';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2 } from '../QueryBuilderV2';
import {
addOnToggles,
appendToAggregation,
DEFAULT_ARGS,
expectFirstQuery,
PANEL_EDITOR_ARGS,
PANEL_EDITOR_PATH,
payloadIn,
routeWith,
selectMetric,
} from './QueryBuilderV2.stories.mocks';
import QueryProbe from './QueryProbe';
const METRICS_EXPLORER_ARGS = {
config: { initialDataSource: DataSource.METRICS, queryVariant: 'static' },
showFunctions: false,
} as const;
const metricsExplorerRoute = routeWith(
ROUTES.METRICS_EXPLORER_EXPLORER,
initialQueriesMap.metrics,
);
/**
* Builder rules that hold without a backend: which add-ons a panel kind
* offers, the aggregations a metric type defaults to, and input limits. The
* probe below the builder prints the payload each play asserts on.
*/
const meta = {
title: 'Components/Query Builder/Behaviour',
component: QueryBuilderV2,
tags: ['play'],
decorators: [withCanvas({ maxWidth: 1100 })],
args: DEFAULT_ARGS,
render: (args): JSX.Element => (
<>
<QueryBuilderV2 {...args} />
<QueryProbe />
</>
),
} satisfies Meta<typeof QueryBuilderV2>;
export default meta;
type Story = StoryObj<typeof meta>;
/** A Value panel offers only the legend add-on. */
export const LogsOnValuePanel: Story = {
args: { ...PANEL_EDITOR_ARGS, panelType: PANEL_TYPES.VALUE },
parameters: {
signoz: { route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.logs) },
},
play: async ({ canvasElement }) => {
await waitFor(() =>
expect(addOnToggles(canvasElement)).toStrictEqual(['legend_format']),
);
},
};
/** Metrics on a Table: reduce-to is offered; group by lives in the aggregation row. */
export const MetricsOnTablePanel: Story = {
args: { ...PANEL_EDITOR_ARGS, panelType: PANEL_TYPES.TABLE },
parameters: {
signoz: { route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.metrics) },
},
play: async ({ canvasElement }) => {
await waitFor(() =>
expect(addOnToggles(canvasElement)).toStrictEqual([
'having',
'order_by',
'limit',
'legend_format',
'reduce_to',
]),
);
},
};
/** Traces on a time series: every add-on, with group by as a toggle. */
export const TracesOnTimeSeriesPanel: Story = {
args: PANEL_EDITOR_ARGS,
parameters: {
signoz: { route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.traces) },
},
play: async ({ canvasElement }) => {
await waitFor(() =>
expect(addOnToggles(canvasElement)).toStrictEqual([
'group_by',
'having',
'order_by',
'limit',
'legend_format',
]),
);
},
};
/** Picking a gauge defaults to avg over time and avg across series. */
export const GaugeMetricDefaults: Story = {
args: METRICS_EXPLORER_ARGS,
parameters: { signoz: { route: metricsExplorerRoute } },
play: async ({ canvasElement }) => {
await selectMetric(canvasElement, 'container_cpu_utilization');
await expectFirstQuery(canvasElement, {
aggregations: [
expect.objectContaining({
metricName: 'container_cpu_utilization',
timeAggregation: 'avg',
spaceAggregation: 'avg',
}),
],
});
},
};
/** Picking a histogram defaults to the p90 across series. */
export const HistogramMetricDefaults: Story = {
args: METRICS_EXPLORER_ARGS,
parameters: { signoz: { route: metricsExplorerRoute } },
play: async ({ canvasElement }) => {
await selectMetric(canvasElement, 'signoz_latency_bucket');
await expectFirstQuery(canvasElement, {
aggregations: [
expect.objectContaining({
metricName: 'signoz_latency_bucket',
spaceAggregation: 'p90',
}),
],
});
},
};
/** A Value panel takes a single aggregation: a second one is not accepted. */
export const ValuePanelTakesOneAggregation: Story = {
args: { ...PANEL_EDITOR_ARGS, panelType: PANEL_TYPES.VALUE },
parameters: {
signoz: { route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.logs) },
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, {
aggregations: [{ expression: 'count()' }],
});
const { before, after } = appendToAggregation(
canvasElement,
', count_distinct(service.name)',
);
await expect(after).toBe(before);
await expectFirstQuery(canvasElement, {
aggregations: [{ expression: 'count()' }],
});
await expect(
payloadIn(canvasElement).queries[0].spec.aggregations,
).toHaveLength(1);
},
};

View File

@@ -1,134 +0,0 @@
/**
* AI-owned. Generated and maintained by the `signoz-page-story` skill.
* Do not hand-edit: regenerate instead.
*/
import { EditorView } from '@codemirror/view';
import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import type { QueryBuilderV2 } from '../QueryBuilderV2';
type QueryBuilderProps = Parameters<typeof QueryBuilderV2>[0];
/** The builder's provider reads its starting query from the URL, as every host seeds it. */
export const routeWith = (path: string, query: Query): string =>
`${path}?compositeQuery=${encodeURIComponent(JSON.stringify(query))}`;
export const meterQuery: Query = {
...initialQueriesMap.metrics,
builder: {
...initialQueriesMap.metrics.builder,
queryData: [
{ ...initialQueriesMap.metrics.builder.queryData[0], source: 'meter' },
],
},
};
/** `PanelEditorQueryBuilder`'s props, less the per-kind field config. */
export const PANEL_EDITOR_ARGS: Partial<QueryBuilderProps> = {
allowedDataSources: [
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
TelemetrytypesSignalDTO.metrics,
],
showTraceOperator: true,
signalSourceChangeEnabled: true,
savePreviousQuery: true,
};
export const PANEL_EDITOR_PATH = '/dashboard/story/panel/story';
interface ProbedQuery {
type: string;
spec: {
aggregations?: Record<string, unknown>[];
[key: string]: unknown;
};
}
/** The `compositeQuery` the story's `QueryProbe` prints for the builder's state. */
export const payloadIn = (
canvasElement: HTMLElement,
): { queries: ProbedQuery[] } =>
JSON.parse(
within(canvasElement).getByTestId('qb-payload').textContent ?? '{}',
) as { queries: ProbedQuery[] };
export const expectFirstQuery = async (
canvasElement: HTMLElement,
spec: Record<string, unknown>,
): Promise<void> => {
await waitFor(() =>
expect(payloadIn(canvasElement).queries[0]).toMatchObject({
type: 'builder_query',
spec: expect.objectContaining(spec),
}),
);
};
/** The add-on toggles query `A` offers, in order (`group_by`, `having`, …). */
export const addOnToggles = (canvasElement: HTMLElement): string[] =>
within(within(canvasElement).getByTestId('query-A'))
.queryAllByTestId(/^query-add-on-/)
.map((toggle) =>
(toggle.getAttribute('data-testid') ?? '').replace('query-add-on-', ''),
);
/** Picks `metricName` in query `A`'s metric selector; its list portals out. */
export const selectMetric = async (
canvasElement: HTMLElement,
metricName: string,
): Promise<void> => {
const field = within(canvasElement).getByTestId('metric-name-selector-0');
const input = within(field).getByRole('combobox');
await userEvent.click(input);
await userEvent.type(input, metricName);
const option = await waitFor(async () => {
const match = Array.from(
document.querySelectorAll<HTMLElement>(
'.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option',
),
).find((candidate) => candidate.textContent?.includes(metricName));
await expect(match).toBeDefined();
return match as HTMLElement;
});
await userEvent.click(option);
await waitFor(() => expect(screen.queryByRole('listbox')).toBeNull());
};
/**
* Appends `text` to query `A`'s aggregation editor and returns its document
* before and after. Dispatched as a CodeMirror transaction: `userEvent.type`
* strands the caret when the line redraws, and a transaction runs the editor's
* own filters, which is what an input limit is.
*/
export const appendToAggregation = (
canvasElement: HTMLElement,
text: string,
): { before: string; after: string } => {
const content = within(canvasElement)
.getByTestId('query-aggregation-editor')
.querySelector<HTMLElement>('.cm-content');
const view = content ? EditorView.findFromDOM(content) : null;
if (!view) {
throw new Error('aggregation editor not mounted');
}
const before = view.state.doc.toString();
view.dispatch({
changes: { from: before.length, insert: text },
selection: { anchor: before.length + text.length },
userEvent: 'input.type',
});
// Read synchronously: the document is the editor's own verdict, before
// React state catches up.
return { before, after: view.state.doc.toString() };
};
export const DEFAULT_ARGS: Partial<QueryBuilderProps> = {
panelType: PANEL_TYPES.TIME_SERIES,
version: 'v3',
};

View File

@@ -1,145 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, within } from 'storybook/test';
import { withCanvas } from '@/storybook/decorators/withCanvas';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2 } from '../QueryBuilderV2';
import {
DEFAULT_ARGS,
expectFirstQuery,
meterQuery,
PANEL_EDITOR_ARGS,
PANEL_EDITOR_PATH,
routeWith,
} from './QueryBuilderV2.stories.mocks';
import QueryProbe from './QueryProbe';
/**
* The query builder as each host mounts it: the props come from that host's
* call site, the starting query from its route. The probe below the builder
* prints the `query_range` payload its state would send.
*/
const meta = {
title: 'Components/Query Builder',
component: QueryBuilderV2,
tags: ['play'],
decorators: [withCanvas({ maxWidth: 1100 })],
args: DEFAULT_ARGS,
render: (args): JSX.Element => (
<>
<QueryBuilderV2 {...args} />
<QueryProbe />
</>
),
} satisfies Meta<typeof QueryBuilderV2>;
export default meta;
type Story = StoryObj<typeof meta>;
/** Logs explorer, graph view: a fixed logs signal with `count()`. */
export const LogsExplorer: Story = {
args: {
config: { initialDataSource: DataSource.LOGS, queryVariant: 'static' },
},
parameters: {
signoz: { route: routeWith(ROUTES.LOGS_EXPLORER, initialQueriesMap.logs) },
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, {
signal: 'logs',
aggregations: [{ expression: 'count()' }],
});
},
};
/** Traces explorer: a fixed traces signal, with trace matching offered. */
export const TracesExplorer: Story = {
args: {
config: { initialDataSource: DataSource.TRACES, queryVariant: 'static' },
showTraceOperator: true,
},
parameters: {
signoz: {
route: routeWith(ROUTES.TRACES_EXPLORER, initialQueriesMap.traces),
},
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'traces' });
},
};
/** Metrics explorer: a fixed metrics signal, functions hidden. */
export const MetricsExplorer: Story = {
args: {
config: { initialDataSource: DataSource.METRICS, queryVariant: 'static' },
showFunctions: false,
},
parameters: {
signoz: {
route: routeWith(
ROUTES.METRICS_EXPLORER_EXPLORER,
initialQueriesMap.metrics,
),
},
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'metrics' });
},
};
/** Meter explorer: metrics read from the meter source. */
export const MeterExplorer: Story = {
args: {
config: {
initialDataSource: DataSource.METRICS,
queryVariant: 'static',
signalSource: 'meter',
},
showFunctions: false,
},
parameters: {
signoz: { route: routeWith(ROUTES.METER_EXPLORER, meterQuery) },
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'metrics', source: 'meter' });
},
};
/**
* V2 panel editor: a data-source dropdown per query, several queries,
* formulas and trace matching.
*/
export const PanelEditor: Story = {
args: PANEL_EDITOR_ARGS,
parameters: {
signoz: {
route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.logs),
},
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'logs' });
await expect(
within(canvasElement).getByTestId('query-data-source-selector-0'),
).toBeVisible();
},
};
/** Logs alert: a fixed logs signal with functions, as the alert form mounts it. */
export const LogsAlert: Story = {
args: {
config: { initialDataSource: DataSource.LOGS, queryVariant: 'static' },
showFunctions: true,
signalSourceChangeEnabled: true,
},
parameters: {
signoz: { route: routeWith(ROUTES.ALERTS_NEW, initialQueriesMap.logs) },
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'logs' });
},
};

View File

@@ -1,11 +0,0 @@
.probe {
margin-top: 16px;
padding: 12px;
max-height: 320px;
overflow: auto;
border: 1px solid var(--l1-border);
border-radius: 4px;
font-size: 12px;
color: var(--l2-foreground);
background: var(--l1-background);
}

View File

@@ -1,38 +0,0 @@
import { useMemo } from 'react';
import { prepareQueryRangePayloadV5 } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import styles from './QueryProbe.module.scss';
/** Fixed, so the printed payload is the same on every render and capture. */
const WINDOW = { start: 1_700_000_000_000, end: 1_700_003_600_000 };
/**
* Prints the `compositeQuery` the builder's live state would send, so a play
* function asserts on the query rather than on the DOM that edits it.
*/
function QueryProbe(): JSX.Element {
const { currentQuery, panelType } = useQueryBuilder();
const payload = useMemo(() => {
try {
return prepareQueryRangePayloadV5({
query: currentQuery,
graphType: panelType ?? PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
...WINDOW,
}).queryPayload.compositeQuery;
} catch (error) {
return { error: String(error) };
}
}, [currentQuery, panelType]);
return (
<pre className={styles.probe} data-testid="qb-payload">
{JSON.stringify(payload, null, 2)}
</pre>
);
}
export default QueryProbe;

View File

@@ -1,4 +1,5 @@
import { navigate } from 'lib/router/navigation';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { fireEvent, render, screen } from 'tests/test-utils';
import RouteTab from './index';
@@ -9,11 +10,6 @@ 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>;
}
@@ -37,39 +33,63 @@ const testRoutes: RouteTabProps['routes'] = [
];
describe('RouteTab component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders correctly', () => {
render(<RouteTab routes={testRoutes} activeKey="Tab1" />);
const history = createMemoryHistory();
render(
<Router history={history}>
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
</Router>,
);
expect(screen.getByRole('tab', { name: 'Tab1' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Tab2' })).toBeInTheDocument();
});
it('renders correct number of tabs', () => {
render(<RouteTab routes={testRoutes} activeKey="Tab1" />);
const history = createMemoryHistory();
render(
<Router history={history}>
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
</Router>,
);
const tabs = screen.getAllByRole('tab');
expect(tabs).toHaveLength(testRoutes.length);
});
it('sets provided activeKey as active tab', () => {
render(<RouteTab routes={testRoutes} activeKey="Tab2" />);
const history = createMemoryHistory();
render(
<Router history={history}>
<RouteTab history={history} routes={testRoutes} activeKey="Tab2" />
</Router>,
);
expect(
screen.getByRole('tab', { name: 'Tab2', selected: true }),
).toBeInTheDocument();
});
it('navigates to correct route on tab click', () => {
render(<RouteTab routes={testRoutes} activeKey="Tab1" />);
expect(navigate).not.toHaveBeenCalled();
const history = createMemoryHistory();
render(
<Router history={history}>
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
</Router>,
);
expect(history.location.pathname).toBe('/');
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
expect(navigate).toHaveBeenCalledWith('/tab2');
expect(history.location.pathname).toBe('/tab2');
});
it('applies the layout class alongside a custom className', () => {
const history = createMemoryHistory();
const { container } = render(
<RouteTab routes={testRoutes} activeKey="Tab1" className="custom-tabs" />,
<Router history={history}>
<RouteTab
history={history}
routes={testRoutes}
activeKey="Tab1"
className="custom-tabs"
/>
</Router>,
);
expect(container.querySelector('.ant-tabs')).toHaveClass(
'routeTab',
@@ -78,8 +98,11 @@ describe('RouteTab component', () => {
});
it('renders the active tab content inside an overlay scrollbar', () => {
const history = createMemoryHistory();
const { container } = render(
<RouteTab routes={testRoutes} activeKey="Tab1" />,
<Router history={history}>
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
</Router>,
);
expect(
container.querySelector('.ant-tabs-tabpane-active > .overlay-scrollbar'),
@@ -88,12 +111,16 @@ describe('RouteTab component', () => {
it('calls onChangeHandler on tab change', () => {
const onChangeHandler = jest.fn();
const history = createMemoryHistory();
render(
<RouteTab
routes={testRoutes}
activeKey="Tab1"
onChangeHandler={onChangeHandler}
/>,
<Router history={history}>
<RouteTab
routes={testRoutes}
activeKey="Tab1"
onChangeHandler={onChangeHandler}
history={history}
/>
</Router>,
);
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
expect(onChangeHandler).toHaveBeenCalled();

View File

@@ -1,12 +1,13 @@
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';
@@ -27,17 +28,21 @@ function RouteTab({
routes,
activeKey,
onChangeHandler,
history,
showRightSection,
className,
...rest
}: RouteTabProps & TabsProps): JSX.Element {
const params = useAppParams<Params>();
const location = useAppLocation();
const params = useParams<Params>();
const location = useLocation();
// Find the matching route for the current pathname
const currentRoute = routes.find((route) => {
const routePath = route.route.split('?')[0];
return matchRoute(location.pathname, routePath, { exact: true });
return matchPath(location.pathname, {
path: routePath,
exact: true,
});
});
const onChange = (activeRoute: string): void => {
@@ -48,7 +53,8 @@ function RouteTab({
const selectedRoute = routes.find((e) => e.key === activeRoute);
if (selectedRoute) {
navigate(buildRoutePath(selectedRoute.route, params));
const resolvedRoute = generatePath(selectedRoute.route, params);
history.push(resolvedRoute);
}
};

View File

@@ -1,5 +1,6 @@
import { ComponentType } from 'react';
import { TabsProps } from 'antd';
import { History } from 'history';
export type TabRoutes = {
name: React.ReactNode;
@@ -12,5 +13,6 @@ export interface RouteTabProps {
routes: TabRoutes[];
activeKey: TabsProps['activeKey'];
onChangeHandler?: (key: string) => void;
history: History<unknown>;
showRightSection: boolean;
}

View File

@@ -3,7 +3,7 @@
*/
// ---- Mocks (must run BEFORE importing the component) ----
import ROUTES from 'constants/routes';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { render, screen, userEvent } from 'tests/test-utils';
import '@testing-library/jest-dom/extend-expect';
@@ -24,12 +24,61 @@ afterAll(() => {
delete (HTMLElement.prototype as any).scrollIntoView;
});
jest.mock('lib/router/navigation', () => ({
...jest.requireActual('lib/router/navigation'),
navigate: jest.fn(),
}));
// mock history.push / replace / go / location
jest.mock('lib/history', () => {
const location = { pathname: '/', search: '', hash: '' };
const mockNavigate = navigate as jest.MockedFunction<typeof navigate>;
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,
};
});
// Mock ResizeObserver for Jest/jsdom
class ResizeObserver {
@@ -110,14 +159,14 @@ describe('CmdKPalette', () => {
expect(screen.getByText('Switch to Dark Mode')).toBeInTheDocument();
});
it('clicking a navigation item navigates to the correct route', async () => {
it('clicking a navigation item calls history.push with correct route', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CmdKPalette userRole="ADMIN" />);
const homeItem = screen.getByText(HOME_LABEL);
await user.click(homeItem);
expect(mockNavigate).toHaveBeenCalledWith(ROUTES.HOME);
expect(history.push).toHaveBeenCalledWith(ROUTES.HOME);
});
it('role-based filtering (basic smoke)', () => {

View File

@@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import cx from 'classnames';
import { useLocation } from 'react-router-dom';
import {
CommandDialog,
CommandEmpty,
@@ -22,8 +23,7 @@ import {
import { useThemeMode } from 'hooks/useDarkMode';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { IS_DEV } from 'lib/env';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import history from 'lib/history';
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 = useAppLocation();
const location = useLocation();
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const startNewConversation = useAIAssistantStore(
(s) => s.startNewConversation,
@@ -120,7 +120,7 @@ export function CmdKPalette({
}
function onClickHandler(key: string): void {
navigate(key);
history.push(key);
}
const handleOpenAIAssistant = (): void => {

View File

@@ -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,6 +12,8 @@ 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,
@@ -26,10 +28,10 @@ export default function AIAssistantDrawer(): JSX.Element {
return;
}
closeDrawer();
navigate(
history.push(
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
);
}, [activeConversationId, closeDrawer]);
}, [activeConversationId, closeDrawer, history]);
const handleNewConversation = useCallback(() => {
startNewConversation();

View File

@@ -1,8 +1,7 @@
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';
@@ -32,7 +31,8 @@ import styles from './AIAssistantModal.module.scss';
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function AIAssistantModal(): JSX.Element | null {
const { pathname } = useAppLocation();
const history = useHistory();
const { pathname } = useLocation();
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.
navigate(
history.push(
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
{ state: { fromInApp: true } },
{ fromInApp: true },
);
}, [activeConversationId, closeModal]);
}, [activeConversationId, closeModal, history]);
const handleNew = useCallback(() => {
void logEvent(AIAssistantEvents.NewChatClicked, {

View File

@@ -1,8 +1,6 @@
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';
@@ -23,11 +21,13 @@ 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 { pathname } = useAppLocation();
const history = useHistory();
const { pathname } = useLocation();
const [showHistory, setShowHistory] = useState(false);
const isOpen = useAIAssistantStore((s) => s.isDrawerOpen);
const isFullScreenPage = !!matchRoute(pathname, ROUTES.AI_ASSISTANT, {
const isFullScreenPage = !!matchPath(pathname, {
path: 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.
navigate(
history.push(
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
{ state: { fromInApp: true } },
{ fromInApp: true },
);
}, [activeConversationId, closeDrawer]);
}, [activeConversationId, closeDrawer, history]);
const handleNew = useCallback(() => {
void logEvent(AIAssistantEvents.NewChatClicked, {

View File

@@ -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 = useAppLocation();
const location = useLocation();
const conversation = useAIAssistantStore(
(s) => s.conversations[conversationId],

View File

@@ -1,8 +1,9 @@
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';
@@ -86,11 +87,11 @@ function renderView(variant: 'panel' | 'page' | 'modal'): {
getByTestId: (id: string) => HTMLElement;
} {
return render(
<TestRouter initialRoute="/dashboard/dashboard-123">
<MemoryRouter initialEntries={['/dashboard/dashboard-123']}>
<VariantContext.Provider value={variant}>
<ConversationView conversationId={CONVERSATION_ID} />
</VariantContext.Provider>
</TestRouter>,
</MemoryRouter>,
);
}

View File

@@ -1,8 +1,6 @@
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';
@@ -215,11 +213,20 @@ function signalMatchesPathname(
): boolean {
switch (signal) {
case ApplyFilterSignalDTO.logs:
return Boolean(matchRoute(pathname, ROUTES.LOGS_EXPLORER));
return Boolean(
matchPath(pathname, { path: ROUTES.LOGS_EXPLORER, exact: false }),
);
case ApplyFilterSignalDTO.traces:
return Boolean(matchRoute(pathname, ROUTES.TRACES_EXPLORER));
return Boolean(
matchPath(pathname, { path: ROUTES.TRACES_EXPLORER, exact: false }),
);
case ApplyFilterSignalDTO.metrics:
return Boolean(matchRoute(pathname, ROUTES.METRICS_EXPLORER_EXPLORER));
return Boolean(
matchPath(pathname, {
path: ROUTES.METRICS_EXPLORER_EXPLORER,
exact: false,
}),
);
default:
return false;
}
@@ -268,6 +275,7 @@ function explorerRouteForSignal(signal: ApplyFilterSignalDTO): string | null {
}
interface ApplyFilterDeps {
history: ReturnType<typeof useHistory>;
pathname: string;
redirectWithQueryBuilderData: ReturnType<
typeof useQueryBuilder
@@ -309,8 +317,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: `navigate()` so the landing explorer initializes from the
* new URL on mount.
* - Off-page: use `history.push` so the landing explorer initializes from
* the new URL on mount.
*/
function applyFilter(action: MessageActionDTO, deps: ApplyFilterDeps): void {
// eslint-disable-next-line no-console
@@ -352,9 +360,9 @@ function applyFilter(action: MessageActionDTO, deps: ApplyFilterDeps): void {
return;
}
// eslint-disable-next-line no-console
console.log('[apply_filter] off-page → navigate', base);
console.log('[apply_filter] off-page → history.push', base);
const encoded = encodeURIComponent(JSON.stringify(normalized));
navigate(`${base}?${QueryParams.compositeQuery}=${encoded}`);
deps.history.push(`${base}?${QueryParams.compositeQuery}=${encoded}`);
}
/** Picks the right rollback API call for a given action kind. */
@@ -383,7 +391,8 @@ export default function ActionsSection({
actions,
messageId,
}: ActionsSectionProps): JSX.Element | null {
const { pathname } = useAppLocation();
const history = useHistory();
const { pathname } = useLocation();
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
const { redirectWithQueryBuilderData, handleSetQueryData } = useQueryBuilder();
@@ -415,12 +424,19 @@ export default function ActionsSection({
}
autoAppliedFilterKeys.add(key);
applyFilter(action, {
history,
pathname,
redirectWithQueryBuilderData,
handleSetQueryData,
});
});
}, [actions, pathname, redirectWithQueryBuilderData, handleSetQueryData]);
}, [
actions,
pathname,
history,
redirectWithQueryBuilderData,
handleSetQueryData,
]);
if (actions.length === 0) {
return null;
@@ -440,7 +456,11 @@ export default function ActionsSection({
}
setResult(key, { state: 'loading' });
try {
await openSavedViewByKey(resourceId, resolveSavedViewSourceHint(action));
await openSavedViewByKey(
resourceId,
resolveSavedViewSourceHint(action),
history,
);
void logEvent(AIAssistantEvents.ResourceOpened, {
threadId,
messageId,
@@ -495,7 +515,7 @@ export default function ActionsSection({
targetModule: targetModuleForResource(resourceType),
resourceId,
});
navigate(path);
history.push(path);
};
const handleClick = (key: string, action: MessageActionDTO): void => {
@@ -556,6 +576,7 @@ export default function ActionsSection({
});
}
applyFilter(action, {
history,
pathname,
redirectWithQueryBuilderData,
handleSetQueryData,

View File

@@ -16,16 +16,19 @@ 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,
findSavedViewInLists,
openSavedView,
openSavedViewByKey,
} from '../openSavedView';
import {
entityToSavedViewSource,
entityToDataSource,
isSavedViewOpenAction,
resolveActionEntity,
resolveOpenResourceType,
@@ -36,9 +39,6 @@ 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,7 +60,6 @@ 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,
@@ -129,14 +128,10 @@ describe('resolveOpenResource', () => {
).toBe('abc-123');
});
it('maps entity values to saved view sources', () => {
expect(entityToSavedViewSource('logs')).toBe(SavedviewtypesSourceDTO.logs);
expect(entityToSavedViewSource('logs_explorer')).toBe(
SavedviewtypesSourceDTO.logs,
);
expect(entityToSavedViewSource('traces')).toBe(
SavedviewtypesSourceDTO.traces,
);
it('maps entity values to explorer data sources', () => {
expect(entityToDataSource('logs')).toBe(DataSource.LOGS);
expect(entityToDataSource('logs_explorer')).toBe(DataSource.LOGS);
expect(entityToDataSource('traces')).toBe(DataSource.TRACES);
});
it('prefers entity over signal for saved-view source hints', () => {
@@ -147,7 +142,7 @@ describe('resolveOpenResource', () => {
entity: SavedViewEntityDTO.traces,
signal: ApplyFilterSignalDTO.logs,
}),
).toBe(SavedviewtypesSourceDTO.traces);
).toBe(DataSource.TRACES);
});
it('falls back to signal when entity is absent', () => {
@@ -157,7 +152,7 @@ describe('resolveOpenResource', () => {
label: 'Open view',
signal: ApplyFilterSignalDTO.metrics,
}),
).toBe(SavedviewtypesSourceDTO.metrics);
).toBe(DataSource.METRICS);
});
it('normalises saved-view resource types', () => {
@@ -210,10 +205,7 @@ describe('findSavedViewInLists', () => {
const tracesView = makeView('view-traces', SavedviewtypesSourceDTO.traces);
mockedListSavedViews.mockResolvedValueOnce(mockViewsResponse([tracesView]));
const result = await findSavedViewInLists(
'view-traces',
SavedviewtypesSourceDTO.traces,
);
const result = await findSavedViewInLists('view-traces', DataSource.TRACES);
expect(result).toStrictEqual(tracesView);
expect(mockedListSavedViews).toHaveBeenCalledTimes(1);
@@ -236,18 +228,33 @@ describe('findSavedViewInLists', () => {
});
});
describe('openSavedView', () => {
beforeEach(() => {
mockedNavigate.mockClear();
});
describe('buildExplorerNavigationUrl', () => {
it('encodes composite query and view selectors', () => {
const url = buildExplorerNavigationUrl(
ROUTES.LOGS_EXPLORER,
{ queryType: 'builder' } as never,
{
[QueryParams.panelTypes]: PANEL_TYPES.LIST,
[QueryParams.viewKey]: 'view-1',
},
);
it('navigates with the view query params', () => {
expect(url).toContain(ROUTES.LOGS_EXPLORER);
expect(url).toContain(`${QueryParams.compositeQuery}=`);
expect(url).toContain(`${QueryParams.viewKey}=`);
});
});
describe('openSavedView', () => {
it('navigates with history.push and view query params', () => {
const push = jest.fn();
const history = { push } as unknown as History;
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
openSavedView(view);
openSavedView(view, history);
expect(mockedNavigate).toHaveBeenCalledTimes(1);
const pushedUrl = mockedNavigate.mock.calls[0][0] as string;
expect(push).toHaveBeenCalledTimes(1);
const pushedUrl = push.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"');
@@ -259,14 +266,18 @@ describe('openSavedView', () => {
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
delete view.source;
expect(() => openSavedView(view)).toThrow('Unsupported saved view source');
expect(() =>
openSavedView(view, { push: jest.fn() } as unknown as History),
).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)).toThrow('Saved view is missing query data');
expect(() =>
openSavedView(view, { push: jest.fn() } as unknown as History),
).toThrow('Saved view is missing query data');
});
});
@@ -274,31 +285,34 @@ 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', SavedviewtypesSourceDTO.logs);
await openSavedViewByKey('view-logs', DataSource.LOGS, history);
expect(mockedGetSavedView).toHaveBeenCalledWith({ id: 'view-logs' });
expect(mockedListSavedViews).not.toHaveBeenCalled();
expect(mockedNavigate).toHaveBeenCalled();
expect(push).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', SavedviewtypesSourceDTO.traces);
await openSavedViewByKey('view-traces', DataSource.TRACES, history);
expect(mockedListSavedViews).toHaveBeenCalledWith({
source: SavedviewtypesSourceDTO.traces,
});
expect(mockedNavigate).toHaveBeenCalled();
expect(push).toHaveBeenCalled();
});
it('throws when the saved view does not exist', async () => {
@@ -306,7 +320,9 @@ describe('openSavedViewByKey', () => {
mockedListSavedViews.mockResolvedValue(mockViewsResponse([]));
await expect(
openSavedViewByKey('missing', SavedviewtypesSourceDTO.logs),
openSavedViewByKey('missing', DataSource.LOGS, {
push: jest.fn(),
} as unknown as History),
).rejects.toThrow('Saved view not found');
});
});

View File

@@ -2,34 +2,37 @@ import {
getSavedView,
listSavedViews,
} from 'api/generated/services/saved-view';
import {
SavedviewtypesSavedViewDTO,
SavedviewtypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { SavedviewtypesSavedViewDTO } from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { SavedViewSourcePage } from 'container/SavedViews/types';
import { findSavedView } from 'container/SavedViews/utils/findSavedView';
import { getSavedViewQuery } from 'container/SavedViews/utils/getSavedViewQuery';
import { toSavedViewSource } from 'container/SavedViews/utils/toSavedViewSource';
import { SOURCEPAGE_VS_ROUTES } from 'container/SavedViews/constants';
import { navigate } from 'lib/router/navigation';
import { DataSource } from 'types/common/queryBuilder';
import { buildExplorerNavigationUrl } from 'utils/buildExplorerNavigationUrl';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { History } from 'history';
const DEFAULT_PROBE_SOURCES: SavedviewtypesSourceDTO[] = [
SavedviewtypesSourceDTO.logs,
SavedviewtypesSourceDTO.traces,
SavedviewtypesSourceDTO.metrics,
type SavedViewSourceHint = SavedViewSourcePage;
const DEFAULT_PROBE_SOURCES: SavedViewSourceHint[] = [
DataSource.LOGS,
DataSource.TRACES,
DataSource.METRICS,
];
export async function findSavedViewInLists(
viewKey: string,
sourceHint?: SavedviewtypesSourceDTO | null,
sourceHint?: SavedViewSourceHint | null,
): Promise<SavedviewtypesSavedViewDTO | null> {
const sources = sourceHint ? [sourceHint] : DEFAULT_PROBE_SOURCES;
for (const source of sources) {
try {
const response = await listSavedViews({ source });
const response = await listSavedViews({
source: toSavedViewSource(source),
});
const match = findSavedView(response.data, viewKey);
if (match) {
return match;
@@ -44,7 +47,7 @@ export async function findSavedViewInLists(
async function loadSavedView(
viewKey: string,
sourceHint?: SavedviewtypesSourceDTO | null,
sourceHint?: SavedViewSourceHint | null,
): Promise<SavedviewtypesSavedViewDTO> {
try {
const response = await getSavedView({ id: viewKey });
@@ -69,7 +72,30 @@ export function explorerRouteForSourcePage(
return SOURCEPAGE_VS_ROUTES[sourcePage] ?? null;
}
export function openSavedView(view: SavedviewtypesSavedViewDTO): void {
/**
* Builds an explorer URL the same way `redirectWithQueryBuilderData` does —
* without inheriting stale query params from the current page's `urlQuery`.
*/
export function buildExplorerNavigationUrl(
route: string,
query: Query,
searchParams: Record<string, unknown>,
): string {
const params = new URLSearchParams();
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(query)),
);
Object.entries(searchParams).forEach(([key, value]) => {
params.set(key, JSON.stringify(value));
});
return `${route}?${params.toString()}`;
}
export function openSavedView(
view: SavedviewtypesSavedViewDTO,
history: History,
): void {
const route = view.source ? explorerRouteForSourcePage(view.source) : null;
if (!route) {
throw new Error('Unsupported saved view source');
@@ -84,13 +110,14 @@ export function openSavedView(view: SavedviewtypesSavedViewDTO): void {
[QueryParams.panelTypes]: view.spec.panelType as unknown as PANEL_TYPES,
[QueryParams.viewKey]: view.id,
});
navigate(url);
history.push(url);
}
export async function openSavedViewByKey(
viewKey: string,
sourceHint: SavedviewtypesSourceDTO | null | undefined,
sourceHint: SavedViewSourceHint | null | undefined,
history: History,
): Promise<void> {
const view = await loadSavedView(viewKey, sourceHint);
openSavedView(view);
openSavedView(view, history);
}

View File

@@ -3,7 +3,7 @@ import {
ApplyFilterSignalDTO,
SavedViewEntityDTO,
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { DataSource } from 'types/common/queryBuilder';
import { ResourceType } from './resourceRoute';
@@ -119,15 +119,15 @@ export function resolveActionEntity(
}
function normalizeToSavedViewEntity(value: string): SavedViewEntityDTO | null {
const source = entityToSavedViewSource(value);
const source = entityToDataSource(value);
switch (source) {
case SavedviewtypesSourceDTO.logs:
case DataSource.LOGS:
return SavedViewEntityDTO.logs;
case SavedviewtypesSourceDTO.traces:
case DataSource.TRACES:
return SavedViewEntityDTO.traces;
case SavedviewtypesSourceDTO.metrics:
case DataSource.METRICS:
return SavedViewEntityDTO.metrics;
case SavedviewtypesSourceDTO.meter:
case 'meter':
return SavedViewEntityDTO.meter;
default:
return null;
@@ -135,26 +135,26 @@ function normalizeToSavedViewEntity(value: string): SavedViewEntityDTO | null {
}
/**
* Maps an action `entity` to the saved views api source for saved-view lookups.
* Maps an action `entity` to an explorer `DataSource` for saved-view lookups.
* Accepts both short (`logs`) and taxonomy (`logs_explorer`) values.
*/
export function entityToSavedViewSource(
export function entityToDataSource(
entity: SavedViewEntityDTO | string,
): SavedviewtypesSourceDTO | null {
): DataSource | 'meter' | null {
const normalized = entity.trim().toLowerCase().replace(/-/g, '_');
switch (normalized) {
case SavedViewEntityDTO.logs:
case ResourceType.logs_explorer:
return SavedviewtypesSourceDTO.logs;
return DataSource.LOGS;
case SavedViewEntityDTO.traces:
case ResourceType.traces_explorer:
return SavedviewtypesSourceDTO.traces;
return DataSource.TRACES;
case SavedViewEntityDTO.metrics:
case ResourceType.metrics_explorer:
return SavedviewtypesSourceDTO.metrics;
return DataSource.METRICS;
case SavedViewEntityDTO.meter:
return SavedviewtypesSourceDTO.meter;
return 'meter';
default:
return null;
}
@@ -166,10 +166,10 @@ export function entityToSavedViewSource(
*/
export function resolveSavedViewSourceHint(
action: MessageActionDTO,
): SavedviewtypesSourceDTO | null {
): DataSource | 'meter' | null {
const entity = resolveActionEntity(action);
if (entity) {
const fromEntity = entityToSavedViewSource(entity);
const fromEntity = entityToDataSource(entity);
if (fromEntity) {
return fromEntity;
}
@@ -178,11 +178,11 @@ export function resolveSavedViewSourceHint(
if (action.signal) {
switch (action.signal) {
case ApplyFilterSignalDTO.logs:
return SavedviewtypesSourceDTO.logs;
return DataSource.LOGS;
case ApplyFilterSignalDTO.traces:
return SavedviewtypesSourceDTO.traces;
return DataSource.TRACES;
case ApplyFilterSignalDTO.metrics:
return SavedviewtypesSourceDTO.metrics;
return DataSource.METRICS;
default: {
const _exhaustive: never = action.signal;
return _exhaustive;

View File

@@ -298,8 +298,7 @@ export default function ChatInput({
(value: string, caret: number) => {
const beforeCaret = value.slice(0, caret);
const atIndex = beforeCaret.lastIndexOf('@');
// `@` only triggers at a word start, so words containing `@` stay plain text.
if (atIndex < 0 || /\S/.test(beforeCaret.charAt(atIndex - 1))) {
if (atIndex < 0) {
setIsContextPickerOpen(false);
setMentionRange(null);
return;

View File

@@ -1,30 +0,0 @@
import { render, screen, userEvent } from 'tests/test-utils';
import ChatInput from '../ChatInput';
function getComposer(): HTMLTextAreaElement {
return screen.getByPlaceholderText(/Ask anything/i) as HTMLTextAreaElement;
}
describe('ChatInput — @ mention trigger', () => {
it('opens the context picker when @ starts a word', async () => {
render(<ChatInput onSend={jest.fn()} />);
await userEvent.type(getComposer(), 'check @');
await expect(
screen.findByPlaceholderText(/search dashboards/i),
).resolves.toBeInTheDocument();
});
it('keeps typing an email without opening the context picker', async () => {
render(<ChatInput onSend={jest.fn()} />);
await userEvent.type(getComposer(), 'mail user@example.com');
expect(getComposer()).toHaveValue('mail user@example.com');
expect(
screen.queryByPlaceholderText(/search dashboards/i),
).not.toBeInTheDocument();
});
});

View File

@@ -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,10 +33,9 @@ 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 = matchRoute<'dashboardId' | 'panelId'>(
const panelEditorMatch = matchPath<{ dashboardId: string; panelId: string }>(
pathname,
ROUTES.DASHBOARD_PANEL_EDITOR,
{ exact: true },
{ path: ROUTES.DASHBOARD_PANEL_EDITOR, exact: true },
);
if (panelEditorMatch) {
const { dashboardId, panelId } = panelEditorMatch.params;
@@ -56,7 +55,8 @@ 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 = matchRoute<'dashboardId'>(pathname, ROUTES.DASHBOARD, {
const dashboardMatch = matchPath<{ dashboardId: string }>(pathname, {
path: ROUTES.DASHBOARD,
exact: true,
});
if (dashboardMatch) {
@@ -89,7 +89,7 @@ export function getAutoContexts(
}
// Dashboard list — `/dashboard`.
if (matchRoute(pathname, ROUTES.ALL_DASHBOARD, { exact: true })) {
if (matchPath(pathname, { path: 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 (
matchRoute(pathname, ROUTES.ALERT_OVERVIEW, { exact: true }) ||
matchRoute(pathname, ROUTES.ALERT_HISTORY, { exact: true })
matchPath(pathname, { path: ROUTES.ALERT_OVERVIEW, exact: true }) ||
matchPath(pathname, { path: 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 (matchRoute(pathname, ROUTES.EDIT_ALERTS, { exact: true })) {
if (matchPath(pathname, { path: 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 (matchRoute(pathname, ROUTES.ALERTS_NEW, { exact: true })) {
if (matchPath(pathname, { path: 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 (matchRoute(pathname, ROUTES.ALERT_HISTORY, { exact: true })) {
if (matchPath(pathname, { path: 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 (matchRoute(pathname, ROUTES.LIST_ALL_ALERT, { exact: true })) {
if (matchPath(pathname, { path: 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 = matchRoute<'servicename'>(
pathname,
ROUTES.SERVICE_METRICS,
);
const serviceMatch = matchPath<{ servicename: string }>(pathname, {
path: ROUTES.SERVICE_METRICS,
exact: false,
});
if (serviceMatch?.params.servicename) {
return [
{
@@ -210,7 +210,7 @@ export function getAutoContexts(
}
// Services list — `/services`.
if (matchRoute(pathname, ROUTES.APPLICATION, { exact: true })) {
if (matchPath(pathname, { path: ROUTES.APPLICATION, exact: true })) {
return [
{
source: 'auto',
@@ -226,7 +226,7 @@ export function getAutoContexts(
// ── Logs ──────────────────────────────────────────────────────────────────
if (matchRoute(pathname, ROUTES.LOGS_EXPLORER)) {
if (matchPath(pathname, { path: ROUTES.LOGS_EXPLORER, exact: false })) {
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,7 +251,8 @@ export function getAutoContexts(
// Trace detail — `/trace/:id`. Treated as a detail-as-metadata page
// (resourceId null, `traceId` lives in metadata).
const traceMatch = matchRoute<'id'>(pathname, ROUTES.TRACE_DETAIL, {
const traceMatch = matchPath<{ id: string }>(pathname, {
path: ROUTES.TRACE_DETAIL,
exact: true,
});
if (traceMatch?.params.id) {
@@ -271,7 +272,7 @@ export function getAutoContexts(
];
}
if (matchRoute(pathname, ROUTES.TRACES_EXPLORER)) {
if (matchPath(pathname, { path: ROUTES.TRACES_EXPLORER, exact: false })) {
return [
{
source: 'auto',
@@ -288,7 +289,9 @@ export function getAutoContexts(
// ── Metrics ───────────────────────────────────────────────────────────────
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer).
if (matchRoute(pathname, ROUTES.METRICS_EXPLORER_BASE)) {
if (
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_BASE, exact: false })
) {
return [
{
source: 'auto',

View File

@@ -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 (matchRoute(pathname, template, { exact: true })) {
if (matchPath(pathname, { path: template, exact: true })) {
return template;
}
}
@@ -46,7 +46,7 @@ export function normalizePage(pathname: string): string {
export function useAIAssistantAnalyticsContext(
conversationId?: string,
): AIAssistantAnalyticsContext {
const { pathname } = useAppLocation();
const { pathname } = useLocation();
const variant = useVariant();
const threadId = useAIAssistantStore((s) => {
const id = conversationId ?? s.activeConversationId;

View File

@@ -1,7 +1,7 @@
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { matchRoute } from 'lib/router/matchRoute';
import { matchPath } from 'react-router-dom';
import { getAutoContexts } from './getAutoContexts';
@@ -45,10 +45,15 @@ 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 (matchRoute(pathname, ROUTES.HOME, { exact: true })) {
if (matchPath(pathname, { path: ROUTES.HOME, exact: true })) {
return PageTypeDTO.homepage;
}
if (matchRoute(pathname, ROUTES.INFRASTRUCTURE_MONITORING_BASE)) {
if (
matchPath(pathname, {
path: ROUTES.INFRASTRUCTURE_MONITORING_BASE,
exact: false,
})
) {
return PageTypeDTO.infra_entity_detail;
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { AppLink } from 'lib/router/AppLink';
import { Link } from 'react-router-dom';
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 && (
<AppLink
<Link
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>
</AppLink>
</Link>
)}
{!!relatedTracesLink && (
<AppLink
<Link
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>
</AppLink>
</Link>
)}
</div>
);

View File

@@ -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 { useAppLocation } from 'lib/router/useAppLocation';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { ArrowRight } from '@signozhq/icons';
import TopContributorsContent from './TopContributorsContent';
@@ -16,7 +16,7 @@ function TopContributorsCard({
topContributorsData,
totalCurrentTriggers,
}: TopContributorsCardProps): JSX.Element {
const { search } = useAppLocation();
const { search } = useLocation();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
const viewAllTopContributorsParam = searchParams.get('viewAllTopContributors');
@@ -43,7 +43,7 @@ function TopContributorsCard({
return newState;
});
navigate({ search: searchParams.toString() });
history.push({ search: searchParams.toString() });
};
return (

View File

@@ -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 { navigate } from 'lib/router/navigation';
import history from 'lib/history';
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());
navigate({
history.push({
search: urlQuery.toString(),
});
}

View File

@@ -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 { useAppLocation } from 'lib/router/useAppLocation';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
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 } = useAppLocation();
const { search } = useLocation();
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);
navigate({ search: searchParams.toString() });
history.push({ search: searchParams.toString() });
};
const tabs = [

View File

@@ -4,8 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { AppLink } from 'lib/router/AppLink';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Link, useLocation } from 'react-router-dom';
import { Button, Card, Input, Space, TableProps, Tooltip, Flex } from 'antd';
import { Search } from '@signozhq/icons';
import type { ColumnType, TablePaginationConfig } from 'antd/es/table';
@@ -29,7 +28,7 @@ import {
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { useAllErrorsQueryState } from 'pages/AllErrors/QueryStateContext';
import { useTimezone } from 'providers/Timezone';
@@ -68,7 +67,7 @@ function AllErrors(): JSX.Element {
const { maxTime, minTime, loading } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { pathname } = useAppLocation();
const { pathname } = useLocation();
const params = useUrlQuery();
const { t } = useTranslation(['common']);
const {
@@ -229,9 +228,7 @@ function AllErrors(): JSX.Element {
queryParams.serviceName = serviceFilterValue;
}
navigate(`${pathname}?${createQueryParams(queryParams)}`, {
replace: true,
});
history.replace(`${pathname}?${createQueryParams(queryParams)}`);
confirm();
},
[
@@ -339,13 +336,13 @@ function AllErrors(): JSX.Element {
...getFilter(onExceptionTypeFilter, 'Search By Exception', 'exceptionType'),
render: (value, record): JSX.Element => (
<Tooltip overlay={(): JSX.Element => value}>
<AppLink
<Link
to={`${ROUTES.ERROR_DETAIL}?groupId=${
record.groupID
}&timestamp=${getNanoSeconds(record.lastSeen)}`}
>
{value}
</AppLink>
</Link>
</Tooltip>
),
sorter: true,
@@ -441,7 +438,7 @@ function AllErrors(): JSX.Element {
exceptionType: getFilterString(params.get(urlKey.exceptionType)),
});
const compositeQuery = params.get(urlKey.compositeQuery) || '';
navigate(
history.replace(
`${pathname}?${createQueryParams({
order: updatedOrder,
offset: (current - 1) * pageSize,
@@ -451,7 +448,6 @@ function AllErrors(): JSX.Element {
serviceName,
compositeQuery,
})}`,
{ replace: true },
);
}
},

View File

@@ -1,5 +1,6 @@
// 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';
@@ -7,7 +8,6 @@ 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 (
<TestRouter initialRoute={initUrl ?? '/exceptions'}>
<MemoryRouter initialEntries={initUrl ?? ['/exceptions']}>
<TimezoneProvider>
<Provider store={store}>
<MockQueryClientProvider>
@@ -65,12 +65,12 @@ function Exceptions({ initUrl }: { initUrl?: string }): JSX.Element {
</MockQueryClientProvider>
</Provider>
</TimezoneProvider>
</TestRouter>
</MemoryRouter>
);
}
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,7 +143,11 @@ 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
@@ -167,7 +171,11 @@ 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');
@@ -180,7 +188,11 @@ 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');

View File

@@ -4,14 +4,16 @@ import {
getApiMonitoringParams,
setApiMonitoringParams,
} from 'container/ApiMonitoring/queryParams';
import { mockNavigate } from 'tests/router-mocks';
const navigateSpy = mockNavigate();
function navigatedSearch(): string | undefined {
const [to] = navigateSpy.mock.calls[0];
return typeof to === 'string' ? to : to.search;
}
// 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(),
};
});
describe('API Monitoring Query Params', () => {
describe('getApiMonitoringParams', () => {
@@ -56,21 +58,25 @@ 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, false);
setApiMonitoringParams(newParams, search, history as any, false);
expect(navigateSpy).toHaveBeenCalledWith(
{ search: expect.stringContaining('apiMonitoringParams') },
{ replace: false },
);
expect(history.push).toHaveBeenCalledWith({
search: expect.stringContaining('apiMonitoringParams'),
});
expect(history.replace).not.toHaveBeenCalled();
// Verify that the search string contains the expected encoded params
const searchArg = navigatedSearch();
const searchArg = history.push.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
@@ -82,21 +88,30 @@ 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, true);
setApiMonitoringParams(newParams, search, history as any, true);
expect(navigateSpy).toHaveBeenCalledWith(
{ search: expect.stringContaining('apiMonitoringParams') },
{ replace: true },
);
expect(history.replace).toHaveBeenCalledWith({
search: expect.stringContaining('apiMonitoringParams'),
});
expect(history.push).not.toHaveBeenCalled();
});
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,
@@ -117,10 +132,10 @@ describe('API Monitoring Query Params', () => {
selectedEndPointName: '/api/test',
};
setApiMonitoringParams(newParams, search, false);
setApiMonitoringParams(newParams, search, history as any, false);
// Verify merged params
const searchArg = navigatedSearch();
const searchArg = history.push.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
@@ -146,7 +161,27 @@ describe('API Monitoring Query Params', () => {
state: null,
};
return { location };
// 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 };
};
it('retrieves URL params correctly from location', () => {
@@ -172,7 +207,7 @@ describe('API Monitoring Query Params', () => {
});
it('updates URL correctly with new params', () => {
const { location } = mockUseLocationAndHistory();
const { location, history } = mockUseLocationAndHistory();
const newParams: Partial<ApiMonitoringParams> = {
selectedDomain: 'new-domain',
@@ -180,14 +215,13 @@ describe('API Monitoring Query Params', () => {
};
// Manually execute the core logic of the hook's setParams function
setApiMonitoringParams(newParams, location.search);
setApiMonitoringParams(newParams, location.search, history as any);
expect(navigateSpy).toHaveBeenCalledWith(
{ search: expect.stringContaining('apiMonitoringParams') },
{ replace: false },
);
expect(history.push).toHaveBeenCalledWith({
search: expect.stringContaining('apiMonitoringParams'),
});
const searchArg = navigatedSearch();
const searchArg = history.push.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
@@ -213,16 +247,20 @@ describe('API Monitoring Query Params', () => {
const initialSearch = `?${urlParams.toString()}`;
// Set up mocks
const { location } = mockUseLocationAndHistory(initialSearch);
const { location, history } = mockUseLocationAndHistory(initialSearch);
// Manually execute the core logic
setApiMonitoringParams({ selectedView: 'new-view' }, location.search);
setApiMonitoringParams(
{ selectedView: 'new-view' },
location.search,
history as any,
);
// Verify history was updated
expect(navigateSpy).toHaveBeenCalled();
expect(history.push).toHaveBeenCalled();
// Parse the new query params from the URL
const searchArg = navigatedSearch();
const searchArg = history.push.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),

View File

@@ -1,6 +1,5 @@
import { useCallback } from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { navigate } from 'lib/router/navigation';
import { useHistory, useLocation } from 'react-router-dom';
// --- Types for all API Monitoring query params ---
export interface ApiMonitoringParams {
@@ -56,6 +55,7 @@ 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,7 +63,11 @@ export function setApiMonitoringParams(
const merged = { ...current, ...newParams };
urlParams.set(PARAM_KEY, encodeParams(merged));
const newSearch = `?${urlParams.toString()}`;
navigate({ search: newSearch }, { replace });
if (replace) {
history.replace({ search: newSearch });
} else {
history.push({ search: newSearch });
}
}
// --- React hook to use query params in a component ---
@@ -71,14 +75,15 @@ export function useApiMonitoringParams(): [
ApiMonitoringParams,
(newParams: Partial<ApiMonitoringParams>, replace?: boolean) => void,
] {
const location = useAppLocation();
const location = useLocation();
const history = useHistory();
const params = getApiMonitoringParams(location.search);
const setParams = useCallback(
(newParams: Partial<ApiMonitoringParams>, replace = false) => {
setApiMonitoringParams(newParams, location.search, replace);
setApiMonitoringParams(newParams, location.search, history, replace);
},
[location.search],
[location.search, history],
);
return [params, setParams];

View File

@@ -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 { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
import * as Sentry from '@sentry/react';
import { Toaster } from '@signozhq/ui/sonner';
import { TooltipProvider } from '@signozhq/ui/tooltip';
@@ -51,7 +51,7 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useNotifications } from 'hooks/useNotifications';
import useTabVisibility from 'hooks/useTabFocus';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useAppContext } from 'providers/App/App';
@@ -190,7 +190,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { pathname } = useAppLocation();
const { pathname } = useLocation();
const { t } = useTranslation(['titles']);
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
@@ -464,7 +464,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}, [isLoggedIn]);
const handleUpgrade = useCallback((): void => {
navigate(ROUTES.BILLING);
history.push(ROUTES.BILLING);
}, []);
const handleFailedPayment = useCallback((): void => {

View File

@@ -9,7 +9,8 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import CancelSubscriptionBanner from './CancelSubscriptionBanner';
jest.mock('utils/basePath', () => ({
...jest.requireActual('utils/basePath'),
getBasePath: (): string => '/',
withBasePath: (path: string): string => path,
getAbsoluteUrl: (path: string): string => `https://test.signoz.io${path}`,
getBaseUrl: (): string => 'https://test.signoz.io',
}));

View File

@@ -1,4 +1,5 @@
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';
@@ -15,7 +16,6 @@ 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 } = useAppLocation();
const { pathname } = useLocation();
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isDrawerOpen = useAIAssistantStore((state) => state.isDrawerOpen);
const isModalOpen = useAIAssistantStore((state) => state.isModalOpen);

View File

@@ -6,6 +6,18 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_TO_TITLE, ALERT_TYPE_URL_MAP } from './constants';
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigationType: jest.fn(() => 'PUSH'),
useLocation: jest.fn(() => ({
pathname: '/alerts/new',
search: '',
hash: '',
state: null,
})),
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
}));
jest
.spyOn(usePrefillAlertConditions, 'usePrefillAlertConditions')
.mockReturnValue({

View File

@@ -1,3 +1,4 @@
import { MemoryRouter, Route } from 'react-router-dom';
import ROUTES from 'constants/routes';
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
import CreateAlertPage from 'pages/CreateAlert';
@@ -6,6 +7,27 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_URL_MAP } from './constants';
// The setup guide button only exists in the classic form.
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string; search: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERTS_NEW}`,
search: 'ruleType=anomaly_rule&showClassicCreateAlertsPage=true',
}),
}));
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigationType: jest.fn(() => 'PUSH'),
useLocation: jest.fn(() => ({
pathname: '/alerts/new',
search: 'ruleType=anomaly_rule&showClassicCreateAlertsPage=true',
hash: '',
state: null,
})),
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
}));
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
@@ -37,10 +59,13 @@ describe('Anomaly Alert Documentation Redirection', () => {
});
it('should handle anomaly alert documentation redirection correctly', () => {
// The setup guide button only exists in the classic form.
const { getByRole } = render(<CreateAlertPage />, undefined, {
initialRoute: `${ROUTES.ALERTS_NEW}?ruleType=anomaly_rule&showClassicCreateAlertsPage=true`,
});
const { getByRole } = render(
<MemoryRouter initialEntries={['/alerts/new']}>
<Route path={ROUTES.ALERTS_NEW}>
<CreateAlertPage />
</Route>
</MemoryRouter>,
);
const alertType = AlertTypes.ANOMALY_BASED_ALERT;

View File

@@ -16,6 +16,18 @@ import { DataSource } from 'types/common/queryBuilder';
import CreateAlertRule from '../index';
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigationType: jest.fn(() => 'PUSH'),
useLocation: jest.fn(() => ({
pathname: '/alerts/new',
search: '',
hash: '',
state: null,
})),
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
}));
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
__esModule: true,
default: function MockDateTimeSelector(): JSX.Element {

View File

@@ -1,6 +1,6 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { TestRouter } from 'tests/router';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';
@@ -129,13 +129,13 @@ const createTestQueryClient = (): QueryClient =>
const renderAlertThreshold = (): ReturnType<typeof render> => {
const queryClient = createTestQueryClient();
return render(
<TestRouter>
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
<AlertThreshold {...mockProps} />
</CreateAlertProvider>
</QueryClientProvider>
</TestRouter>,
</MemoryRouter>,
);
};

View File

@@ -4,7 +4,6 @@ import ROUTES from 'constants/routes';
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
import * as useSafeNavigateHook from 'hooks/useSafeNavigate';
import { mockAppLocation } from 'tests/router-mocks';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import * as rulesHook from '../../../../api/generated/services/rules';
@@ -43,7 +42,12 @@ jest.mock('uplot', () => {
};
});
mockAppLocation({ search: '' });
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { search: string } => ({
search: '',
}),
}));
const ENTER_ALERT_RULE_NAME_PLACEHOLDER = 'Enter alert rule name';

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -41,7 +41,7 @@ function ChartPreview({
const yAxisUnit = alertState.yAxisUnit || '';
const location = useAppLocation();
const location = useLocation();
const yAxisUnitFromURL = new URLSearchParams(location.search).get(
QueryParams.yAxisUnit,
);

View File

@@ -1,6 +1,7 @@
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
@@ -9,7 +10,6 @@ import {
} from 'container/CreateAlertV2/context/constants';
import { buildInitialAlertDef } from 'container/CreateAlertV2/context/utils';
import store from 'store';
import { TestRouter } from 'tests/router';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { EQueryType } from 'types/common/dashboard';
@@ -130,11 +130,11 @@ const renderChartPreview = (): ReturnType<typeof render> =>
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<TestRouter>
<MemoryRouter>
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
<ChartPreview alertDef={mockAlertDef} />
</CreateAlertProvider>
</TestRouter>
</MemoryRouter>
</QueryClientProvider>
</Provider>,
);

View File

@@ -1,6 +1,7 @@
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryParams } from 'constants/query';
@@ -10,7 +11,6 @@ import {
} from 'constants/queryBuilder';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import store from 'store';
import { TestRouter } from 'tests/router';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
@@ -133,11 +133,11 @@ const renderQuerySection = (search = ''): ReturnType<typeof render> =>
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<TestRouter initialRoute={`/alerts/new${search}`}>
<MemoryRouter initialEntries={[`/alerts/new${search}`]}>
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
<QuerySection />
</CreateAlertProvider>
</TestRouter>
</MemoryRouter>
</QueryClientProvider>
</Provider>,
);
@@ -427,11 +427,11 @@ describe('QuerySection', () => {
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<TestRouter>
<MemoryRouter>
<CreateAlertProvider initialAlertType={AlertTypes.LOGS_BASED_ALERT}>
<QuerySection />
</CreateAlertProvider>
</TestRouter>
</MemoryRouter>
</QueryClientProvider>
</Provider>,
);
@@ -481,11 +481,11 @@ describe('QuerySection', () => {
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<TestRouter>
<MemoryRouter>
<CreateAlertProvider initialAlertType={AlertTypes.TRACES_BASED_ALERT}>
<QuerySection />
</CreateAlertProvider>
</TestRouter>
</MemoryRouter>
</QueryClientProvider>
</Provider>,
);

View File

@@ -1,4 +1,4 @@
import { navigate } from 'lib/router/navigation';
import { useHistory } from 'react-router-dom';
import { render, screen, userEvent } from 'tests/test-utils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -125,11 +125,12 @@ function SearchMutator({
}: {
to?: string;
}): JSX.Element {
const routerHistory = useHistory();
return (
<button
type="button"
data-testid="mutate-search"
onClick={(): void => navigate(to, { replace: true })}
onClick={(): void => routerHistory.replace(to)}
>
change search
</button>

View File

@@ -10,7 +10,7 @@ import {
useRef,
useState,
} from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
import {
useCreateRule,
useTestRule,
@@ -129,7 +129,7 @@ export function CreateAlertProvider(
[setCreateAlertState],
);
const location = useAppLocation();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const ruleNameFromURL = queryParams.get(QueryParams.ruleName);
const yAxisUnitFromURL = queryParams.get(QueryParams.yAxisUnit);

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
import { Button, Space } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
@@ -13,7 +13,7 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getNanoSeconds } from 'container/AllError/utils';
import { useNotifications } from 'hooks/useNotifications';
import createQueryParams from 'lib/createQueryParams';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { urlKey } from 'pages/ErrorDetails/utils';
import { useTimezone } from 'providers/Timezone';
@@ -29,7 +29,7 @@ import './styles.scss';
function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
const { idPayload } = props;
const { t } = useTranslation(['errorDetails', 'common']);
const { search, pathname } = useAppLocation();
const { search, pathname } = useLocation();
const params = useMemo(() => new URLSearchParams(search), [search]);
@@ -100,7 +100,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
errorId: id,
};
navigate(`${pathname}?${createQueryParams(queryParams)}`, { replace: true });
history.replace(`${pathname}?${createQueryParams(queryParams)}`);
} catch (error) {
notifications.error({
message: t('something_went_wrong'),
@@ -126,7 +126,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
if (isModifierKeyPressed(event)) {
openInNewTab(path);
} else {
navigate(path);
history.push(path);
}
};

View File

@@ -1,9 +1,9 @@
import { useHistory } from 'react-router-dom';
import { ConciergeBell } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useAppNavigate } from 'lib/router/useAppNavigate';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -22,7 +22,7 @@ function CreateAlertButton({
sourcepage: DataSource;
isOneChartPerQuery?: boolean;
}): JSX.Element {
const navigate = useAppNavigate();
const history = useHistory();
const { panelType } = useQueryBuilder();
const createAlert = (): void => {
@@ -33,7 +33,7 @@ function CreateAlertButton({
EXPLORER_ACTION_EVENTS[sourcepage].createAlert,
getExplorerActionEventPayload({ sourcepage, panelType, isOneChartPerQuery }),
);
navigate(getCreateAlertLink({ query, panelType }));
history.push(getCreateAlertLink({ query, panelType }));
};
return isOneChartPerQuery ? (

View File

@@ -1,3 +1,4 @@
import { useHistory } from 'react-router-dom';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
@@ -8,13 +9,16 @@ import {
} from 'container/LogsExplorerViews/explorerUtils';
import { getQueryByPanelType as getTracesQueryByPanelType } from 'container/TracesExplorer/explorerUtils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { mockAppNavigate } from 'tests/router-mocks';
import { render, screen, userEvent } from 'tests/test-utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import CreateAlertButton from '../CreateAlertButton';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: jest.fn(),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
@@ -23,7 +27,8 @@ jest.mock('api/common/logEvent', () => ({
default: jest.fn(() => Promise.resolve()),
}));
const mockedNavigate = mockAppNavigate();
const mockPush = jest.fn();
const mockedUseHistory = jest.mocked(useHistory);
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
const mockedLogEvent = jest.mocked(logEvent);
@@ -56,8 +61,8 @@ function stagedQuery(
}
function pushedQuery(): Query {
expect(mockedNavigate).toHaveBeenCalledTimes(1);
const [path, search] = (mockedNavigate.mock.calls[0][0] as string).split('?');
expect(mockPush).toHaveBeenCalledTimes(1);
const [path, search] = (mockPush.mock.calls[0][0] as string).split('?');
expect(path).toBe(ROUTES.ALERTS_NEW);
const raw = new URLSearchParams(search).get(QueryParams.compositeQuery);
return JSON.parse(raw as string);
@@ -81,8 +86,11 @@ async function clickCreateAlert(
describe('CreateAlertButton', () => {
beforeEach(() => {
mockedNavigate.mockReset();
mockPush.mockReset();
mockedLogEvent.mockClear();
mockedUseHistory.mockReturnValue({ push: mockPush } as unknown as ReturnType<
typeof useHistory
>);
});
it('is disabled and does nothing without a query', async () => {
@@ -92,7 +100,7 @@ describe('CreateAlertButton', () => {
'aria-disabled',
'true',
);
expect(mockedNavigate).not.toHaveBeenCalled();
expect(mockPush).not.toHaveBeenCalled();
});
it("logs the explorer's own create alert event", async () => {

View File

@@ -1,5 +1,5 @@
import ROUTES from 'constants/routes';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import {
createErrorResponse,
handleInternalServerError,
@@ -12,12 +12,19 @@ import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
import ForgotPassword, { ForgotPasswordRouteState } from '../index';
// Mock dependencies
jest.mock('lib/router/navigation', () => ({
...jest.requireActual('lib/router/navigation'),
navigate: jest.fn(),
jest.mock('lib/history', () => ({
__esModule: true,
default: {
push: jest.fn(),
location: {
search: '',
},
},
}));
const mockNavigate = navigate as jest.MockedFunction<typeof navigate>;
const mockHistoryPush = history.push as jest.MockedFunction<
typeof history.push
>;
const FORGOT_PASSWORD_ENDPOINT = '*/api/v2/factor_password/forgot';
@@ -229,7 +236,7 @@ describe('ForgotPassword Component', () => {
const backToLoginButton = screen.getByTestId('back-to-login');
await user.click(backToLoginButton);
expect(mockNavigate).toHaveBeenCalledWith(ROUTES.LOGIN);
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
});
});
@@ -321,7 +328,7 @@ describe('ForgotPassword Component', () => {
const backButton = screen.getByTestId('forgot-password-back');
await user.click(backButton);
expect(mockNavigate).toHaveBeenCalledWith(ROUTES.LOGIN);
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
});
});

View File

@@ -7,7 +7,7 @@ import { ErrorResponseHandlerForGeneratedAPIs } from 'api/ErrorResponseHandlerFo
import { useForgotPassword } from 'api/generated/services/users';
import AuthError from 'components/AuthError/AuthError';
import ROUTES from 'constants/routes';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import APIError from 'types/api/error';
import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
import { getBaseUrl } from 'utils/basePath';
@@ -108,7 +108,7 @@ function ForgotPassword({
}, [form, forgotPasswordMutate, initialOrgId, hasMultipleOrgs]);
const handleBackToLogin = useCallback((): void => {
navigate(ROUTES.LOGIN);
history.push(ROUTES.LOGIN);
}, []);
// Success screen

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import Spinner from 'components/Spinner';
@@ -28,7 +28,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { isEmpty } from 'lodash-es';
import { useIsAnomalyDetectionAvailable } from 'container/CreateAlertV2/useIsAnomalyDetectionAvailable';
@@ -223,7 +223,7 @@ function ChartPreview({
const isDarkMode = useIsDarkMode();
const urlQuery = useUrlQuery();
const location = useAppLocation();
const location = useLocation();
const optionName =
getFormatNameByOptionId(alertDef?.condition.targetUnit || '') || '';
@@ -245,7 +245,7 @@ function ChartPreview({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
navigate(generatedUrl);
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);

View File

@@ -15,7 +15,7 @@ const TEST_MAPPINGS = {
},
};
jest.mock('lib/router/useAppLocation', () => {
jest.mock('react-router-dom-v5-compat', () => {
const mockThreshold1 = {
index: '0d11f426-a02e-48da-867c-b79c6ef1ff06',
isEditEnabled: false,
@@ -43,7 +43,8 @@ jest.mock('lib/router/useAppLocation', () => {
thresholdValue: 900,
};
return {
useAppLocation: jest.fn().mockReturnValue({
...jest.requireActual('react-router-dom-v5-compat'),
useLocation: jest.fn().mockReturnValue({
state: {
thresholds: [mockThreshold1, mockThreshold2],
},

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom';
import { BellDot, CircleAlert, ExternalLink, Save } from '@signozhq/icons';
import { Button, FormInstance, SelectProps } from 'antd';
import { ConfirmDialog } from '@signozhq/ui/dialog';
@@ -107,7 +107,7 @@ function FormAlertRules({
>((state) => state.globalTime);
const urlQuery = useUrlQuery();
const location = useAppLocation();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const dataSource = useMemo(

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useLocation } from 'react-router-dom-v5-compat';
import { ThresholdProps } from 'types/api/widgets/threshold';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -13,7 +13,7 @@ export const usePrefillAlertConditions = (
target: number | undefined;
targetUnit: string | undefined;
} => {
const location = useAppLocation();
const location = useLocation();
// Extract and set match type
const reduceTo = useMemo(() => {

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { AppLink } from 'lib/router/AppLink';
import { Link } from 'react-router-dom';
import { Callout } from '@signozhq/ui/callout';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
@@ -55,21 +55,21 @@ function LicenseRowDismissibleCallout(): JSX.Element | null {
{' '}
Find your{' '}
{hasServiceAccountsAccess && (
<AppLink
<Link
to={ROUTES.SERVICE_ACCOUNTS_SETTINGS}
className="license-key-callout__link"
>
Service account here
</AppLink>
</Link>
)}
{hasServiceAccountsAccess && hasIngestionAccess && ' and '}
{hasIngestionAccess && (
<AppLink
<Link
to={ROUTES.INGESTION_SETTINGS}
className="license-key-callout__link"
>
Ingestion key here
</AppLink>
</Link>
)}
.
</>

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Button, Skeleton } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import logEvent from 'api/common/logEvent';
@@ -6,9 +7,7 @@ import { useListRules } from 'api/generated/services/rules';
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { AppLink } from 'lib/router/AppLink';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { ArrowRight, ArrowUpRight, Plus } from '@signozhq/icons';
import Card from 'periscope/components/Card/Card';
@@ -34,7 +33,7 @@ export default function AlertRules({
[],
);
const location = useAppLocation();
const location = useLocation();
const params = new URLSearchParams(location.search);
// Fetch Alerts
@@ -89,7 +88,7 @@ export default function AlertRules({
{user?.role !== USER_ROLES.VIEWER && (
<div className="empty-actions-container">
<AppLink to={ROUTES.ALERTS_NEW}>
<Link to={ROUTES.ALERTS_NEW}>
<Button
type="default"
className="periscope-btn secondary"
@@ -100,7 +99,7 @@ export default function AlertRules({
>
Create Alert Rule
</Button>
</AppLink>
</Link>
<Button
type="link"
@@ -147,7 +146,7 @@ export default function AlertRules({
params.set(QueryParams.ruleId, record.id);
navigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
};
const renderAlertRules = (): JSX.Element => (
@@ -229,7 +228,7 @@ export default function AlertRules({
{rulesExist && (
<Card.Footer>
<div className="alert-rules-footer home-data-card-footer">
<AppLink to={ROUTES.LIST_ALL_ALERT}>
<Link to={ROUTES.LIST_ALL_ALERT}>
<Button
type="link"
className="periscope-btn link learn-more-link"
@@ -239,7 +238,7 @@ export default function AlertRules({
>
All Alert Rules <ArrowRight size={12} />
</Button>
</AppLink>
</Link>
</div>
</Card.Footer>
)}

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Button, Skeleton } from 'antd';
import { AppLink } from 'lib/router/AppLink';
import { Badge } from '@signozhq/ui/badge';
import logEvent from 'api/common/logEvent';
import { useListDashboardsForUserV2 } from 'api/generated/services/dashboard';
@@ -79,7 +79,7 @@ export default function Dashboards({
{user?.role !== USER_ROLES.VIEWER && (
<div className="empty-actions-container">
<AppLink to={ROUTES.ALL_DASHBOARD}>
<Link to={ROUTES.ALL_DASHBOARD}>
<Button
type="default"
className="periscope-btn secondary"
@@ -90,7 +90,7 @@ export default function Dashboards({
>
New Dashboard
</Button>
</AppLink>
</Link>
<Button
type="link"
@@ -212,7 +212,7 @@ export default function Dashboards({
{dashboardsExist && (
<Card.Footer>
<div className="dashboards-footer home-data-card-footer">
<AppLink to={ROUTES.ALL_DASHBOARD}>
<Link to={ROUTES.ALL_DASHBOARD}>
<Button
type="link"
className="periscope-btn link learn-more-link"
@@ -222,7 +222,7 @@ export default function Dashboards({
>
All Dashboards <ArrowRight size={12} />
</Button>
</AppLink>
</Link>
</div>
</Card.Footer>
)}

View File

@@ -4,7 +4,7 @@ import { Skeleton } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetHosts } from 'api/generated/services/zeus';
import ROUTES from 'constants/routes';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { Link2 } from '@signozhq/icons';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
@@ -51,7 +51,7 @@ function DataSourceInfo({
logEvent('Homepage: Connect dataSource clicked', {});
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
navigate(ROUTES.GET_STARTED_WITH_CLOUD);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
}

View File

@@ -30,7 +30,7 @@ import { useHomeStripInfo } from 'container/Home/useHomeStripInfo';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import cloneDeep from 'lodash-es/cloneDeep';
import { AnimatePresence, motion } from 'motion/react';
import Card from 'periscope/components/Card/Card';
@@ -38,7 +38,8 @@ import { useAppContext } from 'providers/App/App';
import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { isIngestionActive, isModifierKeyPressed } from 'utils/app';
import { isIngestionActive } from 'utils/app';
import { isModifierKeyPressed } from 'utils/app';
import { popupContainer } from 'utils/selectPopupContainer';
import crackerUrl from '@/assets/Icons/cracker.svg';
@@ -370,7 +371,7 @@ export default function Home(): JSX.Element {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Logs',
});
navigate(ROUTES.LOGS_EXPLORER);
history.push(ROUTES.LOGS_EXPLORER);
}
}}
>
@@ -413,7 +414,7 @@ export default function Home(): JSX.Element {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Traces',
});
navigate(ROUTES.TRACES_EXPLORER);
history.push(ROUTES.TRACES_EXPLORER);
}
}}
>
@@ -456,7 +457,7 @@ export default function Home(): JSX.Element {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Metrics',
});
navigate(ROUTES.METRICS_EXPLORER);
history.push(ROUTES.METRICS_EXPLORER);
}
}}
>

View File

@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { ArrowRight, ArrowRightToLine, BookOpenText } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
@@ -93,12 +93,12 @@ function HomeChecklist({
});
if (item.toRoute !== ROUTES.GET_STARTED_WITH_CLOUD) {
navigate(item.toRoute || '');
history.push(item.toRoute || '');
} else if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
navigate(item.toRoute || '');
history.push(item.toRoute || '');
} else {
openInNewTab(item.docsLink || '');
}

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { Button, Skeleton } from 'antd';
import { AppLink } from 'lib/router/AppLink';
import logEvent from 'api/common/logEvent';
import { useListSavedViews } from 'api/generated/services/saved-view';
import {
@@ -148,7 +148,7 @@ export default function SavedViews({
{user?.role !== USER_ROLES.VIEWER && (
<div className="empty-actions-container">
<AppLink to={getStartedLink}>
<Link to={getStartedLink}>
<Button
type="default"
className="periscope-btn secondary"
@@ -161,7 +161,7 @@ export default function SavedViews({
>
Get Started &nbsp; <ArrowRight size={16} />
</Button>
</AppLink>
</Link>
<Button
type="link"

View File

@@ -2,6 +2,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { QueryKey } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { Button, Select, Skeleton, Table } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
@@ -15,8 +16,7 @@ import useGetTopLevelOperations from 'hooks/useGetTopLevelOperations';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { AppLink } from 'lib/router/AppLink';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { ArrowRight, ArrowUpRight } from '@signozhq/icons';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
@@ -77,7 +77,7 @@ const EmptyState = memo(
activeLicenseV3 &&
activeLicenseV3.platform === LicensePlatform.CLOUD
) {
navigate(ROUTES.GET_STARTED_WITH_CLOUD);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
}
@@ -340,7 +340,7 @@ function ServiceMetrics({
{servicesExist && (
<Card.Footer>
<div className="services-footer home-data-card-footer">
<AppLink to="/services">
<Link to="/services">
<Button
type="link"
className="periscope-btn link learn-more-link"
@@ -350,7 +350,7 @@ function ServiceMetrics({
>
All Services <ArrowRight size={12} />
</Button>
</AppLink>
</Link>
</div>
</Card.Footer>
)}

View File

@@ -1,13 +1,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { Link } from 'react-router-dom';
import { Button, Select, Skeleton, Table } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { useQueryService } from 'hooks/useQueryService';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { AppLink } from 'lib/router/AppLink';
import { navigate } from 'lib/router/navigation';
import history from 'lib/history';
import { ArrowRight, ArrowUpRight } from '@signozhq/icons';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
@@ -134,7 +134,7 @@ export default function ServiceTraces({
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
navigate(ROUTES.GET_STARTED_WITH_CLOUD);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
}
@@ -238,7 +238,7 @@ export default function ServiceTraces({
{servicesExist && (
<Card.Footer>
<div className="services-footer home-data-card-footer">
<AppLink to="/services">
<Link to="/services">
<Button
type="link"
className="periscope-btn link learn-more-link"
@@ -248,7 +248,7 @@ export default function ServiceTraces({
>
All Services <ArrowRight size={12} />
</Button>
</AppLink>
</Link>
</div>
</Card.Footer>
)}

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