Skip to content

Commit fb5820a

Browse files
committed
refactor: extract <AlertsTable />
1 parent 0baa10b commit fb5820a

File tree

2 files changed

+210
-189
lines changed

2 files changed

+210
-189
lines changed

src/components/AlertsTable.tsx

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { format } from "date-fns";
2+
import {
3+
Cell,
4+
Column,
5+
Input,
6+
Row,
7+
SearchField,
8+
Table,
9+
TableBody,
10+
FieldGroup,
11+
TableHeader,
12+
SearchFieldClearButton,
13+
Badge,
14+
} from "@stacklok/ui-kit";
15+
import { Switch } from "@stacklok/ui-kit";
16+
import { AlertConversation } from "@/api/generated";
17+
import { Tooltip, TooltipTrigger } from "@stacklok/ui-kit";
18+
import { getMaliciousPackage } from "@/lib/utils";
19+
import { Search } from "lucide-react";
20+
import { Markdown } from "./Markdown";
21+
import { useAlertSearch } from "@/hooks/useAlertSearch";
22+
import { useCallback } from "react";
23+
import { useSearchParams } from "react-router-dom";
24+
import { useFilteredAlerts } from "@/hooks/useAlertsData";
25+
26+
const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => {
27+
const data = getMaliciousPackage(input);
28+
if (data === null) return "N/A";
29+
if (typeof data === "string") {
30+
return (
31+
<div className="bg-gray-25 rounded-lg overflow-auto p-4">
32+
<Markdown>{data}</Markdown>
33+
</div>
34+
);
35+
}
36+
if (!data.type || !data.name) return "N/A";
37+
38+
return (
39+
<div className="max-h-40 w-fit overflow-y-auto whitespace-pre-wrap p-2">
40+
<label className="font-medium">Package:</label>
41+
&nbsp;
42+
<a
43+
href={`https://www.insight.stacklok.com/report/${data.type}/${data.name}`}
44+
target="_blank"
45+
rel="noopener noreferrer"
46+
className="text-brand-500 hover:underline"
47+
>
48+
{data.type}/{data.name}
49+
</a>
50+
{data.status && (
51+
<>
52+
<br />
53+
<label className="font-medium">Status:</label> {data.status}
54+
</>
55+
)}
56+
{data.description && (
57+
<>
58+
<br />
59+
<label className="font-medium">Description:</label> {data.description}
60+
</>
61+
)}
62+
</div>
63+
);
64+
};
65+
66+
export function AlertsTable() {
67+
const {
68+
isMaliciousFilterActive,
69+
setIsMaliciousFilterActive,
70+
setSearch,
71+
search,
72+
} = useAlertSearch();
73+
const [searchParams, setSearchParams] = useSearchParams();
74+
const { data: filteredAlerts = [] } = useFilteredAlerts();
75+
76+
const handleToggleFilter = useCallback(
77+
(isChecked: boolean) => {
78+
if (isChecked) {
79+
searchParams.set("maliciousPkg", "true");
80+
searchParams.delete("search");
81+
setSearch("");
82+
} else {
83+
searchParams.delete("maliciousPkg");
84+
}
85+
setSearchParams(searchParams);
86+
setIsMaliciousFilterActive(isChecked);
87+
},
88+
[setSearchParams, setSearch, searchParams, setIsMaliciousFilterActive],
89+
);
90+
91+
const handleSearch = useCallback(
92+
(value: string) => {
93+
if (value) {
94+
searchParams.set("search", value);
95+
searchParams.delete("maliciousPkg");
96+
setSearch(value);
97+
setIsMaliciousFilterActive(false);
98+
} else {
99+
searchParams.delete("search");
100+
setSearch("");
101+
}
102+
setSearchParams(searchParams);
103+
},
104+
[searchParams, setIsMaliciousFilterActive, setSearch, setSearchParams],
105+
);
106+
107+
return (
108+
<>
109+
<div className="flex mb-2 mx-2 justify-between w-[calc(100vw-20rem)]">
110+
<div className="flex gap-2 items-center">
111+
<h2 className="font-bold text-lg">All Alerts</h2>
112+
<Badge size="sm" variant="inverted" data-testid="alerts-count">
113+
{filteredAlerts.length}
114+
</Badge>
115+
</div>
116+
117+
<div className="flex items-center gap-8">
118+
<div className="flex items-center space-x-2">
119+
<TooltipTrigger>
120+
<Switch
121+
id="malicious-packages"
122+
isSelected={isMaliciousFilterActive}
123+
onChange={handleToggleFilter}
124+
>
125+
Malicious Packages
126+
</Switch>
127+
128+
<Tooltip>
129+
<p>Filter by malicious packages</p>
130+
</Tooltip>
131+
</TooltipTrigger>
132+
</div>
133+
<SearchField
134+
type="text"
135+
aria-label="Search alerts"
136+
value={search}
137+
onChange={(value) => handleSearch(value.toLowerCase().trim())}
138+
>
139+
<FieldGroup>
140+
<Input
141+
type="search"
142+
placeholder="Search..."
143+
isBorderless
144+
icon={<Search />}
145+
/>
146+
<SearchFieldClearButton />
147+
</FieldGroup>
148+
</SearchField>
149+
</div>
150+
</div>
151+
<div className="overflow-x-auto">
152+
<Table data-testid="alerts-table" aria-label="Alerts table">
153+
<TableHeader>
154+
<Row>
155+
<Column isRowHeader width={150}>
156+
Trigger Type
157+
</Column>
158+
<Column width={300}>Trigger Token</Column>
159+
<Column width={150}>File</Column>
160+
<Column width={250}>Code</Column>
161+
<Column width={100}>Timestamp</Column>
162+
</Row>
163+
</TableHeader>
164+
<TableBody>
165+
{filteredAlerts
166+
.sort(
167+
(a, b) =>
168+
new Date(b.timestamp).getTime() -
169+
new Date(a.timestamp).getTime(),
170+
)
171+
.map((alert) => (
172+
<Row key={alert.alert_id} className="h-20">
173+
<Cell className="truncate">{alert.trigger_type}</Cell>
174+
<Cell className="overflow-auto whitespace-nowrap max-w-80">
175+
{wrapObjectOutput(alert.trigger_string)}
176+
</Cell>
177+
<Cell className="truncate">
178+
{alert.code_snippet?.filepath || "N/A"}
179+
</Cell>
180+
<Cell className="overflow-auto whitespace-nowrap max-w-80">
181+
{alert.code_snippet?.code ? (
182+
<pre className="max-h-40 overflow-auto bg-gray-100 p-2 whitespace-pre-wrap">
183+
<code>{alert.code_snippet.code}</code>
184+
</pre>
185+
) : (
186+
"N/A"
187+
)}
188+
</Cell>
189+
<Cell className="truncate">
190+
<div data-testid="date">
191+
{format(new Date(alert.timestamp ?? ""), "y/MM/dd")}
192+
</div>
193+
<div data-testid="time">
194+
{format(new Date(alert.timestamp ?? ""), "hh:mm:ss a")}
195+
</div>
196+
</Cell>
197+
</Row>
198+
))}
199+
</TableBody>
200+
</Table>
201+
</div>
202+
</>
203+
);
204+
}

0 commit comments

Comments
 (0)