diff --git a/package.json b/package.json
index 159ca4c5f..30e646760 100644
--- a/package.json
+++ b/package.json
@@ -140,7 +140,7 @@
"react-error-boundary": "6.0.0",
"react-hook-form": "7.66.0",
"react-popper": "^2.2.5",
- "react-router-dom": "6.27.0",
+ "react-router": "7.13.0",
"rxjs": "7.8.2",
"use-konami": "^1.0.1",
"usehooks-ts": "3.1.1",
diff --git a/src/components/ConfirmLeavingPage/ConfirmLeavingPage.test.tsx b/src/components/ConfirmLeavingPage/ConfirmLeavingPage.test.tsx
index 62ce1012b..97e82568b 100644
--- a/src/components/ConfirmLeavingPage/ConfirmLeavingPage.test.tsx
+++ b/src/components/ConfirmLeavingPage/ConfirmLeavingPage.test.tsx
@@ -1,5 +1,5 @@
import React, { PropsWithChildren } from 'react';
-import { Route, Router, Routes } from 'react-router-dom';
+import { Route, Router, Routes } from 'react-router';
import { locationService } from '@grafana/runtime';
import { TextLink } from '@grafana/ui';
import { fireEvent, render, screen } from '@testing-library/react';
diff --git a/src/components/ConfirmLeavingPage/ConfirmLeavingPage.tsx b/src/components/ConfirmLeavingPage/ConfirmLeavingPage.tsx
index a1aa38782..ae5c05feb 100644
--- a/src/components/ConfirmLeavingPage/ConfirmLeavingPage.tsx
+++ b/src/components/ConfirmLeavingPage/ConfirmLeavingPage.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
-import { useLocation, useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router';
import { locationService } from '@grafana/runtime';
import { Location } from 'history';
@@ -12,7 +12,7 @@ interface ConfirmLeavingPageProps {
}
/**
- * When enabled is `true`, will block `react-router-dom` navigation with confirm modal.
+ * When enabled is `true`, will block `react-router` navigation with confirm modal.
* Native navigations are handled with native `confirm`
*
* @example
diff --git a/src/components/SceneRedirecter.test.tsx b/src/components/SceneRedirecter.test.tsx
index 59e365306..b82cc93d1 100644
--- a/src/components/SceneRedirecter.test.tsx
+++ b/src/components/SceneRedirecter.test.tsx
@@ -19,7 +19,7 @@ jest.mock('components/RunbookRedirectAlert.utils', () => ({
doRunbookRedirect: jest.fn(),
}));
-jest.mock('react-router-dom', () => ({
+jest.mock('react-router', () => ({
Navigate: ({ to, replace }: { to: string; replace: boolean }) => (
Navigate to {to}
diff --git a/src/components/SceneRedirecter.tsx b/src/components/SceneRedirecter.tsx
index d5da0b21e..fe74b3cad 100644
--- a/src/components/SceneRedirecter.tsx
+++ b/src/components/SceneRedirecter.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Navigate } from 'react-router-dom';
+import { Navigate } from 'react-router';
import { PluginPage } from '@grafana/runtime';
import { CheckAlertType, CheckAlertWithRunbookUrl } from 'types';
diff --git a/src/configPage/PluginConfigPage/PluginConfigPage.test.tsx b/src/configPage/PluginConfigPage/PluginConfigPage.test.tsx
index b9c5bdec5..9f586f0c6 100644
--- a/src/configPage/PluginConfigPage/PluginConfigPage.test.tsx
+++ b/src/configPage/PluginConfigPage/PluginConfigPage.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { MemoryRouter, Route, Routes } from 'react-router';
import { render, screen, waitFor, within } from '@testing-library/react';
import { SMDataSource } from '../../datasource/DataSource';
diff --git a/src/contexts/SMDatasourceContext.test.tsx b/src/contexts/SMDatasourceContext.test.tsx
index 34a0e45ba..217af8c70 100644
--- a/src/contexts/SMDatasourceContext.test.tsx
+++ b/src/contexts/SMDatasourceContext.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { MemoryRouter, Route, Routes } from 'react-router';
import { QueryClientProvider } from '@tanstack/react-query';
import { screen } from '@testing-library/react';
import { SM_META } from 'test/fixtures/meta';
@@ -7,6 +7,7 @@ import { ComponentWrapperProps, render } from 'test/render';
import { runTestWithoutSMAccess } from 'test/utils';
import { hasGlobalPermission } from 'utils';
+import { PLUGIN_URL_PATH } from 'routing/constants';
import { getQueryClient } from 'data/queryClient';
import { FeatureFlagProvider } from 'components/FeatureFlagProvider';
@@ -20,14 +21,14 @@ jest.mock('utils', () => {
};
});
-const Wrapper = ({ children, initialEntries, meta }: ComponentWrapperProps) => {
+const Wrapper = ({ children, initialEntries = [`${PLUGIN_URL_PATH}home`], meta }: ComponentWrapperProps) => {
return (
-
+
diff --git a/src/hooks/useConfirmBeforeUnload.test.ts b/src/hooks/useConfirmBeforeUnload.test.ts
index e76a86044..c10ea22b9 100644
--- a/src/hooks/useConfirmBeforeUnload.test.ts
+++ b/src/hooks/useConfirmBeforeUnload.test.ts
@@ -1,10 +1,10 @@
-import { useBeforeUnload } from 'react-router-dom';
+import { useBeforeUnload } from 'react-router';
import { renderHook } from '@testing-library/react';
import { useConfirmBeforeUnload } from './useConfirmBeforeUnload';
-jest.mock('react-router-dom', () => {
- const originalModule = jest.requireActual('react-router-dom');
+jest.mock('react-router', () => {
+ const originalModule = jest.requireActual('react-router');
return {
...originalModule,
useBeforeUnload: jest.fn(),
diff --git a/src/hooks/useConfirmBeforeUnload.ts b/src/hooks/useConfirmBeforeUnload.ts
index 479f78744..32ed3acb2 100644
--- a/src/hooks/useConfirmBeforeUnload.ts
+++ b/src/hooks/useConfirmBeforeUnload.ts
@@ -1,5 +1,5 @@
import { useCallback } from 'react';
-import { useBeforeUnload } from 'react-router-dom';
+import { useBeforeUnload } from 'react-router';
import { useSessionStorage } from 'usehooks-ts';
import { DEV_STORAGE_KEYS } from 'components/DevTools/DevTools.constants';
diff --git a/src/hooks/useQueryParametersState.test.tsx b/src/hooks/useQueryParametersState.test.tsx
index 563639bfa..dc2c237b0 100644
--- a/src/hooks/useQueryParametersState.test.tsx
+++ b/src/hooks/useQueryParametersState.test.tsx
@@ -1,11 +1,11 @@
-import { useLocation as useLocationFromReactRouter } from 'react-router-dom';
+import { useLocation as useLocationFromReactRouter } from 'react-router';
import { locationService } from '@grafana/runtime';
import { act, renderHook } from '@testing-library/react';
import { useQueryParametersState } from './useQueryParametersState';
-jest.mock('react-router-dom', () => ({
- ...jest.requireActual('react-router-dom'),
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
useLocation: jest.fn(),
}));
diff --git a/src/hooks/useQueryParametersState.ts b/src/hooks/useQueryParametersState.ts
index fad90e338..b767f5910 100644
--- a/src/hooks/useQueryParametersState.ts
+++ b/src/hooks/useQueryParametersState.ts
@@ -1,5 +1,5 @@
import { useCallback, useMemo } from 'react';
-import { useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router';
import { locationService } from '@grafana/runtime';
import { useURLSearchParams } from 'hooks/useURLSearchParams';
diff --git a/src/hooks/useURLSearchParams.ts b/src/hooks/useURLSearchParams.ts
index 67fc2821e..91abdd314 100644
--- a/src/hooks/useURLSearchParams.ts
+++ b/src/hooks/useURLSearchParams.ts
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
-import { useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router';
export function useURLSearchParams() {
const { search } = useLocation();
diff --git a/src/page/CheckList/CheckList.tsx b/src/page/CheckList/CheckList.tsx
index e759030f8..ba8bcd8ef 100644
--- a/src/page/CheckList/CheckList.tsx
+++ b/src/page/CheckList/CheckList.tsx
@@ -1,5 +1,5 @@
import React, { useMemo, useState } from 'react';
-import { useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { locationService, PluginPage } from '@grafana/runtime';
import { Pagination, useStyles2 } from '@grafana/ui';
diff --git a/src/page/ChecksPage.test.tsx b/src/page/ChecksPage.test.tsx
index c4da7e5d5..d10bebafe 100644
--- a/src/page/ChecksPage.test.tsx
+++ b/src/page/ChecksPage.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { MemoryRouter, Route, Routes } from 'react-router';
import { QueryClientProvider } from '@tanstack/react-query';
import { screen, waitFor, within } from '@testing-library/react';
import { DataTestIds } from 'test/dataTestIds';
diff --git a/src/page/ConfigPageLayout/ConfigPageLayout.test.tsx b/src/page/ConfigPageLayout/ConfigPageLayout.test.tsx
index 07cdc97b5..9672f0cca 100644
--- a/src/page/ConfigPageLayout/ConfigPageLayout.test.tsx
+++ b/src/page/ConfigPageLayout/ConfigPageLayout.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { MemoryRouter, Route, Routes } from 'react-router';
import { render } from '@testing-library/react';
import { AppRoutes } from 'routing/types';
diff --git a/src/page/ConfigPageLayout/ConfigPageLayout.tsx b/src/page/ConfigPageLayout/ConfigPageLayout.tsx
index 0b3a5c071..0314dcaa5 100644
--- a/src/page/ConfigPageLayout/ConfigPageLayout.tsx
+++ b/src/page/ConfigPageLayout/ConfigPageLayout.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react';
-import { matchPath, Outlet, useLocation } from 'react-router-dom';
+import { matchPath, Outlet, useLocation } from 'react-router';
import { NavModelItem } from '@grafana/data';
import { PluginPage } from '@grafana/runtime';
diff --git a/src/page/DashboardPage.tsx b/src/page/DashboardPage.tsx
index 67857d947..f2f17ad5f 100644
--- a/src/page/DashboardPage.tsx
+++ b/src/page/DashboardPage.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams } from 'react-router';
import { Spinner } from '@grafana/ui';
import { CheckPageParams, CheckType } from 'types';
diff --git a/src/page/EditCheck/EditCheckV2.tsx b/src/page/EditCheck/EditCheckV2.tsx
index 3980aaa5b..08164fbab 100644
--- a/src/page/EditCheck/EditCheckV2.tsx
+++ b/src/page/EditCheck/EditCheckV2.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams } from 'react-router';
import { GrafanaTheme2 } from '@grafana/data';
import { PluginPage } from '@grafana/runtime';
import { Alert, Button, LinkButton, Modal, Text, useStyles2 } from '@grafana/ui';
diff --git a/src/page/EditProbe/EditProbe.tsx b/src/page/EditProbe/EditProbe.tsx
index 3ee5bb850..25344eca2 100644
--- a/src/page/EditProbe/EditProbe.tsx
+++ b/src/page/EditProbe/EditProbe.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams } from 'react-router';
import { locationService, PluginPage } from '@grafana/runtime';
import { LinkButton, TextLink } from '@grafana/ui';
diff --git a/src/page/NewCheck/NewCheckV2.tsx b/src/page/NewCheck/NewCheckV2.tsx
index 08b84aa9e..378b06b09 100644
--- a/src/page/NewCheck/NewCheckV2.tsx
+++ b/src/page/NewCheck/NewCheckV2.tsx
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
-import { useLocation, useParams, useSearchParams } from 'react-router-dom';
+import { useLocation, useParams, useSearchParams } from 'react-router';
import { GrafanaTheme2 } from '@grafana/data';
import { locationService, PluginPage } from '@grafana/runtime';
import { TextLink, useStyles2 } from '@grafana/ui';
diff --git a/src/page/NotFound/CheckNotFound.tsx b/src/page/NotFound/CheckNotFound.tsx
index 5a241f71d..337ced308 100644
--- a/src/page/NotFound/CheckNotFound.tsx
+++ b/src/page/NotFound/CheckNotFound.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams } from 'react-router';
import { TextLink } from '@grafana/ui';
import { createNavModel } from 'utils';
diff --git a/src/routing/InitialisedRouter.tsx b/src/routing/InitialisedRouter.tsx
index 51698643f..78764c6ad 100644
--- a/src/routing/InitialisedRouter.tsx
+++ b/src/routing/InitialisedRouter.tsx
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
-import { Navigate, Route, Routes } from 'react-router-dom';
+import { Navigate, Route, Routes } from 'react-router';
import { TextLink } from '@grafana/ui';
import { FeatureName } from 'types';
@@ -57,7 +57,7 @@ export const InitialisedRouter = () => {
return (
- } />
+ } />
{
} />
}>
} />
- } />
+ } />
{/* TODO: Create 404 instead of navigating to home(?) */}
- } />
+ } />
);
};
diff --git a/src/routing/utils.ts b/src/routing/utils.ts
index 1a2495ce2..d14046da2 100644
--- a/src/routing/utils.ts
+++ b/src/routing/utils.ts
@@ -1,4 +1,4 @@
-import { generatePath, type PathParam } from 'react-router-dom';
+import { generatePath, type PathParam } from 'react-router';
import { CheckType, CheckTypeGroup } from 'types';
import { PLUGIN_URL_PATH } from 'routing/constants';
diff --git a/src/test/helpers/TestRouteInfo.tsx b/src/test/helpers/TestRouteInfo.tsx
index 9dbede7a3..9eb338134 100644
--- a/src/test/helpers/TestRouteInfo.tsx
+++ b/src/test/helpers/TestRouteInfo.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router';
import { DataTestIds } from '../dataTestIds';
diff --git a/src/test/mocks/@grafana/runtime.tsx b/src/test/mocks/@grafana/runtime.tsx
index 7bfbc04fe..c5a6f8223 100644
--- a/src/test/mocks/@grafana/runtime.tsx
+++ b/src/test/mocks/@grafana/runtime.tsx
@@ -41,7 +41,12 @@ jest.mock('@grafana/runtime', () => {
};
const navigate = (path: PathArg, action: string) => {
- const next: Location = { ...parsePath(path), state: null, key: Math.random().toString(36).slice(2) };
+ const parsed = parsePath(path);
+ // Prevent infinite loops by skipping navigation to the same location
+ if (location.pathname === parsed.pathname && location.search === parsed.search) {
+ return;
+ }
+ const next: Location = { ...parsed, state: null, key: Math.random().toString(36).slice(2) };
for (const blocker of blockers) {
if (blocker(next, action) === false) {
return;
diff --git a/src/test/render.tsx b/src/test/render.tsx
index 706a02f28..2bfe04f84 100644
--- a/src/test/render.tsx
+++ b/src/test/render.tsx
@@ -1,5 +1,5 @@
import React, { PropsWithChildren, type ReactElement, type ReactNode } from 'react';
-import { Route, Router, Routes } from 'react-router-dom';
+import { Route, Router, Routes } from 'react-router';
import { QueryClientProvider } from '@tanstack/react-query';
import { AppPluginMeta } from '@grafana/data';
import { locationService, LocationServiceProvider } from '@grafana/runtime';
diff --git a/yarn.lock b/yarn.lock
index a6ced082a..3065b2572 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2443,11 +2443,6 @@
redux-thunk "^3.1.0"
reselect "^5.1.0"
-"@remix-run/router@1.20.0":
- version "1.20.0"
- resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.20.0.tgz#03554155b45d8b529adf635b2f6ad1165d70d8b4"
- integrity sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==
-
"@remix-run/router@1.23.0":
version "1.23.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.0.tgz#35390d0e7779626c026b11376da6789eb8389242"
@@ -4280,7 +4275,7 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
-cookie@^1.0.2:
+cookie@^1.0.1, cookie@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c"
integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==
@@ -9024,14 +9019,6 @@ react-router-dom@5.3.4:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
-react-router-dom@6.27.0:
- version "6.27.0"
- resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.27.0.tgz#8d7972a425fd75f91c1e1ff67e47240c5752dc3f"
- integrity sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==
- dependencies:
- "@remix-run/router" "1.20.0"
- react-router "6.27.0"
-
react-router@5.3.4:
version "5.3.4"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5"
@@ -9047,13 +9034,6 @@ react-router@5.3.4:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
-react-router@6.27.0:
- version "6.27.0"
- resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.27.0.tgz#db292474926c814c996c0ff3ef0162d1f9f60ed4"
- integrity sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==
- dependencies:
- "@remix-run/router" "1.20.0"
-
react-router@6.30.0:
version "6.30.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.30.0.tgz#9789d775e63bc0df60f39ced77c8c41f1e01ff90"
@@ -9061,6 +9041,14 @@ react-router@6.30.0:
dependencies:
"@remix-run/router" "1.23.0"
+react-router@7.13.0:
+ version "7.13.0"
+ resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.13.0.tgz#de9484aee764f4f65b93275836ff5944d7f5bd3b"
+ integrity sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==
+ dependencies:
+ cookie "^1.0.1"
+ set-cookie-parser "^2.6.0"
+
react-select@5.10.2:
version "5.10.2"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.10.2.tgz#8dffc69dfd7d74684d9613e6eb27204e3b99e127"
@@ -9549,6 +9537,11 @@ serialize-javascript@^6.0.2:
dependencies:
randombytes "^2.1.0"
+set-cookie-parser@^2.6.0:
+ version "2.7.2"
+ resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68"
+ integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==
+
set-function-length@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"