Compare commits

..

18 Commits

Author SHA1 Message Date
Vinícius Lourenço
c615baac03 refactor(router): move files added on main onto the facade 2026-10-15 17:18:15 -03:00
Vinícius Lourenço
32444ac89b feat(lint): reject router mocks in tests and raise the router rule to error 2026-10-15 17:18:15 -03:00
Vinícius Lourenço
10587de709 test(router): drive router-dependent tests through the facade 2026-10-15 17:18:14 -03:00
Vinícius Lourenço
11a813b2b7 docs(router): describe the facade against the v7 target 2026-10-15 17:18:14 -03:00
Vinícius Lourenço
b6712d2c7f test: keep window.location and basePath stubs complete for history@5 2026-10-15 17:18:14 -03:00
Vinícius Lourenço
5b995b5bc1 test(storybook): mount route-driven stories through renderAtRoute 2026-10-15 17:18:14 -03:00
Vinícius Lourenço
9a8e795f31 refactor(router): move the navigation blocker onto the facade 2026-10-15 17:18:14 -03:00
Vinícius Lourenço
3374261d1f test(router): add facade spies for router-dependent tests 2026-10-15 17:18:13 -03:00
Vinícius Lourenço
019c9a6591 refactor(router): route the remaining consumers through the facade 2026-10-15 17:18:13 -03:00
Vinícius Lourenço
77d3a832ea feat(router): add search params and navigation type facade hooks 2026-10-15 17:18:13 -03:00
Vinícius Lourenço
db08d7792a test(router): centralise the test router on the history singleton 2026-10-15 17:18:13 -03:00
Vinícius Lourenço
b4e83ed7e3 test(router): repoint stale router mocks at the facade 2026-10-15 17:18:12 -03:00
Vinícius Lourenço
0542d37adb refactor(router): stop threading a raw History through props 2026-10-15 17:18:12 -03:00
Vinícius Lourenço
5cfef312eb fix(router): stop navigate() passing state as undefined 2026-10-15 17:18:12 -03:00
Vinícius Lourenço
ca011493ea refactor(router): route consumers through the facade 2026-10-15 17:18:12 -03:00
Vinícius Lourenço
969d1c0840 refactor(router): drop the three withRouter HOCs 2026-10-15 17:18:11 -03:00
Vinícius Lourenço
ef71ee3510 feat(lint): warn on direct react-router imports 2026-10-15 17:18:11 -03:00
Vinícius Lourenço
498c89fb14 feat(router): add the src/lib/router facade 2026-10-15 17:18:11 -03:00
442 changed files with 4345 additions and 6309 deletions

View File

@@ -3,44 +3,16 @@ name: e2eci
on:
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
pull_request_target:
types:
- labeled
merge_group:
types:
- checks_requested
concurrency:
group: e2eci-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
changes:
if: github.event_name != 'merge_group'
runs-on: ubuntu-latest
outputs:
e2e: ${{ steps.changes.outputs.e2e }}
steps:
- name: changes
id: changes
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
LABELED: ${{ contains(github.event.pull_request.labels.*.name, 'safe-to-e2e') }}
run: |
files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename')
if [ "$LABELED" = true ] || grep -qE '^(frontend|tests/e2e)/' <<< "$files"; then
echo "e2e=true" >> "$GITHUB_OUTPUT"
fi
fmtlint:
needs: changes
if: |
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && needs.changes.outputs.e2e == 'true'
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && contains(github.event.pull_request.labels.*.name, 'safe-to-e2e')
runs-on: ubuntu-latest
steps:
- name: checkout
@@ -69,10 +41,9 @@ jobs:
project:
- chromium
shard: [1, 2, 3, 4]
needs: changes
if: |
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && needs.changes.outputs.e2e == 'true'
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && contains(github.event.pull_request.labels.*.name, 'safe-to-e2e')
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -181,17 +152,3 @@ jobs:
name: playwright-artifacts-${{ matrix.project }}-${{ matrix.shard }}
path: tests/e2e/artifacts/
retention-days: 5
# Required status check. Fails when e2e was needed but did not pass,
# including fork and dependabot PRs still waiting on safe-to-test.
e2e:
if: always()
needs: [changes, fmtlint, test]
runs-on: ubuntu-latest
steps:
- name: verdict
if: |
github.event_name != 'merge_group' && (needs.changes.result != 'success' ||
(needs.changes.outputs.e2e == 'true' && (needs.fmtlint.result != 'success' || needs.test.result != 'success')))
run: |
echo "::error::e2e did not pass. Fork and dependabot PRs need the safe-to-test label."
exit 1

View File

@@ -16064,8 +16064,10 @@ paths:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key: []
- tokenizer: []
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get stats
tags:
- stats

View File

@@ -297,6 +297,8 @@
// Forces useDashboardFetchRequired() outside the root V2 pages (allowlisted in overrides below)
"signoz/no-msw-in-story-file": "error",
// Bans msw imports in *.stories.tsx; handlers/mock data belong in the sibling .stories.mocks.tsx
"signoz/no-direct-react-router-import": "error",
// Steers call sites to the src/lib/router facade (allowlisted in overrides below)
"no-restricted-globals": [
"error",
{
@@ -582,6 +584,23 @@
"rules": {
"signoz/no-dashboard-fetch-outside-root": "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

@@ -0,0 +1,126 @@
import { ruleTester } from './rule-tester.mjs';
const FACADE = 'src/lib/router facade';
const HISTORY = 'lib/history singleton';
const MOCK = 'tests/router-mocks helpers';
await ruleTester({
rule: 'no-direct-react-router-import',
valid: [
{
name: 'facade import',
code: "import { useAppNavigate } from 'lib/router/useAppNavigate';",
},
{
name:
'the history package itself is a version-bump concern, not a facade one',
code: "import { createBrowserHistory } from 'history';",
},
{
name: 'unrelated module whose name contains history',
code: "import { useHistoryPanel } from 'container/HistoryPanel';",
},
{
name: 'mocking a facade module',
code: "jest.mock('lib/router/navigation');",
},
{
name: 'jest call that does not name a module',
code: 'jest.fn();',
},
{
name: 'require of an unrelated module',
code: "const x = require('lib/dashboardVariables');",
},
{
name: 'export without a source',
code: 'const a = 1;\nexport { a };',
},
],
invalid: [
{
name: 'jest.mock of react-router-dom',
code: "jest.mock('react-router-dom', () => ({}));",
errors: [{ message: MOCK, line: 1, column: 11 }],
},
{
name: 'jest.requireActual of react-router-dom',
code: "const actual = jest.requireActual('react-router-dom');",
errors: [{ message: MOCK }],
},
{
name: 'jest.mock of lib/history',
code: "jest.mock('lib/history');",
errors: [{ message: MOCK }],
},
{
name: 'jest.doMock of react-router-dom-v5-compat',
code: "jest.doMock('react-router-dom-v5-compat');",
errors: [{ message: MOCK }],
},
{
name: 'react-router-dom named import',
code: "import { useHistory } from 'react-router-dom';",
errors: [{ message: FACADE, line: 1, column: 28 }],
},
{
name: 'react-router named import',
code: "import { useLocation } from 'react-router';",
errors: [{ message: FACADE }],
},
{
name: 'react-router-dom type-only import',
code: "import type { RouteProps } from 'react-router-dom';",
errors: [{ message: FACADE }],
},
{
name: 'react-router-dom-v5-compat',
code: "import { useNavigate } from 'react-router-dom-v5-compat';",
errors: [{ message: 'compat package is an implementation detail' }],
},
{
name: 'lib/history default import',
code: "import history from 'lib/history';",
errors: [{ message: HISTORY }],
},
{
name: 'lib/history through the @/ alias',
code: "import history from '@/lib/history';",
errors: [{ message: HISTORY }],
},
{
name: 'jest.mock of lib/history through the @/ alias',
code: "jest.mock('@/lib/history');",
errors: [{ message: MOCK }],
},
{
name: 'lib/history require',
code: "const history = require('lib/history').default;",
errors: [{ message: HISTORY }],
},
{
name: 'dynamic import',
code: "const mod = await import('react-router-dom');",
errors: [{ message: FACADE }],
},
{
name: 're-export',
code: "export { Link } from 'react-router-dom';",
errors: [{ message: FACADE }],
},
{
name: 'export all',
code: "export * from 'react-router-dom';",
errors: [{ message: FACADE }],
},
{
name: 'one report per import statement',
code:
"import { Link } from 'react-router-dom';\nimport history from 'lib/history';",
errors: [
{ message: FACADE, line: 1 },
{ message: HISTORY, line: 2 },
],
},
],
});

View File

@@ -0,0 +1,123 @@
/**
* Rule: no-direct-react-router-import
*
* Every router concern goes through the `src/lib/router/*` facade, so a router
* upgrade is a change to one directory instead of ~300 call sites. This rule
* keeps new call sites, and test mocks, from reaching past it.
*
* `history` (the package) is deliberately not flagged: it is a transitive concern of
* the version bump, not something the facade replaces.
*
* The allowlist (the facade itself, the route table, `useSafeNavigate`, the history
* singleton and the test and story router mounts) is applied via overrides in
* .oxlintrc.json, not here, so the file list stays visible next to the severity.
*/
import path from 'node:path';
const HISTORY_MODULE_SUFFIX = path.join('src', 'lib', 'history');
const JEST_MODULE_METHODS = new Set([
'mock',
'doMock',
'unmock',
'dontMock',
'setMock',
'requireActual',
'requireMock',
'createMockFromModule',
]);
const MESSAGE_IDS = {
'react-router': 'v5Router',
'react-router-dom': 'v5Router',
'react-router-dom-v5-compat': 'v6Compat',
'lib/history': 'historySingleton',
'@/lib/history': 'historySingleton',
};
/** Resolves `./history` / `../history` so files inside src/lib count too. */
function isHistoryModule(specifier, filename) {
if (!specifier.startsWith('.') || !filename) {
return false;
}
const resolved = path.resolve(path.dirname(filename), specifier);
return (
resolved.endsWith(HISTORY_MODULE_SUFFIX) ||
resolved.endsWith(`${HISTORY_MODULE_SUFFIX}.ts`)
);
}
function messageIdFor(specifier, filename) {
if (MESSAGE_IDS[specifier] !== undefined) {
return MESSAGE_IDS[specifier];
}
return isHistoryModule(specifier, filename) ? 'historySingleton' : null;
}
export default {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow direct react-router / lib/history imports; import from src/lib/router instead',
category: 'React Router migration',
},
schema: [],
messages: {
v5Router:
'Do not import react-router-dom directly. Use the src/lib/router facade — useAppNavigate, useAppLocation, useAppParams, AppLink, Redirect, matchRoute — so a router upgrade stays contained.',
v6Compat:
'Do not import react-router-dom-v5-compat directly. Use the src/lib/router facade instead; the compat package is an implementation detail of the migration and disappears with it.',
historySingleton:
'Do not import the lib/history singleton. Use useAppNavigate() inside components, or the imperative helpers in src/lib/router/navigation.ts outside them — 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,6 +16,7 @@ import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root
import noConditionalTextNodesWithSiblings from './rules/no-conditional-text-nodes-with-siblings.mjs';
import noReturnTextNodes from './rules/no-return-text-nodes.mjs';
import noMswInStoryFile from './rules/no-msw-in-story-file.mjs';
import noDirectReactRouterImport from './rules/no-direct-react-router-import.mjs';
export default {
meta: {
@@ -33,5 +34,6 @@ export default {
'no-conditional-text-nodes-with-siblings': noConditionalTextNodesWithSiblings,
'no-return-text-nodes': noReturnTextNodes,
'no-msw-in-story-file': noMswInStoryFile,
'no-direct-react-router-import': noDirectReactRouterImport,
},
};

View File

@@ -1,7 +1,7 @@
import deleteLocalStorageKey from 'api/browser/localstorage/remove';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { navigate } from 'lib/router/navigation';
import deleteSession from './v2/sessions/delete';
@@ -21,5 +21,5 @@ export const Logout = async (): Promise<void> => {
deleteLocalStorageKey(LOCALSTORAGE.CHAT_SUPPORT);
deleteLocalStorageKey(LOCALSTORAGE.USER_ID);
window.dispatchEvent(new CustomEvent('LOGOUT'));
history.push(ROUTES.LOGIN);
navigate(ROUTES.LOGIN);
};

View File

@@ -1,6 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { Row, Select, Spin } from 'antd';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import {
getValuesFromQueryParams,
setQueryParamsFromOptions,
@@ -35,8 +36,7 @@ export function FilterSelect({
useCeleryFilterOptions(filterType);
const urlQuery = useUrlQuery();
const history = useHistory();
const location = useLocation();
const location = useAppLocation();
// Add state to track the current search input
const [searchValue, setSearchValue] = useState<string>('');
@@ -66,7 +66,7 @@ export function FilterSelect({
setQueryParamsFromOptions(
value as string[],
urlQuery,
history,
navigate,
location,
queryParam,
);
@@ -77,7 +77,6 @@ export function FilterSelect({
handleSearch,
shouldSetQueryParams,
urlQuery,
history,
location,
queryParam,
onChange,

View File

@@ -1,5 +1,6 @@
import { useHistory, useLocation } from 'react-router-dom';
import { Select, Spin } from 'antd';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Typography } from '@signozhq/ui/typography';
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query';
@@ -16,8 +17,7 @@ import './CeleryTaskConfigOptions.styles.scss';
function CeleryTaskConfigOptions(): JSX.Element {
const { handleSearch, isFetching, options } =
useCeleryFilterOptions('celery.task_name');
const history = useHistory();
const location = useLocation();
const location = useAppLocation();
const urlQuery = useUrlQuery();
@@ -52,7 +52,7 @@ function CeleryTaskConfigOptions(): JSX.Element {
setQueryParamsFromOptions(
value,
urlQuery,
history,
navigate,
location,
QueryParams.taskName,
);

View File

@@ -1,8 +1,9 @@
import { useCallback, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/WidgetCard/config';
@@ -50,8 +51,7 @@ function CeleryTaskBar({
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const { pathname } = useAppLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const isDarkMode = useIsDarkMode();
@@ -68,13 +68,13 @@ function CeleryTaskBar({
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
navigate(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
[dispatch, pathname, urlQuery],
);
const [barState, setBarState] = useState<CeleryTaskState>(CeleryTaskState.All);

View File

@@ -1,8 +1,9 @@
import { useCallback, useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/WidgetCard/config';
@@ -54,8 +55,7 @@ function CeleryTaskGraph({
checkIfDataExists?: (isDataAvailable: boolean) => void;
analyticsEvent?: string;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const { pathname } = useAppLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const isDarkMode = useIsDarkMode();
@@ -83,13 +83,13 @@ function CeleryTaskGraph({
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
navigate(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
[dispatch, pathname, urlQuery],
);
return (

View File

@@ -1,8 +1,9 @@
import { useCallback, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { Col, Row } from 'antd';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -48,8 +49,7 @@ function CeleryTaskLatencyGraph({
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const { pathname } = useAppLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const isDarkMode = useIsDarkMode();
@@ -80,13 +80,13 @@ function CeleryTaskLatencyGraph({
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
navigate(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
[dispatch, pathname, urlQuery],
);
const selectedFilters = useMemo(

View File

@@ -1,5 +1,5 @@
import { QueryParams } from 'constants/query';
import { History, Location } from 'history';
import type { AppLocation, NavigateFn } from 'lib/router/types';
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
import { Widgets } from 'types/api/widgets/widget';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
@@ -17,13 +17,13 @@ export function getValuesFromQueryParams(
export function setQueryParamsFromOptions(
value: string[],
urlQuery: URLSearchParams,
history: History<unknown>,
location: Location<unknown>,
navigate: NavigateFn,
location: AppLocation,
queryParams: QueryParams,
): void {
urlQuery.set(queryParams, value.join(','));
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
navigate(generatedUrl, { replace: true });
}
export function getFiltersFromQueryParams(

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Button } from 'antd';
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
import logEvent from 'api/common/logEvent';
@@ -8,7 +8,7 @@ import { MessageSquareText } from '@signozhq/icons';
export default function ChatSupportGateway(): JSX.Element {
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
useState(false);
const { pathname } = useLocation();
const { pathname } = useAppLocation();
return (
<>

View File

@@ -1,20 +1,12 @@
import { useState } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import dayjs from 'dayjs';
import { mockAppLocation } from 'tests/router-mocks';
import * as timeUtils from 'utils/timeUtils';
import CustomTimePicker from './CustomTimePicker';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useLocation: jest.fn().mockReturnValue({
pathname: '/test-path',
}),
};
});
mockAppLocation({ pathname: '/test-path' });
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),

View File

@@ -6,7 +6,7 @@ import {
useRef,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Button } from '@signozhq/ui/button';
import { Input, InputRef, Popover, Tooltip } from 'antd';
import cx from 'classnames';
@@ -109,7 +109,7 @@ function CustomTimePicker({
const [inputErrorDetails, setInputErrorDetails] = useState<
TimeRangeValidationResult['errorDetails'] | null
>(null);
const location = useLocation();
const location = useAppLocation();
const inputRef = useRef<InputRef>(null);
const initialInputValueOnOpenRef = useRef<string>('');

View File

@@ -6,7 +6,7 @@ import {
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -114,7 +114,7 @@ function CustomTimePickerPopoverContent({
customDateTimeInputStatus = CustomTimePickerInputStatus.UNSET,
inputErrorDetails,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation();
const { pathname } = useAppLocation();
const isLogsExplorerPage = useMemo(
() => pathname === ROUTES.LOGS_EXPLORER,

View File

@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryParams } from 'constants/query';
import { mockAppLocation } from 'tests/router-mocks';
import { GlobalReducer } from 'types/reducer/globalTime';
import CustomTimePicker from '../CustomTimePicker';
@@ -59,10 +60,6 @@ jest.mock('providers/Timezone', () => ({
}),
}));
jest.mock('react-router-dom', () => ({
useLocation: (): { pathname: string } => ({ pathname: '/logs-explorer' }),
}));
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const now = Date.now();
const defaultProps = {
@@ -85,6 +82,7 @@ describe('CustomTimePicker - zoom out button', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
mockAppLocation({ pathname: '/logs-explorer' });
});
afterEach(() => {

View File

@@ -2,23 +2,15 @@ import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { mockAppLocation } from 'tests/router-mocks';
import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import ExplorerCard from '../ExplorerCard';
const historyReplace = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
useHistory: (): any => ({
...jest.requireActual('react-router-dom').useHistory(),
replace: historyReplace,
}),
}));
mockAppLocation({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
});
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({

View File

@@ -1,17 +1,15 @@
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { mockAppLocation } from 'tests/router-mocks';
import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import MenuItemGenerator from '../MenuItemGenerator';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
mockAppLocation({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
});
jest.mock('antd', () => ({
...jest.requireActual('antd'),

View File

@@ -1,17 +1,9 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { fireEvent, render } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
import SaveViewWithName from '../SaveViewWithName';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
const queryClient = new QueryClient({
defaultOptions: {
queries: {

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
import { toast } from '@signozhq/ui/sonner';
import { Button, Input } from 'antd';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
@@ -11,7 +11,7 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
const [activeTab, setActiveTab] = useState('feedback');
const [feedback, setFeedback] = useState('');
const location = useLocation();
const location = useAppLocation();
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const [isLoading, setIsLoading] = useState(false);

View File

@@ -1,5 +1,5 @@
import { useCallback, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Dot } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
@@ -39,7 +39,7 @@ function HeaderRightSection({
enableFeedback,
shareModalExtraOption,
}: HeaderRightSectionProps): JSX.Element | null {
const location = useLocation();
const location = useAppLocation();
const [openFeedbackModal, setOpenFeedbackModal] = useState(false);
const [openShareURLModal, setOpenShareURLModal] = useState(false);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Button, Tooltip } from 'antd';
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
import logEvent from 'api/common/logEvent';
@@ -43,7 +43,7 @@ function LaunchChatSupport({
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
useState(false);
const { pathname } = useLocation();
const { pathname } = useAppLocation();
const isChatSupportEnabled = useMemo(() => {
if (!isFetchingFeatureFlags && (featureFlags || featureFlagsFetchError)) {

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
import {
generatePath,
matchPath,
useLocation,
useParams,
} from 'react-router-dom';
import { Tabs, TabsProps } from 'antd';
import cx from 'classnames';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { buildRoutePath } from 'lib/router/buildRoutePath';
import { matchRoute } from 'lib/router/matchRoute';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import { useAppParams } from 'lib/router/useAppParams';
import { RouteTabProps } from './types';
@@ -28,21 +27,17 @@ function RouteTab({
routes,
activeKey,
onChangeHandler,
history,
showRightSection,
className,
...rest
}: RouteTabProps & TabsProps): JSX.Element {
const params = useParams<Params>();
const location = useLocation();
const params = useAppParams<Params>();
const location = useAppLocation();
// Find the matching route for the current pathname
const currentRoute = routes.find((route) => {
const routePath = route.route.split('?')[0];
return matchPath(location.pathname, {
path: routePath,
exact: true,
});
return matchRoute(location.pathname, routePath, { exact: true });
});
const onChange = (activeRoute: string): void => {
@@ -53,8 +48,13 @@ function RouteTab({
const selectedRoute = routes.find((e) => e.key === activeRoute);
if (selectedRoute) {
const resolvedRoute = generatePath(selectedRoute.route, params);
history.push(resolvedRoute);
const resolvedRoute = buildRoutePath(
selectedRoute.route,
Object.fromEntries(
Object.entries(params).filter(([, v]) => v !== undefined),
) as Record<string, string>,
);
navigate(resolvedRoute);
}
};

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import React, { useEffect } from 'react';
import cx from 'classnames';
import { useLocation } from 'react-router-dom';
import {
CommandDialog,
CommandEmpty,
@@ -23,7 +22,8 @@ import {
import { useThemeMode } from 'hooks/useDarkMode';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { IS_DEV } from 'lib/env';
import history from 'lib/history';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import { ROLES as UserRole } from 'types/roles';
import { createShortcutActions } from '../../constants/shortcutActions';
@@ -77,7 +77,7 @@ export function CmdKPalette({
const { open, setOpen } = useCmdK();
const { setAutoSwitch, setTheme, theme } = useThemeMode();
const location = useLocation();
const location = useAppLocation();
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const startNewConversation = useAIAssistantStore(
(s) => s.startNewConversation,
@@ -120,7 +120,7 @@ export function CmdKPalette({
}
function onClickHandler(key: string): void {
history.push(key);
navigate(key);
}
const handleOpenAIAssistant = (): void => {

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

View File

@@ -1,7 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Tooltip } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Minus, Plus, X } from '@signozhq/icons';
@@ -31,8 +32,7 @@ import styles from './AIAssistantModal.module.scss';
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function AIAssistantModal(): JSX.Element | null {
const history = useHistory();
const { pathname } = useLocation();
const { pathname } = useAppLocation();
const [showHistory, setShowHistory] = useState(false);
const isOpen = useAIAssistantStore((s) => s.isModalOpen);
@@ -94,11 +94,11 @@ export default function AIAssistantModal(): JSX.Element | null {
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
// the assistant was already open in the modal, so this is a surface
// switch, not a new open.
history.push(
navigate(
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
{ fromInApp: true },
{ state: { fromInApp: true } },
);
}, [activeConversationId, closeModal, history]);
}, [activeConversationId, closeModal]);
const handleNew = useCallback(() => {
void logEvent(AIAssistantEvents.NewChatClicked, {

View File

@@ -1,6 +1,8 @@
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { matchPath, useHistory, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { matchRoute } from 'lib/router/matchRoute';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Tooltip } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Plus, X } from '@signozhq/icons';
@@ -21,13 +23,11 @@ const AI_ASSISTANT_PANEL_OPEN_CLASS = 'ai-assistant-panel-open';
const AI_ASSISTANT_PANEL_WIDTH_VAR = '--ai-assistant-panel-width';
export default function AIAssistantPanel(): JSX.Element | null {
const history = useHistory();
const { pathname } = useLocation();
const { pathname } = useAppLocation();
const [showHistory, setShowHistory] = useState(false);
const isOpen = useAIAssistantStore((s) => s.isDrawerOpen);
const isFullScreenPage = !!matchPath(pathname, {
path: ROUTES.AI_ASSISTANT,
const isFullScreenPage = !!matchRoute(pathname, ROUTES.AI_ASSISTANT, {
exact: true,
});
const activeConversationId = useAIAssistantStore(
@@ -47,11 +47,11 @@ export default function AIAssistantPanel(): JSX.Element | null {
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
// the assistant was already open in the drawer, so this is a surface
// switch, not a new open.
history.push(
navigate(
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
{ fromInApp: true },
{ state: { fromInApp: true } },
);
}, [activeConversationId, closeDrawer, history]);
}, [activeConversationId, closeDrawer]);
const handleNew = useCallback(() => {
void logEvent(AIAssistantEvents.NewChatClicked, {

View File

@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { matchRoute } from 'lib/router/matchRoute';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Tooltip } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
@@ -21,12 +22,11 @@ import styles from './AIAssistantTrigger.module.scss';
* Hidden when the panel is already open or when on the full-screen AI Assistant page.
*/
export default function AIAssistantTrigger(): JSX.Element | null {
const { pathname } = useLocation();
const { pathname } = useAppLocation();
const isDrawerOpen = useAIAssistantStore((s) => s.isDrawerOpen);
const isModalOpen = useAIAssistantStore((s) => s.isModalOpen);
const isFullScreenPage = !!matchPath(pathname, {
path: ROUTES.AI_ASSISTANT,
const isFullScreenPage = !!matchRoute(pathname, ROUTES.AI_ASSISTANT, {
exact: true,
});

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

View File

@@ -1,9 +1,8 @@
import { MemoryRouter } from 'react-router-dom';
// eslint-disable-next-line no-restricted-imports
import { fireEvent, render } from '@testing-library/react';
import { MessageContext } from 'api/ai-assistant/chat';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
import { VariantContext } from 'container/AIAssistant/VariantContext';
import { TestRouter } from 'tests/router';
const CHIP_ID = 'recent-errors';
const CHIP_TEXT = 'Show me recent errors';
@@ -87,11 +86,11 @@ function renderView(variant: 'panel' | 'page' | 'modal'): {
getByTestId: (id: string) => HTMLElement;
} {
return render(
<MemoryRouter initialEntries={['/dashboard/dashboard-123']}>
<TestRouter initialRoute="/dashboard/dashboard-123">
<VariantContext.Provider value={variant}>
<ConversationView conversationId={CONVERSATION_ID} />
</VariantContext.Provider>
</MemoryRouter>,
</TestRouter>,
);
}

View File

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

View File

@@ -16,10 +16,10 @@ import {
SavedviewtypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import { navigate } from 'lib/router/navigation';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import type { History } from 'history';
import {
buildExplorerNavigationUrl,
@@ -39,6 +39,9 @@ import {
import { resourceRoute, ResourceType } from '../resourceRoute';
jest.mock('api/generated/services/saved-view');
jest.mock('lib/router/navigation', () => ({
navigate: jest.fn(),
}));
jest.mock(
'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi',
@@ -60,6 +63,7 @@ const mockedListSavedViews = listSavedViews as jest.MockedFunction<
const mockedGetSavedView = getSavedView as jest.MockedFunction<
typeof getSavedView
>;
const mockedNavigate = navigate as jest.MockedFunction<typeof navigate>;
function makeView(
id: string,
@@ -247,15 +251,17 @@ describe('buildExplorerNavigationUrl', () => {
});
describe('openSavedView', () => {
it('navigates with history.push and view query params', () => {
const push = jest.fn();
const history = { push } as unknown as History;
beforeEach(() => {
mockedNavigate.mockClear();
});
it('navigates with the view query params', () => {
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
openSavedView(view, history);
openSavedView(view);
expect(push).toHaveBeenCalledTimes(1);
const pushedUrl = push.mock.calls[0][0] as string;
expect(mockedNavigate).toHaveBeenCalledTimes(1);
const pushedUrl = mockedNavigate.mock.calls[0][0] as string;
expect(pushedUrl).toContain(ROUTES.LOGS_EXPLORER);
const params = new URLSearchParams(pushedUrl.split('?')[1]);
expect(params.get(QueryParams.viewKey)).toBe('"view-logs"');
@@ -267,18 +273,14 @@ describe('openSavedView', () => {
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
delete view.source;
expect(() =>
openSavedView(view, { push: jest.fn() } as unknown as History),
).toThrow('Unsupported saved view source');
expect(() => openSavedView(view)).toThrow('Unsupported saved view source');
});
it('throws when the view has no queries', () => {
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
view.spec.queries = [];
expect(() =>
openSavedView(view, { push: jest.fn() } as unknown as History),
).toThrow('Saved view is missing query data');
expect(() => openSavedView(view)).toThrow('Saved view is missing query data');
});
});
@@ -286,44 +288,39 @@ describe('openSavedViewByKey', () => {
beforeEach(() => {
mockedListSavedViews.mockReset();
mockedGetSavedView.mockReset();
mockedNavigate.mockClear();
});
it('prefers the direct view lookup endpoint', async () => {
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
mockedGetSavedView.mockResolvedValueOnce(mockViewByIdResponse(view));
const push = jest.fn();
const history = { push } as unknown as History;
await openSavedViewByKey('view-logs', DataSource.LOGS, history);
await openSavedViewByKey('view-logs', DataSource.LOGS);
expect(mockedGetSavedView).toHaveBeenCalledWith({ id: 'view-logs' });
expect(mockedListSavedViews).not.toHaveBeenCalled();
expect(push).toHaveBeenCalled();
expect(mockedNavigate).toHaveBeenCalled();
});
it('falls back to list probing when direct lookup fails', async () => {
const view = makeView('view-traces', SavedviewtypesSourceDTO.traces);
mockedGetSavedView.mockRejectedValueOnce(new Error('not found'));
mockedListSavedViews.mockResolvedValueOnce(mockViewsResponse([view]));
const push = jest.fn();
const history = { push } as unknown as History;
await openSavedViewByKey('view-traces', DataSource.TRACES, history);
await openSavedViewByKey('view-traces', DataSource.TRACES);
expect(mockedListSavedViews).toHaveBeenCalledWith({
source: SavedviewtypesSourceDTO.traces,
});
expect(push).toHaveBeenCalled();
expect(mockedNavigate).toHaveBeenCalled();
});
it('throws when the saved view does not exist', async () => {
mockedGetSavedView.mockRejectedValueOnce(new Error('not found'));
mockedListSavedViews.mockResolvedValue(mockViewsResponse([]));
await expect(
openSavedViewByKey('missing', DataSource.LOGS, {
push: jest.fn(),
} as unknown as History),
).rejects.toThrow('Saved view not found');
await expect(openSavedViewByKey('missing', DataSource.LOGS)).rejects.toThrow(
'Saved view not found',
);
});
});

View File

@@ -9,10 +9,10 @@ import { SavedViewSourcePage } from 'container/SavedViews/types';
import { findSavedView } from 'container/SavedViews/utils/findSavedView';
import { getSavedViewQuery } from 'container/SavedViews/utils/getSavedViewQuery';
import { toSavedViewSource } from 'container/SavedViews/utils/toSavedViewSource';
import { navigate } from 'lib/router/navigation';
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
import { DataSource } from 'types/common/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { History } from 'history';
type SavedViewSourceHint = SavedViewSourcePage;
@@ -92,10 +92,7 @@ export function buildExplorerNavigationUrl(
return `${route}?${params.toString()}`;
}
export function openSavedView(
view: SavedviewtypesSavedViewDTO,
history: History,
): void {
export function openSavedView(view: SavedviewtypesSavedViewDTO): void {
const route = view.source ? explorerRouteForSourcePage(view.source) : null;
if (!route) {
throw new Error('Unsupported saved view source');
@@ -111,14 +108,13 @@ export function openSavedView(
[QueryParams.viewName]: view.spec.displayName,
[QueryParams.viewKey]: view.id,
});
history.push(url);
navigate(url);
}
export async function openSavedViewByKey(
viewKey: string,
sourceHint: SavedViewSourceHint | null | undefined,
history: History,
): Promise<void> {
const view = await loadSavedView(viewKey, sourceHint);
openSavedView(view, history);
openSavedView(view);
}

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,9 +33,10 @@ export function getAutoContexts(
// Panel editor (V2). `panel/new` has no widget id yet and the schema requires
// a non-empty `panel_edit.widgetId`, so it reports `panel_create` instead.
const panelEditorMatch = matchPath<{ dashboardId: string; panelId: string }>(
const panelEditorMatch = matchRoute<'dashboardId' | 'panelId'>(
pathname,
{ path: ROUTES.DASHBOARD_PANEL_EDITOR, exact: true },
ROUTES.DASHBOARD_PANEL_EDITOR,
{ exact: true },
);
if (panelEditorMatch) {
const { dashboardId, panelId } = panelEditorMatch.params;
@@ -55,8 +56,7 @@ export function getAutoContexts(
// Dashboard detail — `/dashboard/:dashboardId`. The `expandedWidgetId`
// query param signals the panel-fullscreen overlay; otherwise it's the
// plain dashboard view.
const dashboardMatch = matchPath<{ dashboardId: string }>(pathname, {
path: ROUTES.DASHBOARD,
const dashboardMatch = matchRoute<'dashboardId'>(pathname, ROUTES.DASHBOARD, {
exact: true,
});
if (dashboardMatch) {
@@ -89,7 +89,7 @@ export function getAutoContexts(
}
// Dashboard list — `/dashboard`.
if (matchPath(pathname, { path: ROUTES.ALL_DASHBOARD, exact: true })) {
if (matchRoute(pathname, ROUTES.ALL_DASHBOARD, { exact: true })) {
return [
{
source: 'auto',
@@ -106,8 +106,8 @@ export function getAutoContexts(
// or `/alerts/history?ruleId=…`. Mirrors dashboard_detail: resourceId is the
// rule id and shared metadata carries the URL time range when present.
if (
matchPath(pathname, { path: ROUTES.ALERT_OVERVIEW, exact: true }) ||
matchPath(pathname, { path: ROUTES.ALERT_HISTORY, exact: true })
matchRoute(pathname, ROUTES.ALERT_OVERVIEW, { exact: true }) ||
matchRoute(pathname, ROUTES.ALERT_HISTORY, { exact: true })
) {
const ruleId = params.get(QueryParams.ruleId);
if (ruleId) {
@@ -129,7 +129,7 @@ export function getAutoContexts(
// Alert edit — `/alerts/edit?ruleId=…`. The form syncs its query-builder
// state to the URL (`useShareBuilderUrl`), so shared metadata carries the
// alert's query + time range, mirroring the dashboard panel editor.
if (matchPath(pathname, { path: ROUTES.EDIT_ALERTS, exact: true })) {
if (matchRoute(pathname, ROUTES.EDIT_ALERTS, { exact: true })) {
const ruleId = params.get(QueryParams.ruleId);
if (ruleId) {
return [
@@ -145,7 +145,7 @@ export function getAutoContexts(
// Alert new — `/alerts/new`. No rule id yet (draft), but the query-builder
// state is on the URL, so shared metadata carries the in-progress query.
if (matchPath(pathname, { path: ROUTES.ALERTS_NEW, exact: true })) {
if (matchRoute(pathname, ROUTES.ALERTS_NEW, { exact: true })) {
return [
{
source: 'auto',
@@ -157,7 +157,7 @@ export function getAutoContexts(
}
// Triggered-alerts index — `/alerts/history` without a rule id.
if (matchPath(pathname, { path: ROUTES.ALERT_HISTORY, exact: true })) {
if (matchRoute(pathname, ROUTES.ALERT_HISTORY, { exact: true })) {
return [
{
source: 'auto',
@@ -172,7 +172,7 @@ export function getAutoContexts(
}
// Alerts index — `/alerts` with tab query param (defaults to Alert Rules).
if (matchPath(pathname, { path: ROUTES.LIST_ALL_ALERT, exact: true })) {
if (matchRoute(pathname, ROUTES.LIST_ALL_ALERT, { exact: true })) {
const page = resolveAlertsIndexPage(params.get(QueryParams.tab));
return [
{
@@ -191,10 +191,10 @@ export function getAutoContexts(
// Service detail (covers sub-routes like top-level-operations) —
// `/services/:servicename[/...]`.
const serviceMatch = matchPath<{ servicename: string }>(pathname, {
path: ROUTES.SERVICE_METRICS,
exact: false,
});
const serviceMatch = matchRoute<'servicename'>(
pathname,
ROUTES.SERVICE_METRICS,
);
if (serviceMatch?.params.servicename) {
return [
{
@@ -210,7 +210,7 @@ export function getAutoContexts(
}
// Services list — `/services`.
if (matchPath(pathname, { path: ROUTES.APPLICATION, exact: true })) {
if (matchRoute(pathname, ROUTES.APPLICATION, { exact: true })) {
return [
{
source: 'auto',
@@ -226,7 +226,7 @@ export function getAutoContexts(
// ── Logs ──────────────────────────────────────────────────────────────────
if (matchPath(pathname, { path: ROUTES.LOGS_EXPLORER, exact: false })) {
if (matchRoute(pathname, ROUTES.LOGS_EXPLORER)) {
const activeLogId = params.get(QueryParams.activeLogId);
// `?activeLogId=…` indicates a log-detail panel is open. Per the
// schema, log_detail requires payload fields (timestamp, service,
@@ -251,8 +251,7 @@ export function getAutoContexts(
// Trace detail — `/trace/:id`. Treated as a detail-as-metadata page
// (resourceId null, `traceId` lives in metadata).
const traceMatch = matchPath<{ id: string }>(pathname, {
path: ROUTES.TRACE_DETAIL,
const traceMatch = matchRoute<'id'>(pathname, ROUTES.TRACE_DETAIL, {
exact: true,
});
if (traceMatch?.params.id) {
@@ -272,7 +271,7 @@ export function getAutoContexts(
];
}
if (matchPath(pathname, { path: ROUTES.TRACES_EXPLORER, exact: false })) {
if (matchRoute(pathname, ROUTES.TRACES_EXPLORER)) {
return [
{
source: 'auto',
@@ -289,9 +288,7 @@ export function getAutoContexts(
// ── Metrics ───────────────────────────────────────────────────────────────
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer, views).
if (
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_BASE, exact: false })
) {
if (matchRoute(pathname, ROUTES.METRICS_EXPLORER_BASE)) {
return [
{
source: 'auto',

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

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 { matchPath } from 'react-router-dom';
import { matchRoute } from 'lib/router/matchRoute';
import { getAutoContexts } from './getAutoContexts';
@@ -45,15 +45,10 @@ export function resolvePageType(
// Pseudo-pages with no attachable resource: resolved straight from the
// route. They deliberately emit no auto-context chip (see `getAutoContexts`),
// so they can't be derived from `metadata.page` like the pages below.
if (matchPath(pathname, { path: ROUTES.HOME, exact: true })) {
if (matchRoute(pathname, ROUTES.HOME, { exact: true })) {
return PageTypeDTO.homepage;
}
if (
matchPath(pathname, {
path: ROUTES.INFRASTRUCTURE_MONITORING_BASE,
exact: false,
})
) {
if (matchRoute(pathname, ROUTES.INFRASTRUCTURE_MONITORING_BASE)) {
return PageTypeDTO.infra_entity_detail;
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { AppLink } from 'lib/router/AppLink';
import { Color } from '@signozhq/design-tokens';
import { Popover } from 'antd';
import LogsIcon from 'assets/AlertHistory/LogsIcon';
@@ -26,7 +26,7 @@ function PopoverContent({
return (
<div className="contributor-row-popover-buttons">
{!!relatedLogsLink && (
<Link
<AppLink
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
className="contributor-row-popover-buttons__button"
data-testid="alert-popover-view-logs"
@@ -35,10 +35,10 @@ function PopoverContent({
<LogsIcon />
</div>
<div className="text">View Logs</div>
</Link>
</AppLink>
)}
{!!relatedTracesLink && (
<Link
<AppLink
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
className="contributor-row-popover-buttons__button"
data-testid="alert-popover-view-traces"
@@ -50,7 +50,7 @@ function PopoverContent({
/>
</div>
<div className="text">View Traces</div>
</Link>
</AppLink>
)}
</div>
);

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

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 history from 'lib/history';
import { navigate } from 'lib/router/navigation';
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
import { uPlotXAxisValuesFormat } from 'lib/uPlotLib/utils/constants';
@@ -103,7 +103,7 @@ function HorizontalTimelineGraph({
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
history.push({
navigate({
search: urlQuery.toString(),
});
}

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 history from 'lib/history';
import { useAppLocation } from 'lib/router/useAppLocation';
import { navigate } from 'lib/router/navigation';
import { Info } from '@signozhq/icons';
import Tabs2 from 'periscope/components/Tabs2';
@@ -42,7 +42,7 @@ function TimelineTabs(): JSX.Element {
}
function TimelineFilters(): JSX.Element {
const { search } = useLocation();
const { search } = useAppLocation();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
const initialSelectedTab = useMemo(
@@ -52,7 +52,7 @@ function TimelineFilters(): JSX.Element {
const handleFilter = (value: TimelineFilter): void => {
searchParams.set('timelineFilter', value);
history.push({ search: searchParams.toString() });
navigate({ search: searchParams.toString() });
};
const tabs = [

View File

@@ -1,13 +1,13 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { Button } from 'antd';
import type { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { buildRoutePath } from 'lib/router/buildRoutePath';
import { navigate } from 'lib/router/navigation';
import { useAppContext } from 'providers/App/App';
import { Channels } from 'types/api/channels/getAll';
@@ -20,8 +20,8 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const [action] = useComponentPermission(['new_alert_action'], user.role);
const onClickEditHandler = useCallback((id: string) => {
history.push(
generatePath(ROUTES.CHANNELS_EDIT, {
navigate(
buildRoutePath(ROUTES.CHANNELS_EDIT, {
channelId: id,
}),
);

View File

@@ -1,5 +1,6 @@
import ROUTES from 'constants/routes';
import AlertChannels from 'container/AllAlertChannels';
import { mockAppLocation } from 'tests/router-mocks';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
const successNotification = jest.fn();
@@ -13,15 +14,11 @@ jest.mock('hooks/useNotifications', () => ({
})),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
}),
}));
describe('Alert Channels Settings List page', () => {
beforeEach(async () => {
mockAppLocation({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
});
jest.useFakeTimers();
jest.setSystemTime(new Date('2023-10-20'));
render(<AlertChannels />);

View File

@@ -1,5 +1,6 @@
import ROUTES from 'constants/routes';
import AlertChannels from 'container/AllAlertChannels';
import { mockAppLocation } from 'tests/router-mocks';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
const successNotification = jest.fn();
@@ -18,15 +19,11 @@ jest.mock('hooks/useComponentPermission', () => ({
default: jest.fn().mockImplementation(() => [false]),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
}),
}));
describe('Alert Channels Settings List page (Normal User)', () => {
beforeEach(async () => {
mockAppLocation({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
});
jest.useFakeTimers();
render(<AlertChannels />);
await waitFor(() =>

View File

@@ -10,7 +10,7 @@ import Spinner from 'components/Spinner';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { navigate } from 'lib/router/navigation';
import { isUndefined } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
@@ -32,7 +32,7 @@ function AlertChannels(): JSX.Element {
user.role,
);
const onToggleHandler = useCallback(() => {
history.push(ROUTES.CHANNELS_NEW);
navigate(ROUTES.CHANNELS_NEW);
}, []);
const { isLoading, data, error } = useQuery<

View File

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

View File

@@ -1,6 +1,5 @@
// eslint-disable-next-line no-restricted-imports
import { Provider, useSelector } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
@@ -8,6 +7,7 @@ import { rest } from 'msw';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
import { TestRouter } from 'tests/router';
import '@testing-library/jest-dom';
@@ -55,9 +55,9 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
},
} as any);
function Exceptions({ initUrl }: { initUrl?: string[] }): JSX.Element {
function Exceptions({ initUrl }: { initUrl?: string }): JSX.Element {
return (
<MemoryRouter initialEntries={initUrl ?? ['/exceptions']}>
<TestRouter initialRoute={initUrl ?? '/exceptions'}>
<TimezoneProvider>
<Provider store={store}>
<MockQueryClientProvider>
@@ -65,12 +65,12 @@ function Exceptions({ initUrl }: { initUrl?: string[] }): JSX.Element {
</MockQueryClientProvider>
</Provider>
</TimezoneProvider>
</MemoryRouter>
</TestRouter>
);
}
Exceptions.defaultProps = {
initUrl: ['/exceptions'],
initUrl: '/exceptions',
};
const BASE_URL = ENVIRONMENT.baseURL;
@@ -130,7 +130,7 @@ describe('Exceptions - All Errors', () => {
});
it('should call useQueries with exact composite query object', async () => {
render(<Exceptions initUrl={[INIT_URL_WITH_COMMON_QUERY]} />);
render(<Exceptions initUrl={INIT_URL_WITH_COMMON_QUERY} />);
await screen.findByText(/redis timeout/i);
expect(postListErrorsSpy).toHaveBeenCalledWith(
expect.objectContaining({
@@ -143,11 +143,7 @@ describe('Exceptions - All Errors', () => {
it('should navigate to page 2 when pageSize=100 and clicking next', async () => {
// Arrange: start with pageSize=100 and offset=0
render(
<Exceptions
initUrl={[
`/exceptions?pageSize=100&offset=0&order=ascending&orderParam=serviceName`,
]}
/>,
<Exceptions initUrl="/exceptions?pageSize=100&offset=0&order=ascending&orderParam=serviceName" />,
);
// Wait for initial load
@@ -171,11 +167,7 @@ describe('Exceptions - All Errors', () => {
it('initializes current page from URL (offset/pageSize)', async () => {
// offset=100, pageSize=100 => current page should be 2
render(
<Exceptions
initUrl={[
`/exceptions?pageSize=100&offset=100&order=ascending&orderParam=serviceName`,
]}
/>,
<Exceptions initUrl="/exceptions?pageSize=100&offset=100&order=ascending&orderParam=serviceName" />,
);
await screen.findByText(/redis timeout/i);
const activeItem = document.querySelector('.ant-pagination-item-active');
@@ -188,11 +180,7 @@ describe('Exceptions - All Errors', () => {
it('clicking a numbered page updates offset correctly', async () => {
// pageSize=100, click page 3 => offset = 200
render(
<Exceptions
initUrl={[
`/exceptions?pageSize=100&offset=0&order=ascending&orderParam=serviceName`,
]}
/>,
<Exceptions initUrl="/exceptions?pageSize=100&offset=0&order=ascending&orderParam=serviceName" />,
);
await screen.findByText(/redis timeout/i);
const page3Item = screen.getByTitle('3');

View File

@@ -5,15 +5,11 @@ import {
setApiMonitoringParams,
} from 'container/ApiMonitoring/queryParams';
// 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(),
};
});
// Mock navigation module
const mockNavigate = jest.fn();
jest.mock('lib/router/navigation', () => ({
navigate: (...args: any[]) => mockNavigate(...args),
}));
describe('API Monitoring Query Params', () => {
describe('getApiMonitoringParams', () => {
@@ -57,26 +53,26 @@ describe('API Monitoring Query Params', () => {
});
describe('setApiMonitoringParams', () => {
beforeEach(() => {
mockNavigate.mockClear();
});
it('updates URL with new params (push mode)', () => {
const history = {
push: jest.fn(),
replace: jest.fn(),
};
const search = '';
const newParams: Partial<ApiMonitoringParams> = {
showIP: false,
selectedDomain: 'updated-domain',
};
setApiMonitoringParams(newParams, search, history as any, false);
setApiMonitoringParams(newParams, search, false);
expect(history.push).toHaveBeenCalledWith({
search: expect.stringContaining('apiMonitoringParams'),
});
expect(history.replace).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith(
{ search: expect.stringContaining('apiMonitoringParams') },
{ replace: false },
);
// Verify that the search string contains the expected encoded params
const searchArg = history.push.mock.calls[0][0].search;
const searchArg = mockNavigate.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
@@ -88,30 +84,21 @@ describe('API Monitoring Query Params', () => {
});
it('updates URL with new params (replace mode)', () => {
const history = {
push: jest.fn(),
replace: jest.fn(),
};
const search = '';
const newParams: Partial<ApiMonitoringParams> = {
showIP: false,
selectedDomain: 'updated-domain',
};
setApiMonitoringParams(newParams, search, history as any, true);
setApiMonitoringParams(newParams, search, true);
expect(history.replace).toHaveBeenCalledWith({
search: expect.stringContaining('apiMonitoringParams'),
});
expect(history.push).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith(
{ search: expect.stringContaining('apiMonitoringParams') },
{ replace: true },
);
});
it('merges new params with existing params', () => {
const history = {
push: jest.fn(),
replace: jest.fn(),
};
// Start with some existing params
const existingParams: Partial<ApiMonitoringParams> = {
showIP: true,
@@ -132,10 +119,10 @@ describe('API Monitoring Query Params', () => {
selectedEndPointName: '/api/test',
};
setApiMonitoringParams(newParams, search, history as any, false);
setApiMonitoringParams(newParams, search, false);
// Verify merged params
const searchArg = history.push.mock.calls[0][0].search;
const searchArg = mockNavigate.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
@@ -161,27 +148,7 @@ describe('API Monitoring Query Params', () => {
state: null,
};
// Create mock history object
const history = {
push: jest.fn((args) => {
// Simulate updating the location search
location.search = args.search;
}),
replace: jest.fn((args) => {
location.search = args.search;
}),
length: 1,
location,
};
// Set up mocks for useLocation and useHistory
const useLocationMock = jest.requireMock('react-router-dom').useLocation;
const useHistoryMock = jest.requireMock('react-router-dom').useHistory;
useLocationMock.mockReturnValue(location);
useHistoryMock.mockReturnValue(history);
return { location, history };
return { location };
};
it('retrieves URL params correctly from location', () => {
@@ -207,7 +174,7 @@ describe('API Monitoring Query Params', () => {
});
it('updates URL correctly with new params', () => {
const { location, history } = mockUseLocationAndHistory();
const { location } = mockUseLocationAndHistory();
const newParams: Partial<ApiMonitoringParams> = {
selectedDomain: 'new-domain',
@@ -215,13 +182,14 @@ describe('API Monitoring Query Params', () => {
};
// Manually execute the core logic of the hook's setParams function
setApiMonitoringParams(newParams, location.search, history as any);
setApiMonitoringParams(newParams, location.search);
expect(history.push).toHaveBeenCalledWith({
search: expect.stringContaining('apiMonitoringParams'),
});
expect(mockNavigate).toHaveBeenCalledWith(
{ search: expect.stringContaining('apiMonitoringParams') },
{ replace: false },
);
const searchArg = history.push.mock.calls[0][0].search;
const searchArg = mockNavigate.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
@@ -247,20 +215,16 @@ describe('API Monitoring Query Params', () => {
const initialSearch = `?${urlParams.toString()}`;
// Set up mocks
const { location, history } = mockUseLocationAndHistory(initialSearch);
const { location } = mockUseLocationAndHistory(initialSearch);
// Manually execute the core logic
setApiMonitoringParams(
{ selectedView: 'new-view' },
location.search,
history as any,
);
setApiMonitoringParams({ selectedView: 'new-view' }, location.search);
// Verify history was updated
expect(history.push).toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalled();
// Parse the new query params from the URL
const searchArg = history.push.mock.calls[0][0].search;
const searchArg = mockNavigate.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),

View File

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

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

View File

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

View File

@@ -1,4 +1,3 @@
import { useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import Noz from 'components/Noz/Noz';
@@ -9,6 +8,7 @@ import {
useAIAssistantStore,
} from 'container/AIAssistant/store/useAIAssistantStore';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useAppLocation } from 'lib/router/useAppLocation';
import ROUTES from 'constants/routes';
import { Dot } from '@signozhq/icons';
@@ -23,7 +23,7 @@ import styles from './AskNoz.module.scss';
* hiding the header button would remove a notification rather than move it.
*/
function AskNoz(): JSX.Element | null {
const { pathname } = useLocation();
const { pathname } = useAppLocation();
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isDrawerOpen = useAIAssistantStore((state) => state.isDrawerOpen);
const isModalOpen = useAIAssistantStore((state) => state.isModalOpen);

View File

@@ -23,7 +23,7 @@ import { ErrorType } from 'api/generatedAPIInstance';
import ROUTES from 'constants/routes';
import FormAlertChannels from 'container/FormAlertChannels';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { navigate } from 'lib/router/navigation';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
@@ -143,7 +143,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(error as APIError);
@@ -205,7 +205,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(error as APIError);
@@ -258,7 +258,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
}
return { status: 'failed', statusMessage: t('channel_creation_failed') };
@@ -298,7 +298,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(error as APIError);
@@ -342,7 +342,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(error as APIError);
@@ -380,7 +380,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(error as APIError);
@@ -429,7 +429,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(toAPIError(error as ErrorType<RenderErrorResponseDTO>));
@@ -496,7 +496,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(toAPIError(error as ErrorType<RenderErrorResponseDTO>));
@@ -537,7 +537,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(toAPIError(error as ErrorType<RenderErrorResponseDTO>));
@@ -587,7 +587,7 @@ function CreateAlertChannels({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(toAPIError(error as ErrorType<RenderErrorResponseDTO>));

View File

@@ -6,18 +6,6 @@ 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,4 +1,3 @@
import { MemoryRouter, Route } from 'react-router-dom';
import ROUTES from 'constants/routes';
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
import CreateAlertPage from 'pages/CreateAlert';
@@ -7,26 +6,6 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_URL_MAP } from './constants';
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',
}),
}));
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',
hash: '',
state: null,
})),
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
}));
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
@@ -58,13 +37,9 @@ describe('Anomaly Alert Documentation Redirection', () => {
});
it('should handle anomaly alert documentation redirection correctly', () => {
const { getByRole } = render(
<MemoryRouter initialEntries={['/alerts/new']}>
<Route path={ROUTES.ALERTS_NEW}>
<CreateAlertPage />
</Route>
</MemoryRouter>,
);
const { getByRole } = render(<CreateAlertPage />, undefined, {
initialRoute: `${ROUTES.ALERTS_NEW}?ruleType=anomaly_rule`,
});
const alertType = AlertTypes.ANOMALY_BASED_ALERT;

View File

@@ -13,18 +13,6 @@ 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 } from '@testing-library/react';
import { TestRouter } from 'tests/router';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { CreateAlertProvider } from '../../context';
@@ -100,15 +100,15 @@ const renderAlertCondition = (
alertType?: string,
): ReturnType<typeof render> => {
const queryClient = createTestQueryClient();
const initialEntries = alertType ? [`/?alertType=${alertType}`] : undefined;
const initialRoute = alertType ? `/?alertType=${alertType}` : '/';
return render(
<MemoryRouter initialEntries={initialEntries}>
<TestRouter initialRoute={initialRoute}>
<QueryClientProvider client={queryClient}>
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
<AlertCondition />
</CreateAlertProvider>
</QueryClientProvider>
</MemoryRouter>,
</TestRouter>,
);
};

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 { Channels } from 'types/api/channels/getAll';
@@ -128,13 +128,13 @@ const createTestQueryClient = (): QueryClient =>
const renderAlertThreshold = (): ReturnType<typeof render> => {
const queryClient = createTestQueryClient();
return render(
<MemoryRouter>
<TestRouter>
<QueryClientProvider client={queryClient}>
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
<AlertThreshold {...mockProps} />
</CreateAlertProvider>
</QueryClientProvider>
</MemoryRouter>,
</TestRouter>,
);
};

View File

@@ -4,6 +4,7 @@ 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';
@@ -42,12 +43,7 @@ jest.mock('uplot', () => {
};
});
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { search: string } => ({
search: '',
}),
}));
mockAppLocation({ 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 { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
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 = useLocation();
const location = useAppLocation();
const yAxisUnitFromURL = new URLSearchParams(location.search).get(
QueryParams.yAxisUnit,
);

View File

@@ -1,7 +1,6 @@
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 {
@@ -10,6 +9,7 @@ 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}>
<MemoryRouter>
<TestRouter>
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
<ChartPreview alertDef={mockAlertDef} />
</CreateAlertProvider>
</MemoryRouter>
</TestRouter>
</QueryClientProvider>
</Provider>,
);

View File

@@ -1,7 +1,6 @@
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';
@@ -11,6 +10,7 @@ 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';
@@ -131,11 +131,11 @@ const renderQuerySection = (): ReturnType<typeof render> =>
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<TestRouter>
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
<QuerySection />
</CreateAlertProvider>
</MemoryRouter>
</TestRouter>
</QueryClientProvider>
</Provider>,
);
@@ -404,11 +404,11 @@ describe('QuerySection', () => {
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<TestRouter>
<CreateAlertProvider initialAlertType={AlertTypes.LOGS_BASED_ALERT}>
<QuerySection />
</CreateAlertProvider>
</MemoryRouter>
</TestRouter>
</QueryClientProvider>
</Provider>,
);
@@ -458,11 +458,11 @@ describe('QuerySection', () => {
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<TestRouter>
<CreateAlertProvider initialAlertType={AlertTypes.TRACES_BASED_ALERT}>
<QuerySection />
</CreateAlertProvider>
</MemoryRouter>
</TestRouter>
</QueryClientProvider>
</Provider>,
);

View File

@@ -1,6 +1,7 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter, useHistory } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { navigate } from 'lib/router/navigation';
import { TestRouter } from 'tests/router';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { INITIAL_CREATE_ALERT_STATE } from '../constants';
@@ -45,13 +46,13 @@ function renderWithSearch(search: string): void {
defaultOptions: { queries: { retry: false } },
});
render(
<MemoryRouter initialEntries={[`/alerts/new${search}`]}>
<TestRouter initialRoute={`/alerts/new${search}`}>
<QueryClientProvider client={queryClient}>
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
<Probe />
</CreateAlertProvider>
</QueryClientProvider>
</MemoryRouter>,
</TestRouter>,
);
}
@@ -115,15 +116,14 @@ describe('CreateAlertProvider — URL-declared prefill (issue #5291)', () => {
// rewrites location.search after the alert loads, which used to re-run the prefill
// effect and RESET the loaded threshold back to 0.
function SearchMutator(): JSX.Element {
const routerHistory = useHistory();
return (
<button
type="button"
data-testid="mutate-search"
onClick={(): void =>
routerHistory.replace(
'/alerts/overview?compositeQuery=normalized&ruleId=r1',
)
navigate('/alerts/overview?compositeQuery=normalized&ruleId=r1', {
replace: true,
})
}
>
change search
@@ -151,7 +151,7 @@ describe('CreateAlertProvider — edit mode ignores URL prefill', () => {
defaultOptions: { queries: { retry: false } },
});
render(
<MemoryRouter initialEntries={['/alerts/overview?ruleId=r1']}>
<TestRouter initialRoute="/alerts/overview?ruleId=r1">
<QueryClientProvider client={queryClient}>
<CreateAlertProvider
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
@@ -163,7 +163,7 @@ describe('CreateAlertProvider — edit mode ignores URL prefill', () => {
<SearchMutator />
</CreateAlertProvider>
</QueryClientProvider>
</MemoryRouter>,
</TestRouter>,
);
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');

View File

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

View File

@@ -47,7 +47,7 @@ import {
} from 'container/CreateAlertChannels/utils';
import FormAlertChannels from 'container/FormAlertChannels';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { navigate } from 'lib/router/navigation';
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
@@ -145,7 +145,7 @@ function EditAlertChannels({
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
notifications.error({
@@ -204,7 +204,7 @@ function EditAlertChannels({
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
notifications.error({
@@ -243,7 +243,7 @@ function EditAlertChannels({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
notifications.error({
@@ -298,7 +298,7 @@ function EditAlertChannels({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
notifications.error({
@@ -345,7 +345,7 @@ function EditAlertChannels({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
notifications.error({
@@ -392,7 +392,7 @@ function EditAlertChannels({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
notifications.error({
@@ -443,7 +443,7 @@ function EditAlertChannels({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
const apiError = notifyError(error);
@@ -511,7 +511,7 @@ function EditAlertChannels({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
const apiError = notifyError(error);
@@ -561,7 +561,7 @@ function EditAlertChannels({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
const apiError = notifyError(error);
@@ -616,7 +616,7 @@ function EditAlertChannels({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
const apiError = notifyError(error);

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
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 history from 'lib/history';
import { navigate } from 'lib/router/navigation';
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 } = useLocation();
const { search, pathname } = useAppLocation();
const params = useMemo(() => new URLSearchParams(search), [search]);
@@ -100,7 +100,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
errorId: id,
};
history.replace(`${pathname}?${createQueryParams(queryParams)}`);
navigate(`${pathname}?${createQueryParams(queryParams)}`, { replace: true });
} 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 {
history.push(path);
navigate(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 history = useHistory();
const navigate = useAppNavigate();
const { panelType } = useQueryBuilder();
const createAlert = (): void => {
@@ -33,7 +33,7 @@ function CreateAlertButton({
EXPLORER_ACTION_EVENTS[sourcepage].createAlert,
getExplorerActionEventPayload({ sourcepage, panelType, isOneChartPerQuery }),
);
history.push(getCreateAlertLink({ query, panelType }));
navigate(getCreateAlertLink({ query, panelType }));
};
return isOneChartPerQuery ? (

View File

@@ -1,4 +1,3 @@
import { useHistory } from 'react-router-dom';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
@@ -9,16 +8,13 @@ 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(),
}));
@@ -27,8 +23,7 @@ jest.mock('api/common/logEvent', () => ({
default: jest.fn(() => Promise.resolve()),
}));
const mockPush = jest.fn();
const mockedUseHistory = jest.mocked(useHistory);
const mockedNavigate = mockAppNavigate();
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
const mockedLogEvent = jest.mocked(logEvent);
@@ -61,8 +56,8 @@ function stagedQuery(
}
function pushedQuery(): Query {
expect(mockPush).toHaveBeenCalledTimes(1);
const [path, search] = (mockPush.mock.calls[0][0] as string).split('?');
expect(mockedNavigate).toHaveBeenCalledTimes(1);
const [path, search] = (mockedNavigate.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);
@@ -86,11 +81,8 @@ async function clickCreateAlert(
describe('CreateAlertButton', () => {
beforeEach(() => {
mockPush.mockReset();
mockedNavigate.mockReset();
mockedLogEvent.mockClear();
mockedUseHistory.mockReturnValue({ push: mockPush } as unknown as ReturnType<
typeof useHistory
>);
});
it('is disabled and does nothing without a query', async () => {
@@ -100,7 +92,7 @@ describe('CreateAlertButton', () => {
'aria-disabled',
'true',
);
expect(mockPush).not.toHaveBeenCalled();
expect(mockedNavigate).not.toHaveBeenCalled();
});
it("logs the explorer's own create alert event", async () => {

View File

@@ -8,7 +8,7 @@ import {
useRef,
useState,
} from 'react';
import { useHistory } from 'react-router-dom';
import { navigate } from 'lib/router/navigation';
import {
Check,
ConciergeBell,
@@ -107,7 +107,6 @@ function ExplorerOptions({
const [newViewName, setNewViewName] = useState<string>('');
const [color, setColor] = useState(Color.BG_SIENNA_500);
const { notifications } = useNotifications();
const history = useHistory();
const ref = useRef<RefSelectProps>(null);
const isDarkMode = useIsDarkMode();
const [queryToExport, setQueryToExport] = useState<Query | null>(null);
@@ -240,14 +239,14 @@ function ExplorerOptions({
const stringifiedQuery = handleConditionalQueryModification(defaultQuery);
history.push(
navigate(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
stringifiedQuery,
)}`,
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[handleConditionalQueryModification, history],
[handleConditionalQueryModification],
);
const onCancel = (value: boolean) => (): void => {
@@ -566,7 +565,7 @@ function ExplorerOptions({
});
if (signalSource === 'meter') {
history.replace(ROUTES.METER_EXPLORER);
navigate(ROUTES.METER_EXPLORER, { replace: true });
return;
}
@@ -574,7 +573,7 @@ function ExplorerOptions({
handleChangeSelectedView(panelTypeToExplorerView[PANEL_TYPES.LIST]);
}
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
navigate(DATASOURCE_VS_ROUTES[sourcepage], { replace: true });
};
const isQueryUpdated = isStagedQueryUpdated(

View File

@@ -1,7 +1,7 @@
import { useHistory } from 'react-router-dom';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { MOCK_QUERY } from 'container/QueryTable/Drilldown/__tests__/mockTableData';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { navigate } from 'lib/router/navigation';
import { rest, server } from 'mocks-server/server';
import {
defaultFeatureFlags,
@@ -17,9 +17,9 @@ import { buildExportPanelLink } from 'pages/DashboardPage/DashboardContainer/Pan
import ExplorerOptionWrapper from '../ExplorerOptionWrapper';
import { getExplorerToolBarVisibility } from '../utils';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: jest.fn(),
jest.mock('lib/router/navigation', () => ({
...jest.requireActual('lib/router/navigation'),
navigate: jest.fn(),
}));
jest.mock('../utils', () => ({
@@ -35,7 +35,7 @@ const mockGetExplorerToolBarVisibility = jest.mocked(
getExplorerToolBarVisibility,
);
const mockUseHistory = jest.mocked(useHistory);
const mockNavigate = jest.mocked(navigate);
// Mock data
const TEST_QUERY_ID = 'test-query-id';
@@ -141,10 +141,6 @@ describe('ExplorerOptionWrapper', () => {
it('should navigate to alert creation page when "Create an Alert" is clicked in logs-explorer', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockPush = jest.fn();
mockUseHistory.mockReturnValue({
push: mockPush,
} as unknown as ReturnType<typeof useHistory>);
renderExplorerOptionWrapper({ sourcepage: DataSource.LOGS });
@@ -153,8 +149,8 @@ describe('ExplorerOptionWrapper', () => {
});
await user.click(createAlertButton);
expect(mockPush).toHaveBeenCalledTimes(1);
const calledWith = mockPush.mock.calls[0][0] as string;
expect(mockNavigate).toHaveBeenCalledTimes(1);
const calledWith = mockNavigate.mock.calls[0][0] as string;
const [path, search = ''] = calledWith.split('?');
expect(path).toBe('/alerts/new');
const params = new URLSearchParams(search);

View File

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

View File

@@ -18,7 +18,7 @@ import {
SlackChannel,
WebhookChannel,
} from 'container/CreateAlertChannels/config';
import history from 'lib/history';
import { navigate } from 'lib/router/navigation';
import EmailSettings from './Settings/Email';
import GoogleChatSettings from './Settings/GoogleChat';
@@ -202,7 +202,7 @@ function FormAlertChannels({
<Button
data-testid="return-button"
onClick={(): void => {
history.replace(ROUTES.ALL_CHANNELS);
navigate(ROUTES.ALL_CHANNELS, { replace: true });
}}
>
{t('button_return')}

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 { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import Spinner from 'components/Spinner';
@@ -29,7 +29,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { navigate } from 'lib/router/navigation';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
@@ -226,7 +226,7 @@ function ChartPreview({
const isDarkMode = useIsDarkMode();
const urlQuery = useUrlQuery();
const location = useLocation();
const location = useAppLocation();
const optionName =
getFormatNameByOptionId(alertDef?.condition.targetUnit || '') || '';
@@ -248,7 +248,7 @@ function ChartPreview({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
navigate(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);

View File

@@ -15,7 +15,7 @@ const TEST_MAPPINGS = {
},
};
jest.mock('react-router-dom-v5-compat', () => {
jest.mock('lib/router/useAppLocation', () => {
const mockThreshold1 = {
index: '0d11f426-a02e-48da-867c-b79c6ef1ff06',
isEditEnabled: false,
@@ -43,8 +43,7 @@ jest.mock('react-router-dom-v5-compat', () => {
thresholdValue: 900,
};
return {
...jest.requireActual('react-router-dom-v5-compat'),
useLocation: jest.fn().mockReturnValue({
useAppLocation: 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 { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
import { BellDot, CircleAlert, ExternalLink, Save } from '@signozhq/icons';
import { Button, FormInstance, SelectProps } from 'antd';
import { ConfirmDialog } from '@signozhq/ui/dialog';
@@ -109,7 +109,7 @@ function FormAlertRules({
>((state) => state.globalTime);
const urlQuery = useUrlQuery();
const location = useLocation();
const location = useAppLocation();
const queryParams = new URLSearchParams(location.search);
const dataSource = useMemo(

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom-v5-compat';
import { useAppLocation } from 'lib/router/useAppLocation';
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 = useLocation();
const location = useAppLocation();
// Extract and set match type
const reduceTo = useMemo(() => {

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { AppLink } from 'lib/router/AppLink';
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 && (
<Link
<AppLink
to={ROUTES.SERVICE_ACCOUNTS_SETTINGS}
className="license-key-callout__link"
>
Service account here
</Link>
</AppLink>
)}
{hasServiceAccountsAccess && hasIngestionAccess && ' and '}
{hasIngestionAccess && (
<Link
<AppLink
to={ROUTES.INGESTION_SETTINGS}
className="license-key-callout__link"
>
Ingestion key here
</Link>
</AppLink>
)}
.
</>

View File

@@ -1,5 +1,4 @@
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';
@@ -7,7 +6,9 @@ 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 history from 'lib/history';
import { AppLink } from 'lib/router/AppLink';
import { navigate } from 'lib/router/navigation';
import { useAppLocation } from 'lib/router/useAppLocation';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { ArrowRight, ArrowUpRight, Plus } from '@signozhq/icons';
import Card from 'periscope/components/Card/Card';
@@ -33,7 +34,7 @@ export default function AlertRules({
[],
);
const location = useLocation();
const location = useAppLocation();
const params = new URLSearchParams(location.search);
// Fetch Alerts
@@ -88,7 +89,7 @@ export default function AlertRules({
{user?.role !== USER_ROLES.VIEWER && (
<div className="empty-actions-container">
<Link to={ROUTES.ALERTS_NEW}>
<AppLink to={ROUTES.ALERTS_NEW}>
<Button
type="default"
className="periscope-btn secondary"
@@ -99,7 +100,7 @@ export default function AlertRules({
>
Create Alert Rule
</Button>
</Link>
</AppLink>
<Button
type="link"
@@ -146,7 +147,7 @@ export default function AlertRules({
params.set(QueryParams.ruleId, record.id);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
navigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
};
const renderAlertRules = (): JSX.Element => (
@@ -228,7 +229,7 @@ export default function AlertRules({
{rulesExist && (
<Card.Footer>
<div className="alert-rules-footer home-data-card-footer">
<Link to={ROUTES.LIST_ALL_ALERT}>
<AppLink to={ROUTES.LIST_ALL_ALERT}>
<Button
type="link"
className="periscope-btn link learn-more-link"
@@ -238,7 +239,7 @@ export default function AlertRules({
>
All Alert Rules <ArrowRight size={12} />
</Button>
</Link>
</AppLink>
</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">
<Link to={ROUTES.ALL_DASHBOARD}>
<AppLink to={ROUTES.ALL_DASHBOARD}>
<Button
type="default"
className="periscope-btn secondary"
@@ -90,7 +90,7 @@ export default function Dashboards({
>
New Dashboard
</Button>
</Link>
</AppLink>
<Button
type="link"
@@ -212,7 +212,7 @@ export default function Dashboards({
{dashboardsExist && (
<Card.Footer>
<div className="dashboards-footer home-data-card-footer">
<Link to={ROUTES.ALL_DASHBOARD}>
<AppLink 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>
</Link>
</AppLink>
</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 history from 'lib/history';
import { navigate } from 'lib/router/navigation';
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) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
navigate(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 history from 'lib/history';
import { navigate } from 'lib/router/navigation';
import cloneDeep from 'lodash-es/cloneDeep';
import { AnimatePresence, motion } from 'motion/react';
import Card from 'periscope/components/Card/Card';
@@ -371,7 +371,7 @@ export default function Home(): JSX.Element {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
navigate(ROUTES.LOGS_EXPLORER);
}
}}
>
@@ -414,7 +414,7 @@ export default function Home(): JSX.Element {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
navigate(ROUTES.TRACES_EXPLORER);
}
}}
>
@@ -457,7 +457,7 @@ export default function Home(): JSX.Element {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER);
navigate(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 history from 'lib/history';
import { navigate } from 'lib/router/navigation';
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) {
history.push(item.toRoute || '');
navigate(item.toRoute || '');
} else if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
history.push(item.toRoute || '');
navigate(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 {
@@ -164,7 +164,7 @@ export default function SavedViews({
{user?.role !== USER_ROLES.VIEWER && (
<div className="empty-actions-container">
<Link to={getStartedLink}>
<AppLink to={getStartedLink}>
<Button
type="default"
className="periscope-btn secondary"
@@ -177,7 +177,7 @@ export default function SavedViews({
>
Get Started &nbsp; <ArrowRight size={16} />
</Button>
</Link>
</AppLink>
<Button
type="link"
@@ -356,7 +356,7 @@ export default function SavedViews({
{!isSavedViewEnabled && selectedEntityViews.length > 0 && (
<Card.Footer>
<div className="services-footer home-data-card-footer">
<Link to={footerLink}>
<AppLink to={footerLink}>
<Button
type="link"
className="periscope-btn link learn-more-link"
@@ -368,7 +368,7 @@ export default function SavedViews({
>
All Views <ArrowRight size={12} />
</Button>
</Link>
</AppLink>
</div>
</Card.Footer>
)}

View File

@@ -2,7 +2,6 @@ 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';
@@ -16,7 +15,8 @@ import useGetTopLevelOperations from 'hooks/useGetTopLevelOperations';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { AppLink } from 'lib/router/AppLink';
import { navigate } from 'lib/router/navigation';
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
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
navigate(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">
<Link to="/services">
<AppLink to="/services">
<Button
type="link"
className="periscope-btn link learn-more-link"
@@ -350,7 +350,7 @@ function ServiceMetrics({
>
All Services <ArrowRight size={12} />
</Button>
</Link>
</AppLink>
</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 history from 'lib/history';
import { AppLink } from 'lib/router/AppLink';
import { navigate } from 'lib/router/navigation';
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
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
navigate(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">
<Link to="/services">
<AppLink 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>
</Link>
</AppLink>
</div>
</Card.Footer>
)}

View File

@@ -1,7 +1,6 @@
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 { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
NuqsTestingAdapter,
@@ -11,6 +10,7 @@ import {
import { AppContext } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
import { TestRouter } from 'tests/router';
import { getAppContextMock } from 'tests/test-utils';
import StatusFilter from './StatusFilter';
@@ -30,7 +30,7 @@ function renderStatusFilter({
onUrlUpdate?: OnUrlUpdateFunction;
}): ReturnType<typeof render> {
return render(
<MemoryRouter>
<TestRouter>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
@@ -45,7 +45,7 @@ function renderStatusFilter({
</Provider>
</QueryClientProvider>
</TimezoneProvider>
</MemoryRouter>,
</TestRouter>,
);
}

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { useLocation } from 'react-router-dom';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
@@ -96,7 +96,7 @@ export function K8sExpandedRow<
const [, setSelectedItemParams] = useInfraMonitoringSelectedItemParams();
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
const { safeNavigate } = useSafeNavigate();
const location = useLocation();
const location = useAppLocation();
const queryClient = useQueryClient();
const orderByParamKey = useMemo(

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