From bb06353a64a898a1909764a1a27f2ac0a25b92c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 19:00:11 +0000 Subject: [PATCH 1/4] Initial plan From be1db8dd8fc323aae668227cc170a17a9f9ba39b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 19:08:57 +0000 Subject: [PATCH 2/4] Add default 404 handling for unknown UI routes --- .../src/components/root/RootLayout.test.tsx | 67 +++++++++++++++++++ ui/app/src/components/root/RootLayout.tsx | 4 ++ .../src/components/shared/NotFoundLayout.tsx | 15 +++++ .../workspaces/WorkspaceProvider.tsx | 6 +- 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 ui/app/src/components/root/RootLayout.test.tsx create mode 100644 ui/app/src/components/shared/NotFoundLayout.tsx 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..5a14cadc08 --- /dev/null +++ b/ui/app/src/components/root/RootLayout.test.tsx @@ -0,0 +1,67 @@ +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(, { + initialEntries: ["/"], + appRolesContext: { roles: [], setAppRoles: vi.fn() }, + } as any); + + await waitFor(() => { + expect(screen.getByText("Root Dashboard")).toBeInTheDocument(); + }); + }); + + it("renders a 404 page for an unknown route", async () => { + render(, { + initialEntries: ["/does-not-exist"], + appRolesContext: { roles: [], setAppRoles: vi.fn() }, + } as any); + + 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={} /> - } /> - )} + } /> + )} } /> + } /> From f129908c21859bca76f4bd056e2bb393172cb0b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 19:10:28 +0000 Subject: [PATCH 3/4] Refine 404 route tests typing --- ui/app/src/components/root/RootLayout.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/app/src/components/root/RootLayout.test.tsx b/ui/app/src/components/root/RootLayout.test.tsx index 5a14cadc08..0f76daf73b 100644 --- a/ui/app/src/components/root/RootLayout.test.tsx +++ b/ui/app/src/components/root/RootLayout.test.tsx @@ -43,9 +43,10 @@ describe("RootLayout routes", () => { it("renders root dashboard on home route", async () => { render(, { + children: <>, initialEntries: ["/"], appRolesContext: { roles: [], setAppRoles: vi.fn() }, - } as any); + }); await waitFor(() => { expect(screen.getByText("Root Dashboard")).toBeInTheDocument(); @@ -54,9 +55,10 @@ describe("RootLayout routes", () => { it("renders a 404 page for an unknown route", async () => { render(, { + children: <>, initialEntries: ["/does-not-exist"], appRolesContext: { roles: [], setAppRoles: vi.fn() }, - } as any); + }); await waitFor(() => { expect(screen.getByText("404 - Page not found")).toBeInTheDocument(); From 231a374f87dc072897fbc74e481d460affcbd67c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 19:20:45 +0000 Subject: [PATCH 4/4] Update changelog with UI 404 enhancement --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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**