Skip to content

Commit acf1293

Browse files
refactor(alerts): consolidate queries (#233)
* fix(alerts): memoize selecting alerts * refactor(alerts): consolidate queries
1 parent f810e2b commit acf1293

19 files changed

+278
-315
lines changed

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"tailwind-merge": "^2.5.5",
4545
"tailwind-variants": "^0.3.0",
4646
"tailwindcss-animate": "^1.0.7",
47+
"zod": "^3.24.1",
4748
"zustand": "^5.0.3"
4849
},
4950
"devDependencies": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
FieldGroup,
3+
Input,
4+
SearchField,
5+
SearchFieldClearButton,
6+
} from "@stacklok/ui-kit";
7+
import { useAlertsFilterSearchParams } from "../hooks/use-alerts-filter-search-params";
8+
import { SearchMd } from "@untitled-ui/icons-react";
9+
10+
export function SearchFieldAlerts() {
11+
const { setSearch, state } = useAlertsFilterSearchParams();
12+
13+
return (
14+
<SearchField
15+
type="text"
16+
aria-label="Search alerts"
17+
value={state.search ?? ""}
18+
onChange={(value) => setSearch(value.toLowerCase().trim())}
19+
>
20+
<FieldGroup>
21+
<Input
22+
type="search"
23+
placeholder="Search..."
24+
isBorderless
25+
icon={<SearchMd />}
26+
/>
27+
<SearchFieldClearButton />
28+
</FieldGroup>
29+
</SearchField>
30+
);
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { TooltipTrigger, Switch, Tooltip } from "@stacklok/ui-kit";
2+
import {
3+
AlertsFilterView,
4+
useAlertsFilterSearchParams,
5+
} from "../hooks/use-alerts-filter-search-params";
6+
7+
export function SwitchMaliciousAlertsFilter() {
8+
const { setView, state } = useAlertsFilterSearchParams();
9+
10+
return (
11+
<TooltipTrigger>
12+
<Switch
13+
id="malicious-packages"
14+
isSelected={state.view === AlertsFilterView.MALICIOUS}
15+
onChange={(isSelected) => {
16+
switch (isSelected) {
17+
case true:
18+
return setView(AlertsFilterView.MALICIOUS);
19+
case false:
20+
return setView(AlertsFilterView.ALL);
21+
default:
22+
return isSelected satisfies never;
23+
}
24+
}}
25+
>
26+
Malicious Packages
27+
</Switch>
28+
29+
<Tooltip>
30+
<p>Filter by malicious packages</p>
31+
</Tooltip>
32+
</TooltipTrigger>
33+
);
34+
}

src/features/alerts/components/table-alerts.tsx

+9-83
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,28 @@ import { formatDistanceToNow } from "date-fns";
22
import {
33
Cell,
44
Column,
5-
Input,
65
Row,
7-
SearchField,
86
Table,
97
TableBody,
10-
FieldGroup,
118
TableHeader,
12-
SearchFieldClearButton,
139
Badge,
1410
Button,
1511
ResizableTableContainer,
1612
} from "@stacklok/ui-kit";
17-
import { Switch } from "@stacklok/ui-kit";
1813
import { AlertConversation, QuestionType } from "@/api/generated";
19-
import { Tooltip, TooltipTrigger } from "@stacklok/ui-kit";
2014
import {
2115
sanitizeQuestionPrompt,
2216
parsingPromptText,
2317
getIssueDetectedType,
2418
} from "@/lib/utils";
2519
import { useAlertSearch } from "@/hooks/useAlertSearch";
26-
import { useCallback } from "react";
27-
import { useNavigate, useSearchParams } from "react-router-dom";
28-
import { useFilteredAlerts } from "@/hooks/useAlertsData";
20+
import { useNavigate } from "react-router-dom";
2921
import { useClientSidePagination } from "@/hooks/useClientSidePagination";
3022
import { TableAlertTokenUsage } from "./table-alert-token-usage";
31-
import { Key01, PackageX, SearchMd } from "@untitled-ui/icons-react";
23+
import { Key01, PackageX } from "@untitled-ui/icons-react";
24+
import { SearchFieldAlerts } from "./search-field-alerts";
25+
import { useQueryGetWorkspaceAlertTable } from "../hooks/use-query-get-workspace-alerts-table";
26+
import { SwitchMaliciousAlertsFilter } from "./switch-malicious-alerts-filter";
3227

3328
const getTitle = (alert: AlertConversation) => {
3429
const prompt = alert.conversation;
@@ -80,56 +75,16 @@ function IssueDetectedCellContent({ alert }: { alert: AlertConversation }) {
8075
}
8176

8277
export function TableAlerts() {
83-
const {
84-
isMaliciousFilterActive,
85-
setIsMaliciousFilterActive,
86-
setSearch,
87-
search,
88-
page,
89-
nextPage,
90-
prevPage,
91-
} = useAlertSearch();
78+
const { page, nextPage, prevPage } = useAlertSearch();
9279
const navigate = useNavigate();
93-
const [searchParams, setSearchParams] = useSearchParams();
94-
const { data: filteredAlerts = [] } = useFilteredAlerts();
80+
const { data: filteredAlerts = [] } = useQueryGetWorkspaceAlertTable();
9581

9682
const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination(
9783
filteredAlerts,
9884
page,
9985
15,
10086
);
10187

102-
const handleToggleFilter = useCallback(
103-
(isChecked: boolean) => {
104-
if (isChecked) {
105-
searchParams.set("maliciousPkg", "true");
106-
searchParams.delete("search");
107-
setSearch("");
108-
} else {
109-
searchParams.delete("maliciousPkg");
110-
}
111-
setSearchParams(searchParams);
112-
setIsMaliciousFilterActive(isChecked);
113-
},
114-
[setSearchParams, setSearch, searchParams, setIsMaliciousFilterActive],
115-
);
116-
117-
const handleSearch = useCallback(
118-
(value: string) => {
119-
if (value) {
120-
searchParams.set("search", value);
121-
searchParams.delete("maliciousPkg");
122-
setSearch(value);
123-
setIsMaliciousFilterActive(false);
124-
} else {
125-
searchParams.delete("search");
126-
setSearch("");
127-
}
128-
setSearchParams(searchParams);
129-
},
130-
[searchParams, setIsMaliciousFilterActive, setSearch, setSearchParams],
131-
);
132-
13388
return (
13489
<>
13590
<div className="flex mb-2 mx-2 justify-between w-[calc(100vw-20rem)]">
@@ -141,37 +96,8 @@ export function TableAlerts() {
14196
</div>
14297

14398
<div className="flex items-center gap-8">
144-
<div className="flex items-center space-x-2">
145-
<TooltipTrigger>
146-
<Switch
147-
id="malicious-packages"
148-
isSelected={isMaliciousFilterActive}
149-
onChange={handleToggleFilter}
150-
>
151-
Malicious Packages
152-
</Switch>
153-
154-
<Tooltip>
155-
<p>Filter by malicious packages</p>
156-
</Tooltip>
157-
</TooltipTrigger>
158-
</div>
159-
<SearchField
160-
type="text"
161-
aria-label="Search alerts"
162-
value={search}
163-
onChange={(value) => handleSearch(value.toLowerCase().trim())}
164-
>
165-
<FieldGroup>
166-
<Input
167-
type="search"
168-
placeholder="Search..."
169-
isBorderless
170-
icon={<SearchMd />}
171-
/>
172-
<SearchFieldClearButton />
173-
</FieldGroup>
174-
</SearchField>
99+
<SwitchMaliciousAlertsFilter />
100+
<SearchFieldAlerts />
175101
</div>
176102
</div>
177103
<div className="overflow-x-auto">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useCallback } from "react";
2+
import { useSearchParams } from "react-router-dom";
3+
import { z } from "zod";
4+
5+
export enum AlertsFilterView {
6+
ALL = "all",
7+
MALICIOUS = "malicious",
8+
SECRETS = "secrets",
9+
}
10+
11+
const alertsFilterSchema = z.object({
12+
search: z.string().optional(),
13+
view: z.nativeEnum(AlertsFilterView),
14+
});
15+
16+
type AlertsFilterSchema = z.output<typeof alertsFilterSchema>;
17+
18+
const DEFAULT_FILTER: AlertsFilterSchema = {
19+
view: AlertsFilterView.ALL,
20+
};
21+
22+
export const useAlertsFilterSearchParams = () => {
23+
const [searchParams, setSearchParams] = useSearchParams(
24+
new URLSearchParams(DEFAULT_FILTER),
25+
);
26+
27+
const setView = useCallback(
28+
(view: AlertsFilterView) => {
29+
setSearchParams((prev) => {
30+
if (view) prev.set("view", view);
31+
if (!view) prev.delete("view");
32+
return prev;
33+
});
34+
},
35+
[setSearchParams],
36+
);
37+
38+
const setSearch = useCallback(
39+
(query: string | null) => {
40+
setSearchParams((prev) => {
41+
if (query !== null) prev.set("search", query);
42+
if (query == null || query === "") prev.delete("search");
43+
return prev;
44+
});
45+
},
46+
[setSearchParams],
47+
);
48+
49+
const state = alertsFilterSchema.parse(Object.fromEntries(searchParams));
50+
51+
return { state, setView, setSearch };
52+
};

src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import {
2-
AlertConversation,
3-
V1GetWorkspaceAlertsResponse,
4-
} from "@/api/generated";
5-
import { filterAlertsCritical } from "../lib/filter-alerts-critical";
1+
import { V1GetWorkspaceAlertsResponse } from "@/api/generated";
62
import { isAlertMalicious } from "../lib/is-alert-malicious";
73
import { useQueryGetWorkspaceAlerts } from "./use-query-get-workspace-alerts";
4+
import { multiFilter } from "@/lib/multi-filter";
5+
import { isAlertCritical } from "../lib/is-alert-critical";
86

97
// NOTE: This needs to be a stable function reference to enable memo-isation of
108
// the select operation on each React re-render.
11-
function select(data: V1GetWorkspaceAlertsResponse): AlertConversation[] {
12-
return filterAlertsCritical(data).filter(isAlertMalicious);
9+
function select(data: V1GetWorkspaceAlertsResponse) {
10+
return multiFilter(data, [isAlertCritical, isAlertMalicious]);
1311
}
1412

1513
export function useQueryGetWorkspaceAlertsMaliciousPkg() {

src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import {
2-
V1GetWorkspaceAlertsResponse,
3-
AlertConversation,
4-
} from "@/api/generated";
5-
import { filterAlertsCritical } from "../lib/filter-alerts-critical";
1+
import { V1GetWorkspaceAlertsResponse } from "@/api/generated";
62
import { isAlertSecret } from "../lib/is-alert-secret";
73
import { useQueryGetWorkspaceAlerts } from "./use-query-get-workspace-alerts";
4+
import { multiFilter } from "@/lib/multi-filter";
5+
import { isAlertCritical } from "../lib/is-alert-critical";
86

97
// NOTE: This needs to be a stable function reference to enable memo-isation of
10-
// the select operation on each React re-render.
11-
function select(data: V1GetWorkspaceAlertsResponse): AlertConversation[] {
12-
return filterAlertsCritical(data).filter(isAlertSecret);
8+
// the select operation on each React re-render
9+
function select(data: V1GetWorkspaceAlertsResponse) {
10+
return multiFilter(data, [isAlertCritical, isAlertSecret]);
1311
}
1412

1513
export function useQueryGetWorkspaceAlertSecrets() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { V1GetWorkspaceAlertsResponse } from "@/api/generated";
2+
import { useQueryGetWorkspaceAlerts } from "./use-query-get-workspace-alerts";
3+
import { useCallback } from "react";
4+
import {
5+
AlertsFilterView,
6+
useAlertsFilterSearchParams,
7+
} from "./use-alerts-filter-search-params";
8+
import { multiFilter } from "@/lib/multi-filter";
9+
import { isAlertCritical } from "../lib/is-alert-critical";
10+
import { isAlertMalicious } from "../lib/is-alert-malicious";
11+
import { isAlertSecret } from "../lib/is-alert-secret";
12+
import { doesAlertIncludeSearch } from "../lib/does-alert-include-search";
13+
import { isAlertConversation } from "../lib/is-alert-conversation";
14+
15+
const FILTER: Record<
16+
AlertsFilterView,
17+
(alert: V1GetWorkspaceAlertsResponse[number]) => boolean
18+
> = {
19+
all: () => true,
20+
malicious: isAlertMalicious,
21+
secrets: isAlertSecret,
22+
};
23+
24+
export function useQueryGetWorkspaceAlertTable() {
25+
const { state } = useAlertsFilterSearchParams();
26+
27+
// NOTE: This needs to be a stable function reference to enable memo-isation
28+
// of the select operation on each React re-render.
29+
const select = useCallback(
30+
(data: V1GetWorkspaceAlertsResponse) => {
31+
return multiFilter(data, [
32+
isAlertCritical,
33+
isAlertConversation,
34+
FILTER[state.view],
35+
]).filter((alert) => doesAlertIncludeSearch(alert, state.search ?? null));
36+
},
37+
[state.search, state.view],
38+
);
39+
40+
return useQueryGetWorkspaceAlerts({
41+
select,
42+
});
43+
}

0 commit comments

Comments
 (0)