Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
69 changes: 69 additions & 0 deletions ui/app/src/components/root/RootLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <div>Root Dashboard</div>,
}));

vi.mock("./LeftNav", () => ({
LeftNav: () => <div>Left Nav</div>,
}));

vi.mock("../shared/RequestsList", () => ({
RequestsList: () => <div>Requests List</div>,
}));

vi.mock("../shared/SharedServices", () => ({
SharedServices: () => <div>Shared Services</div>,
}));

vi.mock("../shared/SharedServiceItem", () => ({
SharedServiceItem: () => <div>Shared Service Item</div>,
}));

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(<RootLayout />, {
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(<RootLayout />, {
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", "/");
});
});
4 changes: 4 additions & 0 deletions ui/app/src/components/root/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Workspace>);
Expand Down Expand Up @@ -190,6 +191,7 @@ export const RootLayout: React.FunctionComponent = () => {
/>
}
/>
<Route path="*" element={<NotFoundLayout />} />
</Routes>
}
/>
Expand All @@ -199,9 +201,11 @@ export const RootLayout: React.FunctionComponent = () => {
<Routes>
<Route path="/" />
<Route path="airlock/*" element={<RequestsList />} />
<Route path="*" element={<NotFoundLayout />} />
</Routes>
}
/>
<Route path="*" element={<NotFoundLayout />} />
</Routes>
</Stack.Item>
</Stack>
Expand Down
15 changes: 15 additions & 0 deletions ui/app/src/components/shared/NotFoundLayout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<MessageBar messageBarType={MessageBarType.error} isMultiline={true}>
<h2>404 - Page not found</h2>
<p>The page you are looking for does not exist.</p>
<Link as={RouterLink} to="/">
Go to home page
</Link>
</MessageBar>
);
};
6 changes: 4 additions & 2 deletions ui/app/src/components/workspaces/WorkspaceProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -335,9 +336,10 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
path="shared-services/:sharedServiceId/*"
element={<SharedServiceItem readonly={true} />}
/>
<Route path="requests/*" element={<Airlock />} />
</>)}
<Route path="requests/*" element={<Airlock />} />
</>)}
<Route path="users/*" element={<WorkspaceUsers />} />
<Route path="*" element={<NotFoundLayout />} />
</Routes>
</Stack.Item>
</Stack>
Expand Down