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 () => {