Skip to content

Commit 78bdfb9

Browse files
authored
feat: add ErrorBoundary (#69)
* wip error boundary * feat: replace icon * test: add ErrorBoundary
1 parent 108b8ec commit 78bdfb9

File tree

5 files changed

+109
-4
lines changed

5 files changed

+109
-4
lines changed

src/components/Error.tsx

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { CircleAlert } from "lucide-react";
2+
import { Header } from "./Header";
3+
import { Card } from "./ui/card";
4+
5+
export function Error() {
6+
return (
7+
<div className="w-screen h-screen flex flex-col items-center justify-center">
8+
<div className="flex-0 w-full">
9+
<Header hasError />
10+
</div>
11+
<div className="h-24 flex flex-col flex-1 justify-center">
12+
<Card className="p-8 flex flex-col items-center">
13+
<CircleAlert className="text-red-600 mb-2 size-16" />
14+
<div className="text-xl font-semibold text-gray-600 text-center">
15+
An error occurred
16+
</div>
17+
<div className="text-md mb-4 text-gray-600 text-center text-balance">
18+
If this issue persists, please reach out to us on{" "}
19+
<a
20+
className="underline text-gray-700"
21+
href="https://discord.gg/stacklok"
22+
rel="noopener noreferrer"
23+
target="_blank"
24+
>
25+
Discord
26+
</a>{" "}
27+
or open a new{" "}
28+
<a
29+
className="underline text-gray-700"
30+
href="https://github.com/stacklok/codegate/issues/new"
31+
rel="noopener noreferrer"
32+
target="_blank"
33+
>
34+
Github issue
35+
</a>
36+
</div>
37+
</Card>
38+
</div>
39+
</div>
40+
);
41+
}

src/components/ErrorBoundary.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { ReactNode } from "react";
2+
import { Component } from "react";
3+
4+
interface Props {
5+
children?: ReactNode;
6+
fallback: ReactNode;
7+
}
8+
9+
interface State {
10+
hasError: boolean;
11+
}
12+
13+
export default class ErrorBoundary extends Component<Props, State> {
14+
public state: State = {
15+
hasError: false,
16+
};
17+
18+
public static getDerivedStateFromError(): State {
19+
return { hasError: true };
20+
}
21+
22+
public componentDidCatch() {
23+
// this should log the error to a service like Sentry
24+
}
25+
26+
public render() {
27+
if (this.state.hasError) {
28+
return this.props.fallback;
29+
}
30+
31+
return this.props.children;
32+
}
33+
}

src/components/Header.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import { Link } from "react-router-dom";
22
import { SidebarTrigger } from "./ui/sidebar";
33
import { Separator } from "./ui/separator";
44

5-
export function Header() {
5+
export function Header({ hasError }: { hasError?: boolean }) {
66
return (
77
<header className="flex-shrink-0 h-16 px-3 items-center flex w-full bg-teal-25 opacity-1 border-b-blue-200 border-b">
88
<div className="flex items-center flex-1">
9-
<SidebarTrigger />
10-
<Separator orientation="vertical" className="h-8 mx-3" />
9+
{!hasError && (
10+
<>
11+
<SidebarTrigger />
12+
<Separator orientation="vertical" className="h-8 mx-3" />
13+
</>
14+
)}
15+
1116
<nav className="mx-1 flex">
1217
<Link to="/">
1318
<h1 className="text-2xl w-max flex font-semibold">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { render } from "@/lib/test-utils";
2+
import { screen } from "@testing-library/react";
3+
import { describe, expect, it, vi } from "vitest";
4+
import ErrorBoundary from "../ErrorBoundary";
5+
import { Error } from "../Error";
6+
7+
const ErrorComponent = () => {
8+
throw Error();
9+
};
10+
11+
describe("ErrorBoundary", () => {
12+
it("renders fallback when a child throws an error", () => {
13+
vi.spyOn(console, "error").mockImplementation(() => {});
14+
render(
15+
<ErrorBoundary fallback={<Error />}>
16+
<ErrorComponent />
17+
</ErrorBoundary>,
18+
);
19+
20+
expect(screen.getByText(/an error occurred/i)).toBeVisible();
21+
});
22+
});

src/main.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import "./index.css";
44
import App from "./App.tsx";
55
import { BrowserRouter } from "react-router-dom";
66
import { SidebarProvider } from "./components/ui/sidebar.tsx";
7+
import ErrorBoundary from "./components/ErrorBoundary.tsx";
8+
import { Error } from "./components/Error.tsx";
79

810
createRoot(document.getElementById("root")!).render(
911
<StrictMode>
1012
<BrowserRouter>
1113
<SidebarProvider>
12-
<App />
14+
<ErrorBoundary fallback={<Error />}>
15+
<App />
16+
</ErrorBoundary>
1317
</SidebarProvider>
1418
</BrowserRouter>
1519
</StrictMode>

0 commit comments

Comments
 (0)