Skip to content

Commit 4d8e8ff

Browse files
carlosthe19916github-actions[bot]
authored andcommitted
fix: Search Page does not search on the backend while typing (#344)
(cherry picked from commit f4f4321)
1 parent 2bb13e3 commit 4d8e8ff

File tree

8 files changed

+114
-399
lines changed

8 files changed

+114
-399
lines changed

client/src/app/pages/search/components/SearchMenu.tsx

+101-77
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import { HubRequestParams } from "@app/api/models";
2-
import { FILTER_TEXT_CATEGORY_KEY } from "@app/Constants";
3-
import { useFetchAdvisories } from "@app/queries/advisories";
4-
import { useFetchPackages } from "@app/queries/packages";
5-
import { useFetchSBOMs } from "@app/queries/sboms";
6-
import { useFetchVulnerabilities } from "@app/queries/vulnerabilities";
1+
import React from "react";
2+
import { Link } from "react-router-dom";
3+
74
import {
85
Label,
96
Menu,
@@ -12,9 +9,18 @@ import {
129
MenuList,
1310
Popper,
1411
SearchInput,
12+
Spinner,
1513
} from "@patternfly/react-core";
16-
import React from "react";
17-
import { Link } from "react-router-dom";
14+
15+
import { useDebounceValue } from "usehooks-ts";
16+
17+
import { HubRequestParams } from "@app/api/models";
18+
import { FILTER_TEXT_CATEGORY_KEY } from "@app/Constants";
19+
import { SbomSearchContext } from "@app/pages/sbom-list/sbom-context";
20+
import { useFetchAdvisories } from "@app/queries/advisories";
21+
import { useFetchPackages } from "@app/queries/packages";
22+
import { useFetchSBOMs } from "@app/queries/sboms";
23+
import { useFetchVulnerabilities } from "@app/queries/vulnerabilities";
1824

1925
export interface IEntity {
2026
id: string;
@@ -48,66 +54,36 @@ const entityToMenu = (option: IEntity) => {
4854
);
4955
};
5056

51-
// Filter function
52-
export function filterEntityListByValue(list: IEntity[], searchString: string) {
53-
// When the value of the search input changes, build a list of no more than 10 autocomplete options.
54-
// Options which start with the search input value are listed first, followed by options which contain
55-
// the search input value.
56-
let options: React.JSX.Element[] = list
57-
.filter(
58-
(option) =>
59-
option.id.toLowerCase().startsWith(searchString.toLowerCase()) ||
60-
option.title?.toLowerCase().startsWith(searchString.toLowerCase()) ||
61-
option.description?.toLowerCase().startsWith(searchString.toLowerCase())
62-
)
63-
.map(entityToMenu);
64-
65-
if (options.length > 10) {
66-
options = options.slice(0, 10);
67-
} else {
68-
options = [
69-
...options,
70-
...list
71-
.filter(
72-
(option: IEntity) =>
73-
!option.id.startsWith(searchString.toLowerCase()) &&
74-
option.id.includes(searchString.toLowerCase())
75-
)
76-
.map(entityToMenu),
77-
].slice(0, 10);
78-
}
79-
80-
return options;
81-
}
82-
83-
function useAllEntities(filterText: string) {
57+
function useAllEntities(filterText: string, disableSearch: boolean) {
8458
const params: HubRequestParams = {
8559
filters: [
8660
{ field: FILTER_TEXT_CATEGORY_KEY, operator: "~", value: filterText },
8761
],
88-
page: { pageNumber: 1, itemsPerPage: 10 },
62+
page: { pageNumber: 1, itemsPerPage: 5 },
8963
};
9064

9165
const {
66+
isFetching: isFetchingAdvisories,
9267
result: { data: advisories },
93-
} = useFetchAdvisories({ ...params });
68+
} = useFetchAdvisories({ ...params }, true, disableSearch);
9469

9570
const {
71+
isFetching: isFetchingPackages,
9672
result: { data: packages },
97-
} = useFetchPackages({ ...params });
73+
} = useFetchPackages({ ...params }, true, disableSearch);
9874

9975
const {
76+
isFetching: isFetchingSBOMs,
10077
result: { data: sboms },
101-
} = useFetchSBOMs({ ...params });
78+
} = useFetchSBOMs({ ...params }, true, disableSearch);
10279

10380
const {
81+
isFetching: isFetchingVulnerabilities,
10482
result: { data: vulnerabilities },
105-
} = useFetchVulnerabilities({ ...params });
106-
107-
const tmpArray: IEntity[] = [];
83+
} = useFetchVulnerabilities({ ...params }, true, disableSearch);
10884

10985
const transformedAdvisories: IEntity[] = advisories.map((item) => ({
110-
id: item.document_id,
86+
id: `advisory-${item.uuid}`,
11187
title: item.document_id,
11288
description: item.title?.substring(0, 75),
11389
navLink: `/advisories/${item.uuid}`,
@@ -116,15 +92,15 @@ function useAllEntities(filterText: string) {
11692
}));
11793

11894
const transformedPackages: IEntity[] = packages.map((item) => ({
119-
id: item.uuid,
95+
id: `package-${item.uuid}`,
12096
title: item.purl,
12197
navLink: `/packages/${item.uuid}`,
12298
type: "Package",
12399
typeColor: "cyan",
124100
}));
125101

126102
const transformedSboms: IEntity[] = sboms.map((item) => ({
127-
id: item.id,
103+
id: `sbom-${item.id}`,
128104
title: item.name,
129105
description: item.authors.join(", "),
130106
navLink: `/sboms/${item.id}`,
@@ -133,24 +109,44 @@ function useAllEntities(filterText: string) {
133109
}));
134110

135111
const transformedVulnerabilities: IEntity[] = vulnerabilities.map((item) => ({
136-
id: item.identifier,
112+
id: `vulnerability-${item.identifier}`,
137113
title: item.identifier,
138114
description: item.description?.substring(0, 75),
139115
navLink: `/vulnerabilities/${item.identifier}`,
140116
type: "Vulnerability",
141117
typeColor: "orange",
142118
}));
143119

144-
tmpArray.push(
120+
const filterTextLowerCase = filterText.toLowerCase();
121+
122+
const list = [
123+
...transformedVulnerabilities,
124+
...transformedSboms,
145125
...transformedAdvisories,
146126
...transformedPackages,
147-
...transformedSboms,
148-
...transformedVulnerabilities
149-
);
127+
].sort((a, b) => {
128+
if (a.title?.includes(filterTextLowerCase)) {
129+
return -1;
130+
} else if (b.title?.includes(filterTextLowerCase)) {
131+
return 1;
132+
} else {
133+
const aIndex = (a.description || "")
134+
.toLowerCase()
135+
.indexOf(filterTextLowerCase);
136+
const bIndex = (b.description || "")
137+
.toLowerCase()
138+
.indexOf(filterTextLowerCase);
139+
return aIndex - bIndex;
140+
}
141+
});
150142

151143
return {
152-
list: tmpArray,
153-
defaultValue: "",
144+
isFetching:
145+
isFetchingAdvisories ||
146+
isFetchingPackages ||
147+
isFetchingSBOMs ||
148+
isFetchingVulnerabilities,
149+
list,
154150
};
155151
}
156152

@@ -162,18 +158,34 @@ export interface ISearchMenu {
162158
onChangeSearch: (searchValue: string | undefined) => void;
163159
}
164160

165-
export const SearchMenu: React.FC<ISearchMenu> = ({
166-
filterFunction = filterEntityListByValue,
167-
onChangeSearch,
168-
}) => {
169-
const { list: entityList, defaultValue } = useAllEntities("");
161+
export const SearchMenu: React.FC<ISearchMenu> = ({ onChangeSearch }) => {
162+
// Search value initial value
163+
const { tableControls: sbomTableControls } =
164+
React.useContext(SbomSearchContext);
165+
const initialSearchValue =
166+
sbomTableControls.filterState.filterValues[FILTER_TEXT_CATEGORY_KEY]?.[0] ||
167+
"";
168+
169+
// Search value
170+
const [searchValue, setSearchValue] = React.useState(initialSearchValue);
171+
const [isSearchValueDirty, setIsSearchValueDirty] = React.useState(false);
172+
173+
// Debounce Search value
174+
const [debouncedSearchValue, setDebouncedSearchValue] = useDebounceValue(
175+
searchValue,
176+
500
177+
);
170178

171-
const [searchValue, setSearchValue] = React.useState<string | undefined>(
172-
defaultValue
179+
React.useEffect(() => {
180+
setDebouncedSearchValue(searchValue);
181+
}, [setDebouncedSearchValue, searchValue]);
182+
183+
// Fetch all entities
184+
const { isFetching, list: entityList } = useAllEntities(
185+
debouncedSearchValue,
186+
!isSearchValueDirty
173187
);
174-
const [autocompleteOptions, setAutocompleteOptions] = React.useState<
175-
React.JSX.Element[]
176-
>([]);
188+
177189
const [isAutocompleteOpen, setIsAutocompleteOpen] =
178190
React.useState<boolean>(false);
179191

@@ -188,17 +200,12 @@ export const SearchMenu: React.FC<ISearchMenu> = ({
188200
searchInputRef.current.contains(document.activeElement)
189201
) {
190202
setIsAutocompleteOpen(true);
191-
192-
const options = filterFunction(entityList, newValue);
193-
194-
// The menu is hidden if there are no options
195-
setIsAutocompleteOpen(options.length > 0);
196-
setAutocompleteOptions(options);
197203
} else {
198204
setIsAutocompleteOpen(false);
199205
}
200206

201207
setSearchValue(newValue);
208+
setIsSearchValueDirty(true);
202209
};
203210

204211
const onClearSearchValue = () => {
@@ -273,9 +280,26 @@ export const SearchMenu: React.FC<ISearchMenu> = ({
273280
}, [isAutocompleteOpen]);
274281

275282
const autocomplete = (
276-
<Menu ref={autocompleteRef} style={{ maxWidth: "450px" }}>
283+
<Menu
284+
ref={autocompleteRef}
285+
style={{
286+
maxWidth: "450px",
287+
maxHeight: "450px",
288+
overflow: "scroll",
289+
overflowX: "hidden",
290+
overflowY: "auto",
291+
}}
292+
>
277293
<MenuContent>
278-
<MenuList>{autocompleteOptions}</MenuList>
294+
<MenuList>
295+
{isFetching ? (
296+
<MenuItem itemId="loading">
297+
<Spinner size="sm" />
298+
</MenuItem>
299+
) : (
300+
entityList.map(entityToMenu)
301+
)}
302+
</MenuList>
279303
</MenuContent>
280304
</Menu>
281305
);
@@ -301,7 +325,7 @@ export const SearchMenu: React.FC<ISearchMenu> = ({
301325
triggerRef={searchInputRef}
302326
popper={autocomplete}
303327
popperRef={autocompleteRef}
304-
isVisible={isAutocompleteOpen}
328+
isVisible={(isAutocompleteOpen && entityList.length > 0) || isFetching}
305329
enableFlip={false}
306330
// append the autocomplete menu to the search input in the DOM for the sake of the keyboard navigation experience
307331
appendTo={() =>

client/src/app/pages/search/components/SearchTabs.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export const SearchTabs: React.FC<SearchTabsProps> = ({
106106
</SplitItem>
107107
<SplitItem isFilled>
108108
<Tabs
109+
mountOnEnter
109110
isBox
110111
activeKey={activeTabKey}
111112
onSelect={handleTabClick}

0 commit comments

Comments
 (0)