Skip to content

Commit

Permalink
feat: implement new table design (#189)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kantord authored Jan 27, 2025
1 parent 664cfe3 commit 97bc4ea
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 140 deletions.
176 changes: 90 additions & 86 deletions src/components/AlertsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { format } from "date-fns";
import { formatDistanceToNow } from "date-fns";
import {
Cell,
Column,
Expand All @@ -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 (
<div className="bg-gray-25 rounded-lg overflow-auto p-4">
<Markdown>{data}</Markdown>
</div>
);
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 (
<div className="max-h-40 w-fit overflow-y-auto whitespace-pre-wrap p-2">
<label className="font-medium">Package:</label>
&nbsp;
<a
href={`https://www.insight.stacklok.com/report/${data.type}/${data.name}`}
target="_blank"
rel="noopener noreferrer"
className="text-brand-500 hover:underline"
>
{data.type}/{data.name}
</a>
{data.status && (
function IssueDetectedCellContent({ alert }: { alert: AlertConversation }) {
const issueDetected = getIssueDetectedType(alert);

switch (issueDetected) {
case "leaked_secret":
return (
<>
<br />
<label className="font-medium">Status:</label> {data.status}
<KeyRoundIcon className="size-4 text-blue-700" />
Blocked secret exposure
</>
)}
{data.description && (
);
case "malicious_package":
return (
<>
<br />
<label className="font-medium">Description:</label> {data.description}
<PackageX className="size-4 text-blue-700" />
Blocked malicious package
</>
)}
</div>
);
};
);
default:
return "";
}
}

export function AlertsTable() {
const {
Expand Down Expand Up @@ -161,55 +174,46 @@ export function AlertsTable() {
</div>
</div>
<div className="overflow-x-auto">
<Table data-testid="alerts-table" aria-label="Alerts table">
<TableHeader>
<Row>
<Column isRowHeader width={150}>
Trigger Type
</Column>
<Column width={300}>Trigger Token</Column>
<Column width={150}>File</Column>
<Column width={250}>Code</Column>
<Column width={100}>Timestamp</Column>
</Row>
</TableHeader>
<TableBody>
{dataView.map((alert) => (
<Row
key={alert.alert_id}
className="h-20"
onAction={() =>
navigate(`/prompt/${alert.conversation.chat_id}`)
}
>
<Cell className="truncate">{alert.trigger_type}</Cell>
<Cell className="overflow-auto whitespace-nowrap max-w-80">
{wrapObjectOutput(alert.trigger_string)}
</Cell>
<Cell className="truncate">
{alert.code_snippet?.filepath || "N/A"}
</Cell>
<Cell className="overflow-auto whitespace-nowrap max-w-80">
{alert.code_snippet?.code ? (
<pre className="max-h-40 overflow-auto bg-gray-100 p-2 whitespace-pre-wrap">
<code>{alert.code_snippet.code}</code>
</pre>
) : (
"N/A"
)}
</Cell>
<Cell className="truncate">
<div data-testid="date">
{format(new Date(alert.timestamp ?? ""), "y/MM/dd")}
</div>
<div data-testid="time">
{format(new Date(alert.timestamp ?? ""), "hh:mm:ss a")}
</div>
</Cell>
<ResizableTableContainer>
<Table data-testid="alerts-table" aria-label="Alerts table">
<TableHeader>
<Row>
<Column isRowHeader width={150}>
Time
</Column>
<Column width={150}>Type</Column>
<Column>Event</Column>
<Column width={325}>Issue Detected</Column>
</Row>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{dataView.map((alert) => (
<Row
key={alert.alert_id}
className="h-20"
onAction={() =>
navigate(`/prompt/${alert.conversation.chat_id}`)
}
>
<Cell className="truncate">
{formatDistanceToNow(new Date(alert.timestamp), {
addSuffix: true,
})}
</Cell>
<Cell className="truncate">
<TypeCellContent alert={alert} />
</Cell>
<Cell className="truncate">{getTitle(alert)}</Cell>
<Cell>
<div className="truncate flex gap-2 items-center">
<IssueDetectedCellContent alert={alert} />
</div>
</Cell>
</Row>
))}
</TableBody>
</Table>
</ResizableTableContainer>
</div>

<div className="flex justify-center w-full p-4">
Expand Down
12 changes: 12 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
92 changes: 38 additions & 54 deletions src/routes/__tests__/route-dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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 () => {
Expand All @@ -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(<RouteDashboard />);

await waitFor(() => {
expect(screen.getByText(/are there malicious/i)).toBeVisible();
});
});

it("should filter by malicious pkg", async () => {
mockAlertsWithMaliciousPkg();
render(<RouteDashboard />);
Expand All @@ -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", {
Expand All @@ -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", {
Expand All @@ -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");

Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down

0 comments on commit 97bc4ea

Please sign in to comment.