Skip to content

Commit 97bc4ea

Browse files
authored
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
1 parent 664cfe3 commit 97bc4ea

File tree

3 files changed

+140
-140
lines changed

3 files changed

+140
-140
lines changed

src/components/AlertsTable.tsx

+90-86
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { format } from "date-fns";
1+
import { formatDistanceToNow } from "date-fns";
22
import {
33
Cell,
44
Column,
@@ -12,58 +12,71 @@ import {
1212
SearchFieldClearButton,
1313
Badge,
1414
Button,
15+
ResizableTableContainer,
1516
} from "@stacklok/ui-kit";
1617
import { Switch } from "@stacklok/ui-kit";
17-
import { AlertConversation } from "@/api/generated";
18+
import { AlertConversation, QuestionType } from "@/api/generated";
1819
import { Tooltip, TooltipTrigger } from "@stacklok/ui-kit";
19-
import { getMaliciousPackage } from "@/lib/utils";
20-
import { Search } from "lucide-react";
21-
import { Markdown } from "./Markdown";
20+
import {
21+
sanitizeQuestionPrompt,
22+
parsingPromptText,
23+
getIssueDetectedType,
24+
} from "@/lib/utils";
25+
import { KeyRoundIcon, PackageX, Search } from "lucide-react";
2226
import { useAlertSearch } from "@/hooks/useAlertSearch";
2327
import { useCallback } from "react";
2428
import { useNavigate, useSearchParams } from "react-router-dom";
2529
import { useFilteredAlerts } from "@/hooks/useAlertsData";
2630
import { useClientSidePagination } from "@/hooks/useClientSidePagination";
2731

28-
const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => {
29-
const data = getMaliciousPackage(input);
30-
if (data === null) return "N/A";
31-
if (typeof data === "string") {
32-
return (
33-
<div className="bg-gray-25 rounded-lg overflow-auto p-4">
34-
<Markdown>{data}</Markdown>
35-
</div>
36-
);
32+
const getTitle = (alert: AlertConversation) => {
33+
const prompt = alert.conversation;
34+
const title = parsingPromptText(
35+
sanitizeQuestionPrompt({
36+
question: prompt.question_answers?.[0]?.question.message ?? "",
37+
answer: prompt.question_answers?.[0]?.answer?.message ?? "",
38+
}),
39+
prompt.conversation_timestamp,
40+
);
41+
42+
return title;
43+
};
44+
45+
function TypeCellContent({ alert }: { alert: AlertConversation }) {
46+
const conversationType = alert.conversation.type;
47+
48+
switch (conversationType) {
49+
case QuestionType.CHAT:
50+
return "Chat";
51+
case QuestionType.FIM:
52+
return "Code Suggestion";
53+
default:
54+
return "Unknown";
3755
}
38-
if (!data.type || !data.name) return "N/A";
56+
}
3957

40-
return (
41-
<div className="max-h-40 w-fit overflow-y-auto whitespace-pre-wrap p-2">
42-
<label className="font-medium">Package:</label>
43-
&nbsp;
44-
<a
45-
href={`https://www.insight.stacklok.com/report/${data.type}/${data.name}`}
46-
target="_blank"
47-
rel="noopener noreferrer"
48-
className="text-brand-500 hover:underline"
49-
>
50-
{data.type}/{data.name}
51-
</a>
52-
{data.status && (
58+
function IssueDetectedCellContent({ alert }: { alert: AlertConversation }) {
59+
const issueDetected = getIssueDetectedType(alert);
60+
61+
switch (issueDetected) {
62+
case "leaked_secret":
63+
return (
5364
<>
54-
<br />
55-
<label className="font-medium">Status:</label> {data.status}
65+
<KeyRoundIcon className="size-4 text-blue-700" />
66+
Blocked secret exposure
5667
</>
57-
)}
58-
{data.description && (
68+
);
69+
case "malicious_package":
70+
return (
5971
<>
60-
<br />
61-
<label className="font-medium">Description:</label> {data.description}
72+
<PackageX className="size-4 text-blue-700" />
73+
Blocked malicious package
6274
</>
63-
)}
64-
</div>
65-
);
66-
};
75+
);
76+
default:
77+
return "";
78+
}
79+
}
6780

6881
export function AlertsTable() {
6982
const {
@@ -161,55 +174,46 @@ export function AlertsTable() {
161174
</div>
162175
</div>
163176
<div className="overflow-x-auto">
164-
<Table data-testid="alerts-table" aria-label="Alerts table">
165-
<TableHeader>
166-
<Row>
167-
<Column isRowHeader width={150}>
168-
Trigger Type
169-
</Column>
170-
<Column width={300}>Trigger Token</Column>
171-
<Column width={150}>File</Column>
172-
<Column width={250}>Code</Column>
173-
<Column width={100}>Timestamp</Column>
174-
</Row>
175-
</TableHeader>
176-
<TableBody>
177-
{dataView.map((alert) => (
178-
<Row
179-
key={alert.alert_id}
180-
className="h-20"
181-
onAction={() =>
182-
navigate(`/prompt/${alert.conversation.chat_id}`)
183-
}
184-
>
185-
<Cell className="truncate">{alert.trigger_type}</Cell>
186-
<Cell className="overflow-auto whitespace-nowrap max-w-80">
187-
{wrapObjectOutput(alert.trigger_string)}
188-
</Cell>
189-
<Cell className="truncate">
190-
{alert.code_snippet?.filepath || "N/A"}
191-
</Cell>
192-
<Cell className="overflow-auto whitespace-nowrap max-w-80">
193-
{alert.code_snippet?.code ? (
194-
<pre className="max-h-40 overflow-auto bg-gray-100 p-2 whitespace-pre-wrap">
195-
<code>{alert.code_snippet.code}</code>
196-
</pre>
197-
) : (
198-
"N/A"
199-
)}
200-
</Cell>
201-
<Cell className="truncate">
202-
<div data-testid="date">
203-
{format(new Date(alert.timestamp ?? ""), "y/MM/dd")}
204-
</div>
205-
<div data-testid="time">
206-
{format(new Date(alert.timestamp ?? ""), "hh:mm:ss a")}
207-
</div>
208-
</Cell>
177+
<ResizableTableContainer>
178+
<Table data-testid="alerts-table" aria-label="Alerts table">
179+
<TableHeader>
180+
<Row>
181+
<Column isRowHeader width={150}>
182+
Time
183+
</Column>
184+
<Column width={150}>Type</Column>
185+
<Column>Event</Column>
186+
<Column width={325}>Issue Detected</Column>
209187
</Row>
210-
))}
211-
</TableBody>
212-
</Table>
188+
</TableHeader>
189+
<TableBody>
190+
{dataView.map((alert) => (
191+
<Row
192+
key={alert.alert_id}
193+
className="h-20"
194+
onAction={() =>
195+
navigate(`/prompt/${alert.conversation.chat_id}`)
196+
}
197+
>
198+
<Cell className="truncate">
199+
{formatDistanceToNow(new Date(alert.timestamp), {
200+
addSuffix: true,
201+
})}
202+
</Cell>
203+
<Cell className="truncate">
204+
<TypeCellContent alert={alert} />
205+
</Cell>
206+
<Cell className="truncate">{getTitle(alert)}</Cell>
207+
<Cell>
208+
<div className="truncate flex gap-2 items-center">
209+
<IssueDetectedCellContent alert={alert} />
210+
</div>
211+
</Cell>
212+
</Row>
213+
))}
214+
</TableBody>
215+
</Table>
216+
</ResizableTableContainer>
213217
</div>
214218

215219
<div className="flex justify-center w-full p-4">

src/lib/utils.ts

+12
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,15 @@ export function getMaliciousPackage(
175175

176176
return null;
177177
}
178+
179+
export function getIssueDetectedType(
180+
alert: AlertConversation,
181+
): "malicious_package" | "leaked_secret" {
182+
const maliciousPackage = getMaliciousPackage(alert.trigger_string);
183+
184+
if (maliciousPackage !== null && typeof maliciousPackage === "object") {
185+
return "malicious_package";
186+
}
187+
188+
return "leaked_secret";
189+
}

src/routes/__tests__/route-dashboard.test.tsx

+38-54
Original file line numberDiff line numberDiff line change
@@ -141,28 +141,24 @@ describe("Dashboard", () => {
141141

142142
expect(
143143
screen.getByRole("columnheader", {
144-
name: /trigger type/i,
144+
name: /type/i,
145145
}),
146146
).toBeVisible();
147147
expect(
148148
screen.getByRole("columnheader", {
149-
name: /trigger token/i,
149+
name: /event/i,
150150
}),
151151
).toBeVisible();
152152

153153
expect(
154154
screen.getByRole("columnheader", {
155-
name: /file/i,
156-
}),
157-
).toBeVisible();
158-
expect(
159-
screen.getByRole("columnheader", {
160-
name: /code/i,
155+
name: /time/i,
161156
}),
162157
).toBeVisible();
158+
163159
expect(
164160
screen.getByRole("columnheader", {
165-
name: /timestamp/i,
161+
name: /issue detected/i,
166162
}),
167163
).toBeVisible();
168164

@@ -176,18 +172,14 @@ describe("Dashboard", () => {
176172
const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole(
177173
"row",
178174
)[1] as HTMLElement;
179-
const secondRow = within(screen.getByTestId("alerts-table")).getAllByRole(
180-
"row",
181-
)[2] as HTMLElement;
182175

183-
expect(within(firstRow).getByText(/ghp_token/i)).toBeVisible();
184-
expect(within(firstRow).getByText(/codegate-secrets/i)).toBeVisible();
185-
expect(within(firstRow).getAllByText(/n\/a/i).length).toEqual(2);
186-
expect(within(firstRow).getByText(/2025\/01\/14/i)).toBeVisible();
187-
expect(within(firstRow).getByTestId(/time/i)).toBeVisible();
188-
189-
// check trigger_string null
190-
expect(within(secondRow).getAllByText(/n\/a/i).length).toEqual(3);
176+
expect(within(firstRow).getByText(/chat/i)).toBeVisible();
177+
expect(within(firstRow).getByText(/[0-9]+.*ago/i)).toBeVisible();
178+
expect(
179+
screen.getAllByRole("gridcell", {
180+
name: /blocked secret exposure/i,
181+
}).length,
182+
).toBeGreaterThanOrEqual(1);
191183
});
192184

193185
it("should render malicious pkg", async () => {
@@ -208,20 +200,22 @@ describe("Dashboard", () => {
208200
),
209201
).toBeVisible();
210202

211-
expect(screen.getByText(/package:/i)).toBeVisible();
212203
expect(
213-
screen.getByRole("link", {
214-
name: /pypi\/invokehttp/i,
204+
screen.getByRole("gridcell", {
205+
name: /blocked malicious package/i,
215206
}),
216-
).toHaveAttribute(
217-
"href",
218-
"https://www.insight.stacklok.com/report/pypi/invokehttp",
219-
);
220-
expect(
221-
screen.getByText(/malicious python http for humans\./i),
222207
).toBeVisible();
223208
});
224209

210+
it("renders event column", async () => {
211+
mockAlertsWithMaliciousPkg();
212+
render(<RouteDashboard />);
213+
214+
await waitFor(() => {
215+
expect(screen.getByText(/are there malicious/i)).toBeVisible();
216+
});
217+
});
218+
225219
it("should filter by malicious pkg", async () => {
226220
mockAlertsWithMaliciousPkg();
227221
render(<RouteDashboard />);
@@ -232,10 +226,10 @@ describe("Dashboard", () => {
232226

233227
expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("2");
234228
expect(
235-
screen.getByRole("row", {
236-
name: /codegate-secrets/i,
237-
}),
238-
).toBeVisible();
229+
screen.getAllByRole("gridcell", {
230+
name: /chat/i,
231+
}).length,
232+
).toBeGreaterThanOrEqual(1);
239233

240234
userEvent.click(
241235
screen.getByRole("switch", {
@@ -247,15 +241,11 @@ describe("Dashboard", () => {
247241
expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("1"),
248242
);
249243

250-
expect(screen.getByText(/package:/i)).toBeVisible();
251244
expect(
252-
screen.getByRole("link", {
253-
name: /pypi\/invokehttp/i,
254-
}),
255-
).toBeVisible();
256-
expect(
257-
screen.getByText(/malicious python http for humans\./i),
258-
).toBeVisible();
245+
screen.queryAllByRole("gridcell", {
246+
name: /blocked secret exposure/i,
247+
}).length,
248+
).toBe(0);
259249

260250
userEvent.click(
261251
screen.getByRole("switch", {
@@ -277,15 +267,10 @@ describe("Dashboard", () => {
277267

278268
expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("2");
279269
expect(
280-
screen.getByRole("row", {
281-
name: /codegate-secrets/i,
282-
}),
283-
).toBeVisible();
284-
expect(
285-
screen.getByRole("row", {
286-
name: /codegate-context-retriever/i,
287-
}),
288-
).toBeVisible();
270+
screen.getAllByRole("gridcell", {
271+
name: /chat/i,
272+
}).length,
273+
).toBeGreaterThanOrEqual(1);
289274

290275
await userEvent.type(screen.getByRole("searchbox"), "codegate-secrets");
291276

@@ -295,8 +280,7 @@ describe("Dashboard", () => {
295280
const row = within(screen.getByTestId("alerts-table")).getAllByRole(
296281
"row",
297282
)[1] as HTMLElement;
298-
expect(within(row).getByText(/ghp_token/i)).toBeVisible();
299-
expect(within(row).getByText(/codegate-secrets/i)).toBeVisible();
283+
expect(within(row).getByText(/chat/i)).toBeVisible();
300284
});
301285

302286
it("should sort alerts by date desc", async () => {
@@ -312,8 +296,8 @@ describe("Dashboard", () => {
312296
"row",
313297
)[2] as HTMLElement;
314298

315-
expect(within(firstRow).getByText(/2025\/01\/14/i)).toBeVisible();
316-
expect(within(secondRow).getByText(/2025\/01\/07/i)).toBeVisible();
299+
expect(within(firstRow).getByText(/[0-9]+.*ago/i)).toBeVisible();
300+
expect(within(secondRow).getByText(/[0-9]+.*ago/i)).toBeVisible();
317301
});
318302

319303
it("only displays a limited number of items in the table", async () => {

0 commit comments

Comments
 (0)