From 87661f15021f5646e7671ae64fe0ee76c1d6a78c Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 14 Jan 2025 09:26:55 +0000 Subject: [PATCH 1/6] feat: initial work on health-check card --- package-lock.json | 27 ++++++ package.json | 3 +- src/components/Dashboard.tsx | 2 + .../dashboard/components/card-health.tsx | 90 +++++++++++++++++++ src/main.tsx | 5 +- 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/features/dashboard/components/card-health.tsx diff --git a/package-lock.json b/package-lock.json index a3415931..63388d73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", + "@tanstack/react-query": "^5.64.1", "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "class-variance-authority": "^0.7.1", @@ -2725,6 +2726,32 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" } }, + "node_modules/@tanstack/query-core": { + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz", + "integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz", + "integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.64.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", diff --git a/package.json b/package.json index 7feb2ab0..7df4627b 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", + "@tanstack/react-query": "^5.64.1", "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "class-variance-authority": "^0.7.1", @@ -81,4 +82,4 @@ "overrides": { "vite": "^6.0.1" } -} \ No newline at end of file +} diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index ba200ee7..bdeca1bc 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -21,6 +21,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { useSearchParams } from "react-router-dom"; import { AlertConversation } from "@/api/generated"; import { getMaliciousPackage } from "@/lib/utils"; +import { CardHealth } from "@/features/dashboard/components/card-health"; const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => { const data = getMaliciousPackage(input); @@ -128,6 +129,7 @@ export function Dashboard() { return (
+
diff --git a/src/features/dashboard/components/card-health.tsx b/src/features/dashboard/components/card-health.tsx new file mode 100644 index 00000000..6c3d8019 --- /dev/null +++ b/src/features/dashboard/components/card-health.tsx @@ -0,0 +1,90 @@ +import { client } from "@/api/generated"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { TableRow, TableBody, TableCell, Table } from "@/components/ui/table"; +import { useQuery } from "@tanstack/react-query"; +import { Check, CheckCheck, CheckCircle, CheckCircle2, XCircle } from "lucide-react"; + +enum Status { + HEALTHY = "Healthy", + LOADING = "Loading", + NOT_REACHABLE = "Not reachable", +} + +type HealthResp = { status: "healthy" | null } | null; + +const getStatus = async (): Promise => { + const status = await client.get({ + url: "/health", + throwOnError: true, + }); + + if (status.data?.status === "healthy") return Status.HEALTHY; + return Status.NOT_REACHABLE; +}; + +const useStatus = () => + useQuery({ + queryFn: getStatus, + queryKey: ["getStatus"], + refetchInterval: 1000, + staleTime: 0, + gcTime: 0, + refetchIntervalInBackground: true, + refetchOnMount: true, + refetchOnReconnect: true, + refetchOnWindowFocus: true, + }); + +const StatusText = ({ status }: { status: Status }) => { + switch (status) { + case Status.HEALTHY: + return ( +
+ {Status.HEALTHY} +
+ ); + case Status.LOADING: + return ( +
+ {Status.LOADING} +
+ ); + case Status.NOT_REACHABLE: + return ( +
+ {Status.NOT_REACHABLE} +
+ ); + default: { + status satisfies never; + } + } +}; + +export function CardHealth() { + const { data: status, error: statusError } = useStatus(); + + return ( + + + System health + + + + + + System health + + {status ? : "Error"} + + + + CA certificate + Trusted + + +
+
+
+ ); +} diff --git a/src/main.tsx b/src/main.tsx index 0115a0af..d99394dd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,12 +4,15 @@ import "./index.css"; import App from "./App.tsx"; import { BrowserRouter } from "react-router-dom"; import { SidebarProvider } from "./components/ui/sidebar.tsx"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; createRoot(document.getElementById("root")!).render( - + + + From 627e6cc29f72fbf8ae65971b90aea22abbb16859 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 14 Jan 2025 12:52:35 +0000 Subject: [PATCH 2/6] feat: tidy up health check card --- src/components/Dashboard.tsx | 18 +++---- src/components/ui/card.tsx | 43 ++++++++++------- .../dashboard/components/card-health.tsx | 48 ++++++++----------- src/viz/LineChart.tsx | 4 +- 4 files changed, 52 insertions(+), 61 deletions(-) diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index bdeca1bc..3531a82b 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -21,7 +21,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { useSearchParams } from "react-router-dom"; import { AlertConversation } from "@/api/generated"; import { getMaliciousPackage } from "@/lib/utils"; -import { CardHealth } from "@/features/dashboard/components/card-health"; +import { CardCodegateStatus } from "@/features/dashboard/components/card-health"; const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => { const data = getMaliciousPackage(input); @@ -128,17 +128,11 @@ export function Dashboard() { return (
-
- -
- -
-
- -
-
- -
+
+ + + +
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 754ad93a..8d81333d 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Card = React.forwardRef< HTMLDivElement, @@ -9,13 +9,13 @@ const Card = React.forwardRef<
-)) -Card.displayName = "Card" +)); +Card.displayName = "Card"; const CardHeader = React.forwardRef< HTMLDivElement, @@ -26,8 +26,8 @@ const CardHeader = React.forwardRef< className={cn("flex flex-col space-y-1 p-4", className)} {...props} /> -)) -CardHeader.displayName = "CardHeader" +)); +CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef< HTMLDivElement, @@ -37,12 +37,12 @@ const CardTitle = React.forwardRef< ref={ref} className={cn( "text-xl font-semibold leading-none tracking-tight", - className + className, )} {...props} /> -)) -CardTitle.displayName = "CardTitle" +)); +CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef< HTMLDivElement, @@ -53,16 +53,16 @@ const CardDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -CardDescription.displayName = "CardDescription" +)); +CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
-)) -CardContent.displayName = "CardContent" +)); +CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef< HTMLDivElement, @@ -73,7 +73,14 @@ const CardFooter = React.forwardRef< className={cn("flex items-center p-4 pt-0", className)} {...props} /> -)) -CardFooter.displayName = "CardFooter" +)); +CardFooter.displayName = "CardFooter"; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/src/features/dashboard/components/card-health.tsx b/src/features/dashboard/components/card-health.tsx index 6c3d8019..1f73315a 100644 --- a/src/features/dashboard/components/card-health.tsx +++ b/src/features/dashboard/components/card-health.tsx @@ -2,12 +2,11 @@ import { client } from "@/api/generated"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { TableRow, TableBody, TableCell, Table } from "@/components/ui/table"; import { useQuery } from "@tanstack/react-query"; -import { Check, CheckCheck, CheckCircle, CheckCircle2, XCircle } from "lucide-react"; +import { CheckCircle2, LoaderCircle } from "lucide-react"; enum Status { - HEALTHY = "Healthy", - LOADING = "Loading", - NOT_REACHABLE = "Not reachable", + CONNECTED = "Connected", + NOT_CONNECTED = "Not connected", } type HealthResp = { status: "healthy" | null } | null; @@ -18,8 +17,8 @@ const getStatus = async (): Promise => { throwOnError: true, }); - if (status.data?.status === "healthy") return Status.HEALTHY; - return Status.NOT_REACHABLE; + if (status.data?.status === "healthy") return Status.CONNECTED; + return Status.NOT_CONNECTED; }; const useStatus = () => @@ -37,22 +36,17 @@ const useStatus = () => const StatusText = ({ status }: { status: Status }) => { switch (status) { - case Status.HEALTHY: + case Status.CONNECTED: return ( -
- {Status.HEALTHY} +
+ {Status.CONNECTED}
); - case Status.LOADING: + case Status.NOT_CONNECTED: return ( -
- {Status.LOADING} -
- ); - case Status.NOT_REACHABLE: - return ( -
- {Status.NOT_REACHABLE} +
+ {Status.NOT_CONNECTED}{" "} +
); default: { @@ -61,27 +55,23 @@ const StatusText = ({ status }: { status: Status }) => { } }; -export function CardHealth() { - const { data: status, error: statusError } = useStatus(); +export function CardCodegateStatus() { + const { data: status } = useStatus(); return ( - System health + CodeGate Status - - System health - - {status ? : "Error"} + + CodeGate server + + - - CA certificate - Trusted -
diff --git a/src/viz/LineChart.tsx b/src/viz/LineChart.tsx index f9c8ecc6..8af1c0d7 100644 --- a/src/viz/LineChart.tsx +++ b/src/viz/LineChart.tsx @@ -82,8 +82,8 @@ export function LineChart({ Alerts by date - - + + Date: Tue, 14 Jan 2025 15:53:16 +0000 Subject: [PATCH 3/6] feat: add ability to control polling interval --- src/components/Dashboard.tsx | 2 +- src/components/ui/card.tsx | 2 +- .../__tests__/card-codegate-status.test.tsx | 50 ++++ .../components/card-codegate-status.tsx | 232 ++++++++++++++++++ .../dashboard/components/card-health.tsx | 80 ------ src/lib/test-utils.tsx | 30 ++- 6 files changed, 306 insertions(+), 90 deletions(-) create mode 100644 src/features/dashboard/components/__tests__/card-codegate-status.test.tsx create mode 100644 src/features/dashboard/components/card-codegate-status.tsx delete mode 100644 src/features/dashboard/components/card-health.tsx diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 3531a82b..db39b86d 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -21,7 +21,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { useSearchParams } from "react-router-dom"; import { AlertConversation } from "@/api/generated"; import { getMaliciousPackage } from "@/lib/utils"; -import { CardCodegateStatus } from "@/features/dashboard/components/card-health"; +import { CardCodegateStatus } from "@/features/dashboard/components/card-codegate-status"; const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => { const data = getMaliciousPackage(input); diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 8d81333d..0b46be9e 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -9,7 +9,7 @@ const Card = React.forwardRef<
render(); + +describe("CardCodegateStatus", () => { + test("renders 'healthy' state", async () => { + server.use( + http.get("*/health", () => HttpResponse.json({ status: "healthy" })), + ); + + const { getByText } = renderComponent(); + + await waitFor( + () => { + expect(getByText(/healthy/i)).toBeVisible(); + }, + { timeout: 10_000 }, + ); + }); + + test("renders 'unhealthy' state", async () => { + server.use(http.get("*/health", () => HttpResponse.json({ status: null }))); + + const { getByText } = renderComponent(); + + await waitFor( + () => { + expect(getByText(/unhealthy/i)).toBeVisible(); + }, + { timeout: 10_000 }, + ); + }); + + test("renders 'error' state", async () => { + server.use(http.get("*/health", () => HttpResponse.error())); + + const { getByText } = renderComponent(); + + await waitFor( + () => { + expect(getByText(/an error occurred/i)).toBeVisible(); + }, + { timeout: 10_000 }, + ); + }); +}); diff --git a/src/features/dashboard/components/card-codegate-status.tsx b/src/features/dashboard/components/card-codegate-status.tsx new file mode 100644 index 00000000..fa1c89ba --- /dev/null +++ b/src/features/dashboard/components/card-codegate-status.tsx @@ -0,0 +1,232 @@ +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { TableRow, TableBody, TableCell, Table } from "@/components/ui/table"; +import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; +import { format } from "date-fns"; +import { + CheckCircle2, + LoaderCircle, + XCircle, + ChevronDown, + Check, +} from "lucide-react"; +import { Dispatch, SetStateAction, useState } from "react"; + +const INTERVAL = { + "1_SEC": { value: 1_000, name: "1 second" }, + "5_SEC": { value: 5_000, name: "5 seconds" }, + "10_SEC": { value: 10_000, name: "10 seconds" }, + "30_SEC": { value: 30_000, name: "30 seconds" }, + "1_MIN": { value: 60_000, name: "1 minute" }, + "5_MIN": { value: 300_000, name: "5 minutes" }, + "10_MIN": { value: 600_000, name: "10 minutes" }, +} as const; + +const DEFAULT_INTERVAL: Interval = "5_SEC"; + +type Interval = keyof typeof INTERVAL; + +enum Status { + HEALTHY = "Healthy", + UNHEALTHY = "Unhealthy", +} + +type HealthResp = { status: "healthy" | unknown } | null; + +const getStatus = async (): Promise => { + const resp = await fetch( + new URL("/health", import.meta.env.VITE_BASE_API_URL), + ); + const data = (await resp.json()) as unknown as HealthResp; + + if (data?.status === "healthy") return Status.HEALTHY; + if (data?.status !== "healthy") return Status.UNHEALTHY; + + return null; +}; + +const useStatus = (pollingInterval: Interval) => + useQuery({ + queryFn: getStatus, + queryKey: ["getStatus", { pollingInterval }], + refetchInterval: INTERVAL[pollingInterval].value, + staleTime: Infinity, + gcTime: Infinity, + refetchIntervalInBackground: true, + refetchOnMount: true, + refetchOnReconnect: true, + refetchOnWindowFocus: true, + retry: false, + }); + +const StatusText = ({ + status, + isPending, +}: { + status: Status | null; + isPending: boolean; +}) => { + if (isPending || status === null) { + return ( +
+ Checking +
+ ); + } + + switch (status) { + case Status.HEALTHY: + return ( +
+ {Status.HEALTHY} +
+ ); + case Status.UNHEALTHY: + return ( +
+ {Status.UNHEALTHY} +
+ ); + default: { + status satisfies never; + } + } +}; + +function ErrorUI() { + return ( +
+ +
+ An error occurred +
+
+ If this issue persists, please reach out to us on{" "} + + Discord + {" "} + or open a new{" "} + + Github issue + +
+
+ ); +} + +function PollIntervalControl({ + className, + pollingInterval, + setPollingInterval, +}: { + className?: string; + pollingInterval: Interval; + setPollingInterval: Dispatch>; +}) { + return ( +
+
+
+
+ Check for updates +
+
+ every {INTERVAL[pollingInterval].name} +
+
+ +
+
+ {Object.entries(INTERVAL).map(([key, { name }]) => { + const isActive = key === pollingInterval; + + return ( + + ); + })} +
+
+ ); +} + +export function InnerContent({ + isError, + isPending, + data, +}: Pick, "data" | "isPending" | "isError">) { + if (!isPending && isError) { + return ; + } + + return ( + + + + CodeGate server + + + + + +
+ ); +} + +export function CardCodegateStatus() { + const [pollingInterval, setPollingInterval] = useState( + () => DEFAULT_INTERVAL, + ); + const { data, dataUpdatedAt, isPending, isError } = + useStatus(pollingInterval); + + return ( + + + + CodeGate Status + + + + + + + + +
+
+ Last checked +
+
+ {format(new Date(dataUpdatedAt), "pp")} +
+
+ + +
+
+ ); +} diff --git a/src/features/dashboard/components/card-health.tsx b/src/features/dashboard/components/card-health.tsx deleted file mode 100644 index 1f73315a..00000000 --- a/src/features/dashboard/components/card-health.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { client } from "@/api/generated"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { TableRow, TableBody, TableCell, Table } from "@/components/ui/table"; -import { useQuery } from "@tanstack/react-query"; -import { CheckCircle2, LoaderCircle } from "lucide-react"; - -enum Status { - CONNECTED = "Connected", - NOT_CONNECTED = "Not connected", -} - -type HealthResp = { status: "healthy" | null } | null; - -const getStatus = async (): Promise => { - const status = await client.get({ - url: "/health", - throwOnError: true, - }); - - if (status.data?.status === "healthy") return Status.CONNECTED; - return Status.NOT_CONNECTED; -}; - -const useStatus = () => - useQuery({ - queryFn: getStatus, - queryKey: ["getStatus"], - refetchInterval: 1000, - staleTime: 0, - gcTime: 0, - refetchIntervalInBackground: true, - refetchOnMount: true, - refetchOnReconnect: true, - refetchOnWindowFocus: true, - }); - -const StatusText = ({ status }: { status: Status }) => { - switch (status) { - case Status.CONNECTED: - return ( -
- {Status.CONNECTED} -
- ); - case Status.NOT_CONNECTED: - return ( -
- {Status.NOT_CONNECTED}{" "} - -
- ); - default: { - status satisfies never; - } - } -}; - -export function CardCodegateStatus() { - const { data: status } = useStatus(); - - return ( - - - CodeGate Status - - - - - - CodeGate server - - - - - -
-
-
- ); -} diff --git a/src/lib/test-utils.tsx b/src/lib/test-utils.tsx index c1572da7..f6880dbf 100644 --- a/src/lib/test-utils.tsx +++ b/src/lib/test-utils.tsx @@ -1,4 +1,5 @@ import { SidebarProvider } from "@/components/ui/sidebar"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { RenderOptions, render } from "@testing-library/react"; import React from "react"; import { @@ -18,14 +19,27 @@ const renderWithProviders = ( options?: Omit & RoutConfig, ) => render( - - - {children}} - /> - - , + + + + {children}} + /> + + + , ); export * from "@testing-library/react"; From afc8bf0d9319fc58d38069bd6b4a700a5442ec2f Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 14 Jan 2025 15:58:41 +0000 Subject: [PATCH 4/6] fix: use correct referrer type for error UI links --- src/features/dashboard/components/card-codegate-status.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/features/dashboard/components/card-codegate-status.tsx b/src/features/dashboard/components/card-codegate-status.tsx index fa1c89ba..82d12779 100644 --- a/src/features/dashboard/components/card-codegate-status.tsx +++ b/src/features/dashboard/components/card-codegate-status.tsx @@ -111,6 +111,8 @@ function ErrorUI() { Discord {" "} @@ -118,6 +120,8 @@ function ErrorUI() { Github issue From 676a55851856b25e87b176efe8923810fbd3dcd0 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 14 Jan 2025 16:01:57 +0000 Subject: [PATCH 5/6] fix: add default health-check endpoint handler --- src/mocks/msw/handlers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts index f52db2b0..a2277a5a 100644 --- a/src/mocks/msw/handlers.ts +++ b/src/mocks/msw/handlers.ts @@ -3,6 +3,7 @@ import mockedPrompts from "@/mocks/msw/fixtures/GET_MESSAGES.json"; import mockedAlerts from "@/mocks/msw/fixtures/GET_ALERTS.json"; export const handlers = [ + http.get("*/health", () => HttpResponse.json({ status: "healthy" })), http.get("*/dashboard/messages", () => { return HttpResponse.json(mockedPrompts); }), From 2991c276463e9f3eab4c7a3ebb2f2d3103a3808c Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 14 Jan 2025 16:08:32 +0000 Subject: [PATCH 6/6] test: fix failing tests after introducing second table to dashboard page --- src/components/Dashboard.tsx | 2 +- src/components/__tests__/Dashboard.test.tsx | 26 ++++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index db39b86d..938a2fac 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -189,7 +189,7 @@ export function Dashboard() {
- +
Trigger Type diff --git a/src/components/__tests__/Dashboard.test.tsx b/src/components/__tests__/Dashboard.test.tsx index 456100d9..1be53c1a 100644 --- a/src/components/__tests__/Dashboard.test.tsx +++ b/src/components/__tests__/Dashboard.test.tsx @@ -154,19 +154,21 @@ describe("Dashboard", () => { ).toBeVisible(); expect(screen.getByRole("searchbox")).toBeVisible(); - const row = screen.getAllByRole("row")[1] as HTMLElement; + const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole( + "row", + )[1] as HTMLElement; + const secondRow = within(screen.getByTestId("alerts-table")).getAllByRole( + "row", + )[2] as HTMLElement; - expect(within(row).getByText(/ghp_token/i)).toBeVisible(); - expect(within(row).getByText(/codegate-secrets/i)).toBeVisible(); - expect(within(row).getAllByText(/n\/a/i).length).toEqual(2); - expect(within(row).getByText(/2025\/01\/07/i)).toBeVisible(); - expect(within(row).getByTestId(/time/i)).toBeVisible(); + expect(within(firstRow).getByText(/ghp_token/i)).toBeVisible(); + expect(within(firstRow).getByText(/codegate-secrets/i)).toBeVisible(); + expect(within(firstRow).getAllByText(/n\/a/i).length).toEqual(2); + expect(within(firstRow).getByText(/2025\/01\/07/i)).toBeVisible(); + expect(within(firstRow).getByTestId(/time/i)).toBeVisible(); // check trigger_string null - expect( - within(screen.getAllByRole("row")[2] as HTMLElement).getAllByText(/n\/a/i) - .length, - ).toEqual(3); + expect(within(secondRow).getAllByText(/n\/a/i).length).toEqual(3); }); it("should render malicious pkg", async () => { @@ -271,7 +273,9 @@ describe("Dashboard", () => { await waitFor(() => expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("1"), ); - const row = screen.getAllByRole("row")[1] as HTMLElement; + const row = within(screen.getByTestId("alerts-table")).getAllByRole( + "row", + )[1] as HTMLElement; expect(within(row).getByText(/ghp_token/i)).toBeVisible(); expect(within(row).getByText(/codegate-secrets/i)).toBeVisible(); });