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={}
/>
- } />
- >)}
+ } />
+ >)}
} />
+ } />