diff --git a/CHANGELOG.md b/CHANGELOG.md index 1284b51429..372dd7cf9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ENHANCEMENTS: * Specify default_outbound_access_enabled = false setting for all subnets ([#4757](https://github.com/microsoft/AzureTRE/pull/4757)) * Pin all GitHub Actions workflow steps to full commit SHAs to prevent supply chain attacks plus update to latest releases ([#4886](https://github.com/microsoft/AzureTRE/pull/4886)) +* Add a default UI 404 page for invalid URLs, including a link back to the home page ([#4908](https://github.com/microsoft/AzureTRE/pull/4908)) ## (0.28.0) (March 2, 2026) **BREAKING CHANGES** diff --git a/ui/app/src/components/root/RootLayout.test.tsx b/ui/app/src/components/root/RootLayout.test.tsx new file mode 100644 index 0000000000..0f76daf73b --- /dev/null +++ b/ui/app/src/components/root/RootLayout.test.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { render, screen, waitFor } from "../../test-utils"; +import { RootLayout } from "./RootLayout"; + +const mockApiCall = vi.fn(); + +vi.mock("../../hooks/useAuthApiCall", () => ({ + useAuthApiCall: () => mockApiCall, + HttpMethod: { Get: "GET" }, + ResultType: { JSON: "json" }, +})); + +vi.mock("./RootDashboard", () => ({ + RootDashboard: () =>
Root Dashboard
, +})); + +vi.mock("./LeftNav", () => ({ + LeftNav: () =>
Left Nav
, +})); + +vi.mock("../shared/RequestsList", () => ({ + RequestsList: () =>
Requests List
, +})); + +vi.mock("../shared/SharedServices", () => ({ + SharedServices: () =>
Shared Services
, +})); + +vi.mock("../shared/SharedServiceItem", () => ({ + SharedServiceItem: () =>
Shared Service Item
, +})); + +vi.mock("../shared/SecuredByRole", () => ({ + SecuredByRole: ({ element }: { element: React.ReactElement }) => element, +})); + +describe("RootLayout routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockApiCall.mockResolvedValue({ workspaces: [] }); + }); + + it("renders root dashboard on home route", async () => { + render(, { + children: <>, + initialEntries: ["/"], + appRolesContext: { roles: [], setAppRoles: vi.fn() }, + }); + + await waitFor(() => { + expect(screen.getByText("Root Dashboard")).toBeInTheDocument(); + }); + }); + + it("renders a 404 page for an unknown route", async () => { + render(, { + children: <>, + initialEntries: ["/does-not-exist"], + appRolesContext: { roles: [], setAppRoles: vi.fn() }, + }); + + await waitFor(() => { + expect(screen.getByText("404 - Page not found")).toBeInTheDocument(); + }); + + expect(screen.getByRole("link", { name: "Go to home page" })).toHaveAttribute("href", "/"); + }); +}); diff --git a/ui/app/src/components/root/RootLayout.tsx b/ui/app/src/components/root/RootLayout.tsx index ab842b3672..1ded601718 100644 --- a/ui/app/src/components/root/RootLayout.tsx +++ b/ui/app/src/components/root/RootLayout.tsx @@ -22,6 +22,7 @@ import { ExceptionLayout } from "../shared/ExceptionLayout"; import { AppRolesContext } from "../../contexts/AppRolesContext"; import { CostsContext } from "../../contexts/CostsContext"; import config from "../../config.json"; +import { NotFoundLayout } from "../shared/NotFoundLayout"; export const RootLayout: React.FunctionComponent = () => { const [workspaces, setWorkspaces] = useState([] as Array); @@ -190,6 +191,7 @@ export const RootLayout: React.FunctionComponent = () => { /> } /> + } /> } /> @@ -199,9 +201,11 @@ export const RootLayout: React.FunctionComponent = () => { } /> + } /> } /> + } /> diff --git a/ui/app/src/components/shared/NotFoundLayout.tsx b/ui/app/src/components/shared/NotFoundLayout.tsx new file mode 100644 index 0000000000..84b57ff944 --- /dev/null +++ b/ui/app/src/components/shared/NotFoundLayout.tsx @@ -0,0 +1,15 @@ +import { Link, MessageBar, MessageBarType } from "@fluentui/react"; +import React from "react"; +import { Link as RouterLink } from "react-router-dom"; + +export const NotFoundLayout: React.FunctionComponent = () => { + return ( + +

404 - Page not found

+

The page you are looking for does not exist.

+ + Go to home page + +
+ ); +}; diff --git a/ui/app/src/components/workspaces/WorkspaceProvider.tsx b/ui/app/src/components/workspaces/WorkspaceProvider.tsx index 076f64f773..acd94f7ceb 100644 --- a/ui/app/src/components/workspaces/WorkspaceProvider.tsx +++ b/ui/app/src/components/workspaces/WorkspaceProvider.tsx @@ -32,6 +32,7 @@ import { LoadingState } from "../../models/loadingState"; import { ExceptionLayout } from "../shared/ExceptionLayout"; import { AppRolesContext } from "../../contexts/AppRolesContext"; import { RoleName, WorkspaceRoleName } from "../../models/roleNames"; +import { NotFoundLayout } from "../shared/NotFoundLayout"; export const WorkspaceProvider: React.FunctionComponent = () => { const apiCall = useAuthApiCall(); @@ -335,9 +336,10 @@ export const WorkspaceProvider: React.FunctionComponent = () => { path="shared-services/:sharedServiceId/*" element={} /> - } /> - )} + } /> + )} } /> + } />