Skip to content

Commit f2673bc

Browse files
feat: health check card (#62)
* feat: initial work on health-check card * feat: tidy up health check card * feat: add ability to control polling interval * fix: use correct referrer type for error UI links * fix: add default health-check endpoint handler * test: fix failing tests after introducing second table to dashboard page
1 parent 78bdfb9 commit f2673bc

File tree

11 files changed

+390
-51
lines changed

11 files changed

+390
-51
lines changed

package-lock.json

+27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@radix-ui/react-slot": "^1.1.0",
2727
"@radix-ui/react-switch": "^1.1.2",
2828
"@radix-ui/react-tooltip": "^1.1.6",
29+
"@tanstack/react-query": "^5.64.1",
2930
"@types/prismjs": "^1.26.5",
3031
"@types/react-syntax-highlighter": "^15.5.13",
3132
"class-variance-authority": "^0.7.1",
@@ -81,4 +82,4 @@
8182
"overrides": {
8283
"vite": "^6.0.1"
8384
}
84-
}
85+
}

src/components/Dashboard.tsx

+7-11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
2121
import { useSearchParams } from "react-router-dom";
2222
import { AlertConversation } from "@/api/generated";
2323
import { getMaliciousPackage } from "@/lib/utils";
24+
import { CardCodegateStatus } from "@/features/dashboard/components/card-codegate-status";
2425

2526
const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => {
2627
const data = getMaliciousPackage(input);
@@ -127,16 +128,11 @@ export function Dashboard() {
127128

128129
return (
129130
<div className="flex-col">
130-
<div className="flex flex-wrap items-center gap-4 w-full">
131-
<div className="min-w-80 w-1/3 h-60">
132-
<BarChart data={alerts} loading={loading} />
133-
</div>
134-
<div className="min-w-80 w-1/4 h-60">
135-
<PieChart data={maliciousPackages} loading={loading} />
136-
</div>
137-
<div className="relative w-[370px] h-60">
138-
<LineChart data={alerts} loading={loading} />
139-
</div>
131+
<div className="grid 2xl:grid-cols-4 sm:grid-cols-2 grid-cols-1 items-stretch gap-4 w-full">
132+
<CardCodegateStatus />
133+
<BarChart data={alerts} loading={loading} />
134+
<PieChart data={maliciousPackages} loading={loading} />
135+
<LineChart data={alerts} loading={loading} />
140136
</div>
141137

142138
<Separator className="my-8" />
@@ -193,7 +189,7 @@ export function Dashboard() {
193189
</div>
194190
</div>
195191
<div className="overflow-x-auto">
196-
<Table>
192+
<Table data-testid="alerts-table">
197193
<TableHeader>
198194
<TableRow>
199195
<TableHead className="w-[150px]">Trigger Type</TableHead>

src/components/__tests__/Dashboard.test.tsx

+15-11
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,21 @@ describe("Dashboard", () => {
154154
).toBeVisible();
155155
expect(screen.getByRole("searchbox")).toBeVisible();
156156

157-
const row = screen.getAllByRole("row")[1] as HTMLElement;
157+
const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole(
158+
"row",
159+
)[1] as HTMLElement;
160+
const secondRow = within(screen.getByTestId("alerts-table")).getAllByRole(
161+
"row",
162+
)[2] as HTMLElement;
158163

159-
expect(within(row).getByText(/ghp_token/i)).toBeVisible();
160-
expect(within(row).getByText(/codegate-secrets/i)).toBeVisible();
161-
expect(within(row).getAllByText(/n\/a/i).length).toEqual(2);
162-
expect(within(row).getByText(/2025\/01\/07/i)).toBeVisible();
163-
expect(within(row).getByTestId(/time/i)).toBeVisible();
164+
expect(within(firstRow).getByText(/ghp_token/i)).toBeVisible();
165+
expect(within(firstRow).getByText(/codegate-secrets/i)).toBeVisible();
166+
expect(within(firstRow).getAllByText(/n\/a/i).length).toEqual(2);
167+
expect(within(firstRow).getByText(/2025\/01\/07/i)).toBeVisible();
168+
expect(within(firstRow).getByTestId(/time/i)).toBeVisible();
164169

165170
// check trigger_string null
166-
expect(
167-
within(screen.getAllByRole("row")[2] as HTMLElement).getAllByText(/n\/a/i)
168-
.length,
169-
).toEqual(3);
171+
expect(within(secondRow).getAllByText(/n\/a/i).length).toEqual(3);
170172
});
171173

172174
it("should render malicious pkg", async () => {
@@ -271,7 +273,9 @@ describe("Dashboard", () => {
271273
await waitFor(() =>
272274
expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("1"),
273275
);
274-
const row = screen.getAllByRole("row")[1] as HTMLElement;
276+
const row = within(screen.getByTestId("alerts-table")).getAllByRole(
277+
"row",
278+
)[1] as HTMLElement;
275279
expect(within(row).getByText(/ghp_token/i)).toBeVisible();
276280
expect(within(row).getByText(/codegate-secrets/i)).toBeVisible();
277281
});

src/components/ui/card.tsx

+24-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import * as React from "react"
1+
import * as React from "react";
22

3-
import { cn } from "@/lib/utils"
3+
import { cn } from "@/lib/utils";
44

55
const Card = React.forwardRef<
66
HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
1010
ref={ref}
1111
className={cn(
1212
"rounded-lg border bg-card text-card-foreground shadow-sm",
13-
className
13+
className,
1414
)}
1515
{...props}
1616
/>
17-
))
18-
Card.displayName = "Card"
17+
));
18+
Card.displayName = "Card";
1919

2020
const CardHeader = React.forwardRef<
2121
HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
2626
className={cn("flex flex-col space-y-1 p-4", className)}
2727
{...props}
2828
/>
29-
))
30-
CardHeader.displayName = "CardHeader"
29+
));
30+
CardHeader.displayName = "CardHeader";
3131

3232
const CardTitle = React.forwardRef<
3333
HTMLDivElement,
@@ -37,12 +37,12 @@ const CardTitle = React.forwardRef<
3737
ref={ref}
3838
className={cn(
3939
"text-xl font-semibold leading-none tracking-tight",
40-
className
40+
className,
4141
)}
4242
{...props}
4343
/>
44-
))
45-
CardTitle.displayName = "CardTitle"
44+
));
45+
CardTitle.displayName = "CardTitle";
4646

4747
const CardDescription = React.forwardRef<
4848
HTMLDivElement,
@@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
5353
className={cn("text-sm text-muted-foreground", className)}
5454
{...props}
5555
/>
56-
))
57-
CardDescription.displayName = "CardDescription"
56+
));
57+
CardDescription.displayName = "CardDescription";
5858

5959
const CardContent = React.forwardRef<
6060
HTMLDivElement,
6161
React.HTMLAttributes<HTMLDivElement>
6262
>(({ className, ...props }, ref) => (
6363
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
64-
))
65-
CardContent.displayName = "CardContent"
64+
));
65+
CardContent.displayName = "CardContent";
6666

6767
const CardFooter = React.forwardRef<
6868
HTMLDivElement,
@@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
7373
className={cn("flex items-center p-4 pt-0", className)}
7474
{...props}
7575
/>
76-
))
77-
CardFooter.displayName = "CardFooter"
76+
));
77+
CardFooter.displayName = "CardFooter";
7878

79-
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
79+
export {
80+
Card,
81+
CardHeader,
82+
CardFooter,
83+
CardTitle,
84+
CardDescription,
85+
CardContent,
86+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { server } from "@/mocks/msw/node";
2+
import { http, HttpResponse } from "msw";
3+
import { expect } from "vitest";
4+
import { CardCodegateStatus } from "../card-codegate-status";
5+
import { render, waitFor } from "@/lib/test-utils";
6+
7+
const renderComponent = () => render(<CardCodegateStatus />);
8+
9+
describe("CardCodegateStatus", () => {
10+
test("renders 'healthy' state", async () => {
11+
server.use(
12+
http.get("*/health", () => HttpResponse.json({ status: "healthy" })),
13+
);
14+
15+
const { getByText } = renderComponent();
16+
17+
await waitFor(
18+
() => {
19+
expect(getByText(/healthy/i)).toBeVisible();
20+
},
21+
{ timeout: 10_000 },
22+
);
23+
});
24+
25+
test("renders 'unhealthy' state", async () => {
26+
server.use(http.get("*/health", () => HttpResponse.json({ status: null })));
27+
28+
const { getByText } = renderComponent();
29+
30+
await waitFor(
31+
() => {
32+
expect(getByText(/unhealthy/i)).toBeVisible();
33+
},
34+
{ timeout: 10_000 },
35+
);
36+
});
37+
38+
test("renders 'error' state", async () => {
39+
server.use(http.get("*/health", () => HttpResponse.error()));
40+
41+
const { getByText } = renderComponent();
42+
43+
await waitFor(
44+
() => {
45+
expect(getByText(/an error occurred/i)).toBeVisible();
46+
},
47+
{ timeout: 10_000 },
48+
);
49+
});
50+
});

0 commit comments

Comments
 (0)