From 97bc4ea104cbd88e81dbd1aadd055fc04687f977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20K=C3=A1ntor?= Date: Mon, 27 Jan 2025 13:07:57 +0100 Subject: [PATCH] feat: implement new table design (#189) * rename timestamp coloumn to time * make 'time' the first column * use relative date time format in alerts table * rename trigger type column to type * clamp trigger markdown to a single lineoverflowoverflow * remove code and file columns from alerts table * rename trigger token column to event * . * implement event column content correctly * implement type column values * add note about ongoing discussion on message type * fix mapping alerts type * display detected problem properly --- src/components/AlertsTable.tsx | 176 +++++++++--------- src/lib/utils.ts | 12 ++ src/routes/__tests__/route-dashboard.test.tsx | 92 ++++----- 3 files changed, 140 insertions(+), 140 deletions(-) diff --git a/src/components/AlertsTable.tsx b/src/components/AlertsTable.tsx index 0db2c444..7483727a 100644 --- a/src/components/AlertsTable.tsx +++ b/src/components/AlertsTable.tsx @@ -1,4 +1,4 @@ -import { format } from "date-fns"; +import { formatDistanceToNow } from "date-fns"; import { Cell, Column, @@ -12,58 +12,71 @@ import { SearchFieldClearButton, Badge, Button, + ResizableTableContainer, } from "@stacklok/ui-kit"; import { Switch } from "@stacklok/ui-kit"; -import { AlertConversation } from "@/api/generated"; +import { AlertConversation, QuestionType } from "@/api/generated"; import { Tooltip, TooltipTrigger } from "@stacklok/ui-kit"; -import { getMaliciousPackage } from "@/lib/utils"; -import { Search } from "lucide-react"; -import { Markdown } from "./Markdown"; +import { + sanitizeQuestionPrompt, + parsingPromptText, + getIssueDetectedType, +} from "@/lib/utils"; +import { KeyRoundIcon, PackageX, Search } from "lucide-react"; import { useAlertSearch } from "@/hooks/useAlertSearch"; import { useCallback } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useFilteredAlerts } from "@/hooks/useAlertsData"; import { useClientSidePagination } from "@/hooks/useClientSidePagination"; -const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => { - const data = getMaliciousPackage(input); - if (data === null) return "N/A"; - if (typeof data === "string") { - return ( -
- {data} -
- ); +const getTitle = (alert: AlertConversation) => { + const prompt = alert.conversation; + const title = parsingPromptText( + sanitizeQuestionPrompt({ + question: prompt.question_answers?.[0]?.question.message ?? "", + answer: prompt.question_answers?.[0]?.answer?.message ?? "", + }), + prompt.conversation_timestamp, + ); + + return title; +}; + +function TypeCellContent({ alert }: { alert: AlertConversation }) { + const conversationType = alert.conversation.type; + + switch (conversationType) { + case QuestionType.CHAT: + return "Chat"; + case QuestionType.FIM: + return "Code Suggestion"; + default: + return "Unknown"; } - if (!data.type || !data.name) return "N/A"; +} - return ( -
- -   - - {data.type}/{data.name} - - {data.status && ( +function IssueDetectedCellContent({ alert }: { alert: AlertConversation }) { + const issueDetected = getIssueDetectedType(alert); + + switch (issueDetected) { + case "leaked_secret": + return ( <> -
- {data.status} + + Blocked secret exposure - )} - {data.description && ( + ); + case "malicious_package": + return ( <> -
- {data.description} + + Blocked malicious package - )} -
- ); -}; + ); + default: + return ""; + } +} export function AlertsTable() { const { @@ -161,55 +174,46 @@ export function AlertsTable() {
- - - - - Trigger Type - - Trigger Token - File - Code - Timestamp - - - - {dataView.map((alert) => ( - - navigate(`/prompt/${alert.conversation.chat_id}`) - } - > - {alert.trigger_type} - - {wrapObjectOutput(alert.trigger_string)} - - - {alert.code_snippet?.filepath || "N/A"} - - - {alert.code_snippet?.code ? ( -
-                      {alert.code_snippet.code}
-                    
- ) : ( - "N/A" - )} -
- -
- {format(new Date(alert.timestamp ?? ""), "y/MM/dd")} -
-
- {format(new Date(alert.timestamp ?? ""), "hh:mm:ss a")} -
-
+ +
+ + + + Time + + Type + Event + Issue Detected - ))} - -
+ + + {dataView.map((alert) => ( + + navigate(`/prompt/${alert.conversation.chat_id}`) + } + > + + {formatDistanceToNow(new Date(alert.timestamp), { + addSuffix: true, + })} + + + + + {getTitle(alert)} + +
+ +
+
+
+ ))} +
+ +
diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 886881d1..262d785e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -175,3 +175,15 @@ export function getMaliciousPackage( return null; } + +export function getIssueDetectedType( + alert: AlertConversation, +): "malicious_package" | "leaked_secret" { + const maliciousPackage = getMaliciousPackage(alert.trigger_string); + + if (maliciousPackage !== null && typeof maliciousPackage === "object") { + return "malicious_package"; + } + + return "leaked_secret"; +} diff --git a/src/routes/__tests__/route-dashboard.test.tsx b/src/routes/__tests__/route-dashboard.test.tsx index 1f6d30bf..0676cb20 100644 --- a/src/routes/__tests__/route-dashboard.test.tsx +++ b/src/routes/__tests__/route-dashboard.test.tsx @@ -141,28 +141,24 @@ describe("Dashboard", () => { expect( screen.getByRole("columnheader", { - name: /trigger type/i, + name: /type/i, }), ).toBeVisible(); expect( screen.getByRole("columnheader", { - name: /trigger token/i, + name: /event/i, }), ).toBeVisible(); expect( screen.getByRole("columnheader", { - name: /file/i, - }), - ).toBeVisible(); - expect( - screen.getByRole("columnheader", { - name: /code/i, + name: /time/i, }), ).toBeVisible(); + expect( screen.getByRole("columnheader", { - name: /timestamp/i, + name: /issue detected/i, }), ).toBeVisible(); @@ -176,18 +172,14 @@ describe("Dashboard", () => { 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(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\/14/i)).toBeVisible(); - expect(within(firstRow).getByTestId(/time/i)).toBeVisible(); - - // check trigger_string null - expect(within(secondRow).getAllByText(/n\/a/i).length).toEqual(3); + expect(within(firstRow).getByText(/chat/i)).toBeVisible(); + expect(within(firstRow).getByText(/[0-9]+.*ago/i)).toBeVisible(); + expect( + screen.getAllByRole("gridcell", { + name: /blocked secret exposure/i, + }).length, + ).toBeGreaterThanOrEqual(1); }); it("should render malicious pkg", async () => { @@ -208,20 +200,22 @@ describe("Dashboard", () => { ), ).toBeVisible(); - expect(screen.getByText(/package:/i)).toBeVisible(); expect( - screen.getByRole("link", { - name: /pypi\/invokehttp/i, + screen.getByRole("gridcell", { + name: /blocked malicious package/i, }), - ).toHaveAttribute( - "href", - "https://www.insight.stacklok.com/report/pypi/invokehttp", - ); - expect( - screen.getByText(/malicious python http for humans\./i), ).toBeVisible(); }); + it("renders event column", async () => { + mockAlertsWithMaliciousPkg(); + render(); + + await waitFor(() => { + expect(screen.getByText(/are there malicious/i)).toBeVisible(); + }); + }); + it("should filter by malicious pkg", async () => { mockAlertsWithMaliciousPkg(); render(); @@ -232,10 +226,10 @@ describe("Dashboard", () => { expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("2"); expect( - screen.getByRole("row", { - name: /codegate-secrets/i, - }), - ).toBeVisible(); + screen.getAllByRole("gridcell", { + name: /chat/i, + }).length, + ).toBeGreaterThanOrEqual(1); userEvent.click( screen.getByRole("switch", { @@ -247,15 +241,11 @@ describe("Dashboard", () => { expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("1"), ); - expect(screen.getByText(/package:/i)).toBeVisible(); expect( - screen.getByRole("link", { - name: /pypi\/invokehttp/i, - }), - ).toBeVisible(); - expect( - screen.getByText(/malicious python http for humans\./i), - ).toBeVisible(); + screen.queryAllByRole("gridcell", { + name: /blocked secret exposure/i, + }).length, + ).toBe(0); userEvent.click( screen.getByRole("switch", { @@ -277,15 +267,10 @@ describe("Dashboard", () => { expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("2"); expect( - screen.getByRole("row", { - name: /codegate-secrets/i, - }), - ).toBeVisible(); - expect( - screen.getByRole("row", { - name: /codegate-context-retriever/i, - }), - ).toBeVisible(); + screen.getAllByRole("gridcell", { + name: /chat/i, + }).length, + ).toBeGreaterThanOrEqual(1); await userEvent.type(screen.getByRole("searchbox"), "codegate-secrets"); @@ -295,8 +280,7 @@ describe("Dashboard", () => { 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(); + expect(within(row).getByText(/chat/i)).toBeVisible(); }); it("should sort alerts by date desc", async () => { @@ -312,8 +296,8 @@ describe("Dashboard", () => { "row", )[2] as HTMLElement; - expect(within(firstRow).getByText(/2025\/01\/14/i)).toBeVisible(); - expect(within(secondRow).getByText(/2025\/01\/07/i)).toBeVisible(); + expect(within(firstRow).getByText(/[0-9]+.*ago/i)).toBeVisible(); + expect(within(secondRow).getByText(/[0-9]+.*ago/i)).toBeVisible(); }); it("only displays a limited number of items in the table", async () => {