sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #07589
[Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
Peter Makowski has proposed merging ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main.
Commit message:
use strict router types
- disallow react-router-dom import and suggest @/router
- requires Links to use known route types as specified in the router config
- allow any string in ExternalLink
- remove auto import of react-router-dom
Requested reviews:
MAAS Committers (maas-committers)
For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/441882
This requires Links to use specific route types that we know exist as they're specified in the router config.
This helps avoid incorrect use of Link, useNavigation, and other types of react-router components which could lead to links directing to an invalid route.
One exception is an ExternalLink component, which can be used with any string.
--
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main.
diff --git a/frontend/.eslintrc-auto-import.json b/frontend/.eslintrc-auto-import.json
index 172268c..6ca7e59 100644
--- a/frontend/.eslintrc-auto-import.json
+++ b/frontend/.eslintrc-auto-import.json
@@ -1,11 +1,5 @@
{
"globals": {
- "Link": true,
- "NavLink": true,
- "Navigate": true,
- "Outlet": true,
- "Route": true,
- "Routes": true,
"afterAll": true,
"afterEach": true,
"assert": true,
@@ -27,25 +21,13 @@
"useDebugValue": true,
"useDeferredValue": true,
"useEffect": true,
- "useHref": true,
"useId": true,
"useImperativeHandle": true,
- "useInRouterContext": true,
"useInsertionEffect": true,
"useLayoutEffect": true,
- "useLinkClickHandler": true,
- "useLocation": true,
"useMemo": true,
- "useNavigate": true,
- "useNavigationType": true,
- "useOutlet": true,
- "useOutletContext": true,
- "useParams": true,
"useReducer": true,
"useRef": true,
- "useResolvedPath": true,
- "useRoutes": true,
- "useSearchParams": true,
"useState": true,
"useSyncExternalStore": true,
"useTransition": true,
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
index 722eb61..870950e 100644
--- a/frontend/.eslintrc.js
+++ b/frontend/.eslintrc.js
@@ -74,6 +74,13 @@ module.exports = {
message: "Avoid enums, use const or string literal instead",
},
],
+ "no-restricted-imports": [
+ "error",
+ {
+ name: "react-router-dom",
+ message: 'Use strictly typed "@/router" import instead of "react-router-dom"',
+ },
+ ],
"@typescript-eslint/consistent-type-imports": 2,
"import/namespace": "off",
"import/no-named-as-default": 0,
diff --git a/frontend/auto-imports.d.ts b/frontend/auto-imports.d.ts
index 16bd2ed..296822f 100644
--- a/frontend/auto-imports.d.ts
+++ b/frontend/auto-imports.d.ts
@@ -4,12 +4,6 @@
// Generated by unplugin-auto-import
export {}
declare global {
- const Link: typeof import('react-router-dom')['Link']
- const NavLink: typeof import('react-router-dom')['NavLink']
- const Navigate: typeof import('react-router-dom')['Navigate']
- const Outlet: typeof import('react-router-dom')['Outlet']
- const Route: typeof import('react-router-dom')['Route']
- const Routes: typeof import('react-router-dom')['Routes']
const afterAll: typeof import('vitest')['afterAll']
const afterEach: typeof import('vitest')['afterEach']
const assert: typeof import('vitest')['assert']
@@ -31,25 +25,13 @@ declare global {
const useDebugValue: typeof import('react')['useDebugValue']
const useDeferredValue: typeof import('react')['useDeferredValue']
const useEffect: typeof import('react')['useEffect']
- const useHref: typeof import('react-router-dom')['useHref']
const useId: typeof import('react')['useId']
const useImperativeHandle: typeof import('react')['useImperativeHandle']
- const useInRouterContext: typeof import('react-router-dom')['useInRouterContext']
const useInsertionEffect: typeof import('react')['useInsertionEffect']
const useLayoutEffect: typeof import('react')['useLayoutEffect']
- const useLinkClickHandler: typeof import('react-router-dom')['useLinkClickHandler']
- const useLocation: typeof import('react-router-dom')['useLocation']
const useMemo: typeof import('react')['useMemo']
- const useNavigate: typeof import('react-router-dom')['useNavigate']
- const useNavigationType: typeof import('react-router-dom')['useNavigationType']
- const useOutlet: typeof import('react-router-dom')['useOutlet']
- const useOutletContext: typeof import('react-router-dom')['useOutletContext']
- const useParams: typeof import('react-router-dom')['useParams']
const useReducer: typeof import('react')['useReducer']
const useRef: typeof import('react')['useRef']
- const useResolvedPath: typeof import('react-router-dom')['useResolvedPath']
- const useRoutes: typeof import('react-router-dom')['useRoutes']
- const useSearchParams: typeof import('react-router-dom')['useSearchParams']
const useState: typeof import('react')['useState']
const useSyncExternalStore: typeof import('react')['useSyncExternalStore']
const useTransition: typeof import('react')['useTransition']
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index cee9a8b..d4b6fc2 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,11 +1,11 @@
import "./App.scss";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { AppContextProvider, AuthContextProvider } from "./context";
import routes from "./routes";
import apiClient from "@/api";
+import { createBrowserRouter, RouterProvider } from "@/router";
const queryClient = new QueryClient();
const router = createBrowserRouter(routes);
diff --git a/frontend/src/base/routesConfig.ts b/frontend/src/base/routesConfig.ts
index a92033e..2bc2dbc 100644
--- a/frontend/src/base/routesConfig.ts
+++ b/frontend/src/base/routesConfig.ts
@@ -3,6 +3,10 @@ export const protectedRoutes = {
path: "/sites",
title: "Regions",
},
+ settings: {
+ path: "/settings",
+ title: "",
+ },
requests: {
path: "/settings/requests",
title: "Requests",
@@ -11,13 +15,19 @@ export const protectedRoutes = {
path: "/settings/tokens",
title: "Tokens",
},
-};
+ logout: {
+ path: "/logout",
+ title: "",
+ },
+} as const;
+
export const publicRoutes = {
+ index: { path: "/", title: "Homepage" },
login: {
path: "/login",
title: "Login",
},
-};
+} as const;
export const routesConfig = { ...publicRoutes, ...protectedRoutes } as const;
diff --git a/frontend/src/components/EnrollmentActions/EnrollmentNotification/EnrollmentNotification.tsx b/frontend/src/components/EnrollmentActions/EnrollmentNotification/EnrollmentNotification.tsx
index c9c3c48..19c3571 100644
--- a/frontend/src/components/EnrollmentActions/EnrollmentNotification/EnrollmentNotification.tsx
+++ b/frontend/src/components/EnrollmentActions/EnrollmentNotification/EnrollmentNotification.tsx
@@ -1,8 +1,8 @@
import { Notification } from "@canonical/react-components";
import pluralize from "pluralize";
-import { useNavigate } from "react-router-dom";
import type { PostEnrollmentRequestsData } from "@/api/handlers";
+import { useNavigate } from "@/router";
const EnrollmentNotification = ({ accept, ids }: Partial<PostEnrollmentRequestsData>) => {
const navigate = useNavigate();
diff --git a/frontend/src/components/ExternalLink/ExternalLink.tsx b/frontend/src/components/ExternalLink/ExternalLink.tsx
index 4c3c869..a7880f8 100644
--- a/frontend/src/components/ExternalLink/ExternalLink.tsx
+++ b/frontend/src/components/ExternalLink/ExternalLink.tsx
@@ -1,8 +1,9 @@
+/* eslint-disable no-restricted-imports */
import type { LinkProps } from "react-router-dom";
import { Link } from "react-router-dom";
-const ExternalLink = ({ to, children }: LinkProps) => (
- <Link rel="noreferrer noopener" target="_blank" to={to}>
+const ExternalLink = ({ children, ...props }: LinkProps) => (
+ <Link {...props} rel="noreferrer noopener" target="_blank">
{children}
</Link>
);
diff --git a/frontend/src/components/LoginForm/LoginForm.tsx b/frontend/src/components/LoginForm/LoginForm.tsx
index c53503a..1ec8e96 100644
--- a/frontend/src/components/LoginForm/LoginForm.tsx
+++ b/frontend/src/components/LoginForm/LoginForm.tsx
@@ -3,10 +3,10 @@ import { useCallback, useEffect } from "react";
import { Notification, Col, Row, Strip, Input, useId, Label, Card, Button } from "@canonical/react-components";
import type { FormikHelpers } from "formik";
import { Field, Form, Formik } from "formik";
-import { useNavigate, useSearchParams } from "react-router-dom";
import * as Yup from "yup";
import { useAuthContext } from "@/context";
+import { useNavigate, useSearchParams } from "@/router";
const initialValues = {
username: "",
diff --git a/frontend/src/components/MainLayout/MainLayout.tsx b/frontend/src/components/MainLayout/MainLayout.tsx
index 93364ab..82b94d1 100644
--- a/frontend/src/components/MainLayout/MainLayout.tsx
+++ b/frontend/src/components/MainLayout/MainLayout.tsx
@@ -3,7 +3,6 @@ import { useEffect } from "react";
import { Col, Row, Strip, useOnEscapePressed, usePrevious } from "@canonical/react-components";
import classNames from "classnames";
-import { matchPath, Outlet, useLocation } from "react-router-dom";
import { routesConfig } from "@/base/routesConfig";
import type { RoutePath } from "@/base/routesConfig";
@@ -14,6 +13,7 @@ import RemoveRegions from "@/components/RemoveRegions";
import SecondaryNavigation from "@/components/SecondaryNavigation";
import { useAppContext } from "@/context";
import TokensCreate from "@/pages/tokens/create";
+import { matchPath, Outlet, useLocation } from "@/router";
export const sidebarLabels: Record<"removeRegions" | "createToken", string> = {
removeRegions: "Remove regions",
@@ -82,7 +82,7 @@ const MainLayout: React.FC = () => {
}
}, [pathname, previousPathname, setSidebar]);
- const isSideNavVisible = matchPath("settings/*", pathname);
+ const isSideNavVisible = matchPath("/settings/*", pathname);
return (
<>
diff --git a/frontend/src/components/Navigation/Navigation.test.tsx b/frontend/src/components/Navigation/Navigation.test.tsx
index 8a760be..b921281 100644
--- a/frontend/src/components/Navigation/Navigation.test.tsx
+++ b/frontend/src/components/Navigation/Navigation.test.tsx
@@ -1,7 +1,6 @@
-import { MemoryRouter } from "react-router-dom";
-
import Navigation, { navItems, settingsNavItems } from "./Navigation";
+import { MemoryRouter } from "@/router";
import { render, renderWithMemoryRouter, screen, userEvent } from "@/test-utils";
describe("Navigation", () => {
diff --git a/frontend/src/components/Navigation/Navigation.tsx b/frontend/src/components/Navigation/Navigation.tsx
index e66ff83..63c4bbe 100644
--- a/frontend/src/components/Navigation/Navigation.tsx
+++ b/frontend/src/components/Navigation/Navigation.tsx
@@ -1,6 +1,5 @@
import { Button } from "@canonical/react-components";
import classNames from "classnames";
-import { useLocation } from "react-router-dom";
import useLocalStorageState from "use-local-storage-state";
import NavigationBanner from "./NavigationBanner";
@@ -8,6 +7,8 @@ import NavigationCollapseToggle from "./NavigationCollapseToggle";
import NavigationList from "./NavigationList";
import type { NavLink } from "./types";
+import { useLocation } from "@/router";
+
export const navItems: NavLink[] = [
{
label: "Regions",
@@ -24,7 +25,7 @@ export const settingsNavItems: NavLink[] = [
},
];
-const navItemsAccount = [{ label: "Log out", url: "/logout" }];
+const navItemsAccount: NavLink[] = [{ label: "Log out", url: "/logout" }];
export const navItemsBottom: NavLink[] = [
{ external: true, icon: "information", label: "Documentation", url: "https://maas.io/docs" },
diff --git a/frontend/src/components/Navigation/NavigationBanner/NavigationBanner.test.tsx b/frontend/src/components/Navigation/NavigationBanner/NavigationBanner.test.tsx
index 5bce1bb..d597efc 100644
--- a/frontend/src/components/Navigation/NavigationBanner/NavigationBanner.test.tsx
+++ b/frontend/src/components/Navigation/NavigationBanner/NavigationBanner.test.tsx
@@ -1,7 +1,6 @@
-import { BrowserRouter } from "react-router-dom";
-
import NavigationBanner from "./NavigationBanner";
+import { BrowserRouter } from "@/router";
import { screen, render } from "@/test-utils";
describe("Navigation Banner", () => {
diff --git a/frontend/src/components/Navigation/NavigationBanner/NavigationBanner.tsx b/frontend/src/components/Navigation/NavigationBanner/NavigationBanner.tsx
index 61e9607..b72af83 100644
--- a/frontend/src/components/Navigation/NavigationBanner/NavigationBanner.tsx
+++ b/frontend/src/components/Navigation/NavigationBanner/NavigationBanner.tsx
@@ -1,10 +1,9 @@
-import { Link, useLocation } from "react-router-dom";
-
import { isSelected } from "@/components/Navigation/utils";
+import { Link, useLocation } from "@/router";
const NavigationBanner = ({ children }: { children?: React.ReactNode }): JSX.Element => {
const location = useLocation();
- const homepageLink = { url: "/", label: "Homepage" };
+ const homepageLink = { url: "/", label: "Homepage" } as const;
return (
<>
<Link
diff --git a/frontend/src/components/Navigation/NavigationItem/NavigationItem.tsx b/frontend/src/components/Navigation/NavigationItem/NavigationItem.tsx
index f28ce91..79cd8d3 100644
--- a/frontend/src/components/Navigation/NavigationItem/NavigationItem.tsx
+++ b/frontend/src/components/Navigation/NavigationItem/NavigationItem.tsx
@@ -1,45 +1,58 @@
import { useId } from "react";
import { Icon } from "@canonical/react-components";
-import { Link } from "react-router-dom";
+import type { RoutePath } from "@/base/routesConfig";
+import ExternalLink from "@/components/ExternalLink/ExternalLink";
import type { NavLink } from "@/components/Navigation/types";
import { isSelected } from "@/components/Navigation/utils";
+import { Link } from "@/router";
type Props = {
navLink: NavLink;
- path: string;
+ path: RoutePath;
};
+const LinkContent = ({ navLink }: { navLink: NavLink }) => (
+ <>
+ {navLink.icon ? (
+ typeof navLink.icon === "string" ? (
+ <Icon className="p-side-navigation__icon" light name={navLink.icon} />
+ ) : (
+ <>{navLink.icon}</>
+ )
+ ) : null}
+ <span className="p-side-navigation__label">{navLink.label}</span>
+ </>
+);
+
const NavigationItem = ({ navLink, path }: Props): JSX.Element => {
const id = useId();
+ const selected = !navLink.external ? isSelected(path, navLink) : false;
+ const linkProps = {
+ className: "p-side-navigation__link",
+ id: `${navLink.label}-${id}`,
+ onClick: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
+ // removing the focus from the link element after click
+ // this allows the side navigation to collapse on mouseleave
+ event.currentTarget.blur();
+ },
+ };
+
return (
<li
aria-labelledby={`${navLink.label}-${id}`}
- className={`p-side-navigation__item${isSelected(path, navLink) ? " is-selected" : ""}`}
+ className={`p-side-navigation__item${selected ? " is-selected" : ""}`}
>
- <Link
- aria-current={!navLink.external && isSelected(path, navLink) ? "page" : undefined}
- className="p-side-navigation__link"
- id={`${navLink.label}-${id}`}
- onClick={(event) => {
- // removing the focus from the link element after click
- // this allows the side navigation to collapse on mouseleave
- event.currentTarget.blur();
- }}
- rel={navLink.external ? "noreferrer noopener" : ""}
- target={navLink.external ? "_blank" : ""}
- to={navLink.url}
- >
- {navLink.icon ? (
- typeof navLink.icon === "string" ? (
- <Icon className="p-side-navigation__icon" light name={navLink.icon} />
- ) : (
- <>{navLink.icon}</>
- )
- ) : null}
- <span className="p-side-navigation__label">{navLink.label}</span>
- </Link>
+ {!navLink.external ? (
+ <Link {...linkProps} aria-current={isSelected(path, navLink) ? "page" : undefined} to={navLink.url}>
+ <LinkContent navLink={navLink} />
+ </Link>
+ ) : (
+ <ExternalLink {...linkProps} to={navLink.url}>
+ <LinkContent navLink={navLink} />
+ </ExternalLink>
+ )}
</li>
);
};
diff --git a/frontend/src/components/Navigation/NavigationList/NavigationList.tsx b/frontend/src/components/Navigation/NavigationList/NavigationList.tsx
index 0b9d20a..9e78639 100644
--- a/frontend/src/components/Navigation/NavigationList/NavigationList.tsx
+++ b/frontend/src/components/Navigation/NavigationList/NavigationList.tsx
@@ -3,6 +3,7 @@ import { useId, useMemo } from "react";
import { Icon } from "@canonical/react-components";
import classNames from "classnames";
+import type { RoutePath } from "@/base/routesConfig";
import NavigationItem from "@/components/Navigation/NavigationItem/NavigationItem";
import type { NavGroup, NavItem } from "@/components/Navigation/types";
import { isNavGroup, isSelected } from "@/components/Navigation/utils";
@@ -12,7 +13,7 @@ type Props = {
isDark?: boolean;
hasIcons?: boolean;
items: NavItem[];
- path: string;
+ path: RoutePath;
};
const NavigationItemGroup = ({ group, path }: { group: NavGroup } & Pick<Props, "path">) => {
diff --git a/frontend/src/components/Navigation/types.ts b/frontend/src/components/Navigation/types.ts
index d1fe57a..360a88d 100644
--- a/frontend/src/components/Navigation/types.ts
+++ b/frontend/src/components/Navigation/types.ts
@@ -1,11 +1,22 @@
-export type NavLink = {
+import type { RoutePath } from "@/base/routesConfig";
+
+type BaseNavLink = {
adminOnly?: boolean;
external?: boolean;
- highlight?: string | string[];
icon?: string | React.ReactNode;
label: string;
+};
+export type ExternalNavLink = BaseNavLink & {
+ external: true;
url: string;
+ highlight?: RoutePath | RoutePath[];
+};
+export type LocalNavLink = BaseNavLink & {
+ external?: false;
+ url: RoutePath;
+ highlight?: RoutePath | RoutePath[];
};
+export type NavLink = ExternalNavLink | LocalNavLink;
export type NavGroup = {
navLinks: NavLink[];
diff --git a/frontend/src/components/Navigation/utils.ts b/frontend/src/components/Navigation/utils.ts
index a32b44f..89c010c 100644
--- a/frontend/src/components/Navigation/utils.ts
+++ b/frontend/src/components/Navigation/utils.ts
@@ -1,10 +1,14 @@
-import { matchPath } from "react-router-dom";
-
import type { NavLink, NavItem, NavGroup } from "./types";
-export const isSelected = (path: string, link: NavLink): boolean => {
+import type { RoutePath } from "@/base/routesConfig";
+import { matchPath } from "@/router";
+
+export const isSelected = (path: RoutePath, link: NavLink): boolean => {
+ if (link.external) {
+ return false;
+ }
// Use the provided highlight(s) or just use the url.
- let highlights = link.highlight || link.url;
+ let highlights: RoutePath | RoutePath[] = link.highlight || link.url;
// If the provided highlights aren't an array then make them one so that we
// can loop over them.
if (!Array.isArray(highlights)) {
diff --git a/frontend/src/components/NoRegions/NoRegions.tsx b/frontend/src/components/NoRegions/NoRegions.tsx
index a97456e..21fa552 100644
--- a/frontend/src/components/NoRegions/NoRegions.tsx
+++ b/frontend/src/components/NoRegions/NoRegions.tsx
@@ -1,9 +1,8 @@
-import { Link } from "react-router-dom";
-
import docsUrls from "@/base/docsUrls";
import ExternalLink from "@/components/ExternalLink";
import TableCaption from "@/components/TableCaption";
import { useRequestsCountQuery } from "@/hooks/api";
+import { Link } from "@/router";
const NoRegions = () => {
const { data, isSuccess } = useRequestsCountQuery();
@@ -21,7 +20,7 @@ const NoRegions = () => {
</ExternalLink>
</TableCaption.Description>
<TableCaption.Description>
- <Link className="p-button--positive" to="/requests">
+ <Link className="p-button--positive" to="/settings/requests">
Go to Requests Page
</Link>
</TableCaption.Description>
@@ -36,7 +35,7 @@ const NoRegions = () => {
</ExternalLink>
</TableCaption.Description>
<TableCaption.Description>
- <Link className="p-button--positive" to="/tokens">
+ <Link className="p-button--positive" to="/settings/tokens">
Go to Tokens page
</Link>
</TableCaption.Description>
diff --git a/frontend/src/components/SecondaryNavigation/SecondaryNavigation.tsx b/frontend/src/components/SecondaryNavigation/SecondaryNavigation.tsx
index 669d917..c1a47a8 100644
--- a/frontend/src/components/SecondaryNavigation/SecondaryNavigation.tsx
+++ b/frontend/src/components/SecondaryNavigation/SecondaryNavigation.tsx
@@ -1,10 +1,12 @@
import classNames from "classnames";
-import type { Location } from "react-router-dom";
-import { matchPath, Link, useLocation } from "react-router-dom";
+
+import type { RoutePath } from "@/base/routesConfig";
+import { matchPath, Link, useLocation } from "@/router";
+import type { Location } from "@/router";
export type NavItem = {
label: string;
- path?: string;
+ path?: RoutePath;
items?: NavItem[];
};
@@ -81,7 +83,7 @@ const SideNavigationItem = ({ item }: ItemProps) => {
);
};
-export const secondaryNavItems = [
+export const secondaryNavItems: NavItem[] = [
{
label: "Enrollment",
items: [
diff --git a/frontend/src/components/TokensList/TokensList.tsx b/frontend/src/components/TokensList/TokensList.tsx
index d2e6da9..745ad16 100644
--- a/frontend/src/components/TokensList/TokensList.tsx
+++ b/frontend/src/components/TokensList/TokensList.tsx
@@ -1,7 +1,6 @@
import { useState, useEffect } from "react";
import { Accordion, Button, Col, Row } from "@canonical/react-components";
-import { Link } from "react-router-dom";
import TokensTable from "./components/TokensTable/TokensTable";
@@ -12,6 +11,7 @@ import PaginationBar from "@/components/base/PaginationBar";
import { useAppContext } from "@/context";
import { useTokensQuery } from "@/hooks/api";
import usePagination from "@/hooks/usePagination";
+import { Link } from "@/router";
const DEFAULT_PAGE_SIZE = 50;
diff --git a/frontend/src/pages/404.tsx b/frontend/src/pages/404.tsx
index eded7b7..13f5398 100644
--- a/frontend/src/pages/404.tsx
+++ b/frontend/src/pages/404.tsx
@@ -1,5 +1,6 @@
import { Col, Row, Strip } from "@canonical/react-components";
-import { useLocation } from "react-router-dom";
+
+import { useLocation } from "@/router";
const NotFound = () => {
const location = useLocation();
diff --git a/frontend/src/pages/logout.tsx b/frontend/src/pages/logout.tsx
index 6b09b82..5ee16be 100644
--- a/frontend/src/pages/logout.tsx
+++ b/frontend/src/pages/logout.tsx
@@ -1,4 +1,5 @@
import { useAuthContext } from "@/context";
+import { useNavigate } from "@/router";
const Logout = () => {
const navigate = useNavigate();
diff --git a/frontend/src/pages/settings/index.tsx b/frontend/src/pages/settings/index.tsx
index 478f99e..aef80b8 100644
--- a/frontend/src/pages/settings/index.tsx
+++ b/frontend/src/pages/settings/index.tsx
@@ -1,4 +1,4 @@
-import { Outlet } from "react-router-dom";
+import { Outlet } from "@/router";
const Settings = () => {
return <Outlet />;
diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx
new file mode 100644
index 0000000..3934e2b
--- /dev/null
+++ b/frontend/src/router/index.tsx
@@ -0,0 +1,64 @@
+/* eslint-disable no-restricted-imports */
+
+import type {
+ Path as BasePath,
+ LinkProps as BaseLinkProps,
+ Location as BaseLocation,
+ MemoryRouterProps as BaseMemoryRouterProps,
+ PathPattern,
+ NavigateOptions as BaseNavigateOptions,
+ RedirectFunction as BaseRedirectFunction,
+} from "react-router-dom";
+import {
+ createSearchParams as baseCreateSearchParams,
+ Link as BaseLink,
+ useNavigate as baseUseNavigate,
+ useSearchParams as baseUseSearchParams,
+ matchPath as baseMatchPath,
+ useLocation as baseUseLocation,
+ MemoryRouter as BaseMemoryRouter,
+ redirect as baseRedirect,
+} from "react-router-dom";
+
+import type { RoutePath } from "@/base/routesConfig";
+
+export type Path = Partial<Exclude<BasePath, "pathname"> & { pathname: RoutePath }>;
+export type To = RoutePath | Path;
+export type Location = Exclude<BaseLocation, "pathname"> & { pathname: RoutePath };
+export type LinkProps = Exclude<BaseLinkProps, "to"> & {
+ to: To;
+};
+export type InitialEntry = RoutePath | Partial<Location>;
+export type MemoryRouterProps = BaseMemoryRouterProps & { initialEntries?: InitialEntry[] };
+
+export const Link: (props: LinkProps) => ReturnType<typeof BaseLink> = BaseLink;
+
+type UseLocation = () => Location;
+export const useLocation = baseUseLocation as UseLocation;
+export const createSearchParams = baseCreateSearchParams;
+
+type NavigateOptions = BaseNavigateOptions;
+export interface NavigateFunction {
+ (to: To, options?: NavigateOptions): void;
+ (delta: number): void;
+}
+export const useNavigate = baseUseNavigate;
+export const MemoryRouter = BaseMemoryRouter;
+export const useSearchParams = baseUseSearchParams;
+type MatchPath = (
+ pattern: PathPattern<string> | RoutePath | `${RoutePath}/*`,
+ pathname: string,
+) => ReturnType<typeof baseMatchPath>;
+export const matchPath = baseMatchPath as MatchPath;
+export type RedirectFunction = (url: RoutePath, init?: number | ResponseInit) => ReturnType<BaseRedirectFunction>;
+export const redirect = baseRedirect as RedirectFunction;
+
+export {
+ createBrowserRouter,
+ createRoutesFromElements,
+ Route,
+ Outlet,
+ BrowserRouter,
+ createMemoryRouter,
+ RouterProvider,
+} from "react-router-dom";
diff --git a/frontend/src/routes.test.tsx b/frontend/src/routes.test.tsx
index 418038f..da268ea 100644
--- a/frontend/src/routes.test.tsx
+++ b/frontend/src/routes.test.tsx
@@ -1,8 +1,7 @@
-import { createMemoryRouter, RouterProvider } from "react-router-dom";
-
import { allResolvers } from "./mocks/resolvers";
import { routesConfig } from "@/base/routesConfig";
+import { createMemoryRouter, RouterProvider } from "@/router";
import routes from "@/routes";
import { render, screen, waitFor, setupServer } from "@/test-utils";
diff --git a/frontend/src/routes/RequireLogin/RequireLogin.tsx b/frontend/src/routes/RequireLogin/RequireLogin.tsx
index 3c63157..451af91 100644
--- a/frontend/src/routes/RequireLogin/RequireLogin.tsx
+++ b/frontend/src/routes/RequireLogin/RequireLogin.tsx
@@ -1,8 +1,7 @@
import { useEffect } from "react";
-import { createSearchParams, useLocation, useNavigate } from "react-router-dom";
-
import { useAuthContext } from "@/context";
+import { createSearchParams, useLocation, useNavigate } from "@/router";
const RequireLogin = ({ children }: { children?: React.ReactNode }) => {
const location = useLocation();
diff --git a/frontend/src/routes/routes.tsx b/frontend/src/routes/routes.tsx
index 466ac30..811b0f7 100644
--- a/frontend/src/routes/routes.tsx
+++ b/frontend/src/routes/routes.tsx
@@ -1,5 +1,3 @@
-import { createRoutesFromElements, Route, redirect } from "react-router-dom";
-
import RequireLogin from "./RequireLogin";
import MainLayout from "@/components/MainLayout";
@@ -10,6 +8,7 @@ import Requests from "@/pages/requests";
import Settings from "@/pages/settings";
import SitesList from "@/pages/sites";
import Tokens from "@/pages/tokens/tokens";
+import { createRoutesFromElements, Route, redirect } from "@/router";
export const routes = createRoutesFromElements(
<Route element={<MainLayout />} path="/">
@@ -21,7 +20,7 @@ export const routes = createRoutesFromElements(
}
path="*"
/>
- <Route index loader={() => redirect("sites")} />
+ <Route index loader={() => redirect("/sites")} />
<Route element={<Logout />} path="logout" />
<Route element={<Login />} path="login" />
<Route
@@ -40,7 +39,7 @@ export const routes = createRoutesFromElements(
}
path="settings"
>
- <Route element={<RequireLogin />} index loader={() => redirect("tokens")} />
+ <Route element={<RequireLogin />} index loader={() => redirect("/settings/tokens")} />
<Route
element={
<RequireLogin>
diff --git a/frontend/src/test-utils.tsx b/frontend/src/test-utils.tsx
index cabf08d..ec59dc5 100644
--- a/frontend/src/test-utils.tsx
+++ b/frontend/src/test-utils.tsx
@@ -4,12 +4,12 @@ import * as React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { RenderOptions, RenderResult } from "@testing-library/react";
import { screen, render } from "@testing-library/react";
-import type { MemoryRouterProps } from "react-router-dom";
-import { MemoryRouter } from "react-router-dom";
import { AppContextProvider, AuthContextProvider } from "./context";
import apiClient from "@/api";
+import type { MemoryRouterProps } from "@/router";
+import { MemoryRouter } from "@/router";
const queryClient = new QueryClient({
defaultOptions: {
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index dbd3903..c1ce53c 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -15,7 +15,7 @@ export default defineConfig({
plugins: [
react(),
AutoImport({
- imports: ["react", "react-router-dom", "vitest"],
+ imports: ["react", "vitest"],
dts: true,
eslintrc: {
enabled: true,
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
index 5c66a57..a74e0ea 100644
--- a/frontend/vitest.config.ts
+++ b/frontend/vitest.config.ts
@@ -5,7 +5,7 @@ import * as path from "path";
export default defineConfig({
plugins: [
AutoImport({
- imports: ["react", "react-router-dom", "vitest"],
+ imports: ["react", "vitest"],
dts: true,
eslintrc: {
enabled: true,
Follow ups
-
[Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
From: MAAS Lander, 2023-04-28
-
Re: [UNITTESTS] -b strict-router-types lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS PASS
From: MAAS Lander, 2023-04-28
-
[Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
From: Peter Makowski, 2023-04-28
-
[Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
From: MAAS Lander, 2023-04-28
-
Re: [Merge] -b strict-router-types lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - LANDING FAILED
From: MAAS Lander, 2023-04-28
-
[Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
From: Peter Makowski, 2023-04-28
-
Re: [Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
From: Nick De Villiers, 2023-04-28
-
[Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
From: Peter Makowski, 2023-04-28
-
[Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
From: Peter Makowski, 2023-04-28
-
Re: [UNITTESTS] -b strict-router-types lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS PASS
From: MAAS Lander, 2023-04-28
-
Re: [UNITTESTS] -b strict-router-types lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS FAILED
From: MAAS Lander, 2023-04-27
-
Re: [UNITTESTS] -b strict-router-types lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS FAILED
From: MAAS Lander, 2023-04-27
-
Re: [UNITTESTS] -b strict-router-types lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS FAILED
From: MAAS Lander, 2023-04-27
-
Re: [UNITTESTS] -b strict-router-types lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS FAILED
From: MAAS Lander, 2023-04-27
-
Re: [Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
From: Nick De Villiers, 2023-04-26
-
Re: [UNITTESTS] -b strict-router-types lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS FAILED
From: MAAS Lander, 2023-04-25
-
[Merge] ~petermakowski/maas-site-manager:strict-router-types into maas-site-manager:main
From: Peter Makowski, 2023-04-25