Skip to content

Commit 18cbd7f

Browse files
feat: Improve search results formatting (#5326)
* Improve search results formatting for better readability and user experience Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Fix TypeScript errors with CSS implementation in search results Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Add feature to clear search bar after clicking a result Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Fix linting issues in RegistrySearch component Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Implement command palette/spotlight search triggered by Cmd+K Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Fix TypeScript errors in CommandPalette component Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Fix UI issues in command palette: prevent double scrollbars and improve modal positioning Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Fix command palette overlay implementation and UI issues Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Remove unused EuiOverlayMask import Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Fix command palette UI issues and improve user experience Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Fix command palette modal closing when clicking on search results Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Apply formatting to command palette components Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Use React Router navigation instead of window.location.href to prevent full page refreshes Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Format CommandPalette.tsx and clean up code Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> * Remove test button from Layout component Co-Authored-By: Francisco Javier Arceo <arceofrancisco@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 33ea0f5 commit 18cbd7f

File tree

4 files changed

+460
-58
lines changed

4 files changed

+460
-58
lines changed

ui/src/components/CommandPalette.tsx

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import React, { useRef, useEffect } from "react";
2+
import { useNavigate } from "react-router-dom";
3+
import {
4+
EuiFieldSearch,
5+
EuiText,
6+
EuiSpacer,
7+
EuiHorizontalRule,
8+
EuiPanel,
9+
EuiFlexGroup,
10+
EuiFlexItem,
11+
EuiBadge,
12+
EuiTitle,
13+
} from "@elastic/eui";
14+
15+
const commandPaletteStyles: Record<string, React.CSSProperties> = {
16+
overlay: {
17+
position: "fixed",
18+
top: 0,
19+
left: 0,
20+
right: 0,
21+
bottom: 0,
22+
backgroundColor: "rgba(0, 0, 0, 0.7)",
23+
zIndex: 9999,
24+
display: "flex",
25+
alignItems: "center",
26+
justifyContent: "center",
27+
},
28+
modal: {
29+
width: "600px",
30+
maxWidth: "90vw",
31+
maxHeight: "80vh",
32+
backgroundColor: "white",
33+
borderRadius: "8px",
34+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
35+
overflow: "hidden",
36+
display: "flex",
37+
flexDirection: "column",
38+
},
39+
modalHeader: {
40+
padding: "16px",
41+
borderBottom: "1px solid #D3DAE6",
42+
position: "sticky",
43+
top: 0,
44+
backgroundColor: "white",
45+
zIndex: 1,
46+
},
47+
modalBody: {
48+
padding: "0 16px 16px",
49+
maxHeight: "calc(80vh - 60px)",
50+
overflowY: "auto",
51+
},
52+
searchResults: {
53+
marginTop: "8px",
54+
},
55+
categoryGroup: {
56+
marginBottom: "8px",
57+
},
58+
searchResultItem: {
59+
padding: "8px 0",
60+
borderBottom: "1px solid #eee",
61+
},
62+
searchResultItemLast: {
63+
padding: "8px 0",
64+
borderBottom: "none",
65+
},
66+
itemDescription: {
67+
fontSize: "0.85em",
68+
color: "#666",
69+
marginTop: "4px",
70+
},
71+
};
72+
73+
interface CommandPaletteProps {
74+
isOpen: boolean;
75+
onClose: () => void;
76+
categories: {
77+
name: string;
78+
data: any[];
79+
getLink: (item: any) => string;
80+
}[];
81+
}
82+
83+
const getItemType = (item: any, category: string): string | undefined => {
84+
if (category === "Features" && "valueType" in item) {
85+
return item.valueType;
86+
}
87+
if (category === "Feature Views" && "type" in item) {
88+
return item.type;
89+
}
90+
return undefined;
91+
};
92+
93+
const CommandPalette: React.FC<CommandPaletteProps> = ({
94+
isOpen,
95+
onClose,
96+
categories,
97+
}) => {
98+
const [searchText, setSearchText] = React.useState("");
99+
const inputRef = useRef<HTMLInputElement | null>(null);
100+
const navigate = useNavigate();
101+
102+
useEffect(() => {
103+
if (isOpen && inputRef.current) {
104+
setTimeout(() => {
105+
inputRef.current?.focus();
106+
}, 100);
107+
}
108+
}, [isOpen]);
109+
110+
useEffect(() => {
111+
if (!isOpen) {
112+
setSearchText("");
113+
}
114+
}, [isOpen]);
115+
116+
const handleKeyDown = (e: React.KeyboardEvent) => {
117+
if (e.key === "Escape") {
118+
onClose();
119+
}
120+
};
121+
122+
const searchResults = categories.map(({ name, data, getLink }) => {
123+
const filteredItems = searchText
124+
? data.filter((item) => {
125+
const itemName =
126+
"name" in item
127+
? String(item.name)
128+
: "spec" in item && item.spec && "name" in item.spec
129+
? String(item.spec.name ?? "Unknown")
130+
: "Unknown";
131+
132+
return itemName.toLowerCase().includes(searchText.toLowerCase());
133+
})
134+
: [];
135+
136+
const items = filteredItems.map((item) => {
137+
const itemName =
138+
"name" in item
139+
? String(item.name)
140+
: "spec" in item && item.spec && "name" in item.spec
141+
? String(item.spec.name ?? "Unknown")
142+
: "Unknown";
143+
144+
return {
145+
name: itemName,
146+
link: getLink(item),
147+
description:
148+
"spec" in item && item.spec && "description" in item.spec
149+
? String(item.spec.description || "")
150+
: "",
151+
type: getItemType(item, name),
152+
};
153+
});
154+
155+
return {
156+
title: name,
157+
items,
158+
};
159+
});
160+
161+
console.log(
162+
"CommandPalette isOpen:",
163+
isOpen,
164+
"categories:",
165+
categories.length,
166+
); // Debug log
167+
168+
if (!isOpen) {
169+
console.log("CommandPalette not rendering due to isOpen=false");
170+
return null;
171+
}
172+
173+
return (
174+
<div style={commandPaletteStyles.overlay} onClick={onClose}>
175+
<div
176+
style={commandPaletteStyles.modal}
177+
onClick={(e) => e.stopPropagation()}
178+
onKeyDown={handleKeyDown}
179+
>
180+
<div style={commandPaletteStyles.modalHeader}>
181+
<h2 style={{ margin: 0 }}>Search Registry</h2>
182+
</div>
183+
<div style={commandPaletteStyles.modalBody}>
184+
<EuiFieldSearch
185+
placeholder="Search across Feature Views, Features, Entities, etc."
186+
value={searchText}
187+
onChange={(e) => setSearchText(e.target.value)}
188+
isClearable
189+
fullWidth
190+
inputRef={(node) => {
191+
inputRef.current = node;
192+
}}
193+
aria-label="Search registry"
194+
autoFocus
195+
/>
196+
<EuiSpacer size="s" />
197+
{searchText ? (
198+
<div style={commandPaletteStyles.searchResults}>
199+
{searchResults.filter((result) => result.items.length > 0)
200+
.length > 0 ? (
201+
searchResults
202+
.filter((result) => result.items.length > 0)
203+
.map((result) => (
204+
<div
205+
key={result.title}
206+
style={commandPaletteStyles.categoryGroup}
207+
>
208+
<EuiPanel hasBorder={true} paddingSize="m">
209+
<EuiTitle size="xs">
210+
<h3>
211+
{result.title} ({result.items.length})
212+
</h3>
213+
</EuiTitle>
214+
<EuiHorizontalRule margin="xs" />
215+
{result.items.map((item, idx) => (
216+
<div
217+
key={item.name}
218+
style={
219+
idx === result.items.length - 1
220+
? commandPaletteStyles.searchResultItemLast
221+
: commandPaletteStyles.searchResultItem
222+
}
223+
>
224+
<EuiFlexGroup>
225+
<EuiFlexItem>
226+
<a
227+
href={item.link}
228+
onClick={(e) => {
229+
e.preventDefault();
230+
console.log(
231+
"Search result clicked:",
232+
item.name,
233+
);
234+
235+
onClose();
236+
237+
setSearchText("");
238+
239+
console.log("Navigating to:", item.link);
240+
navigate(item.link);
241+
}}
242+
style={{
243+
color: "#0077cc",
244+
textDecoration: "none",
245+
}}
246+
>
247+
<strong>{item.name}</strong>
248+
</a>
249+
{item.description && (
250+
<div
251+
style={commandPaletteStyles.itemDescription}
252+
>
253+
{item.description}
254+
</div>
255+
)}
256+
</EuiFlexItem>
257+
{item.type && (
258+
<EuiFlexItem grow={false}>
259+
<EuiBadge>{item.type}</EuiBadge>
260+
</EuiFlexItem>
261+
)}
262+
</EuiFlexGroup>
263+
</div>
264+
))}
265+
</EuiPanel>
266+
<EuiSpacer size="m" />
267+
</div>
268+
))
269+
) : (
270+
<EuiPanel hasBorder={true} paddingSize="m" color="subdued">
271+
<EuiText textAlign="center">
272+
<p>No matches found for "{searchText}"</p>
273+
</EuiText>
274+
</EuiPanel>
275+
)}
276+
</div>
277+
) : (
278+
<EuiText color="subdued" textAlign="center">
279+
<p>Start typing to search...</p>
280+
</EuiText>
281+
)}
282+
</div>
283+
</div>
284+
</div>
285+
);
286+
};
287+
288+
export default CommandPalette;

ui/src/components/GlobalSearchShortcut.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,26 @@ const GlobalSearchShortcut: React.FC<GlobalSearchShortcutProps> = ({
99
}) => {
1010
useEffect(() => {
1111
const handleKeyDown = (event: KeyboardEvent) => {
12-
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
12+
console.log(
13+
"Key pressed:",
14+
event.key,
15+
"metaKey:",
16+
event.metaKey,
17+
"ctrlKey:",
18+
event.ctrlKey,
19+
);
20+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
21+
console.log("Cmd+K detected, preventing default and calling onOpen");
1322
event.preventDefault();
23+
event.stopPropagation();
1424
onOpen();
1525
}
1626
};
1727

18-
document.addEventListener("keydown", handleKeyDown);
28+
console.log("Adding keydown event listener to window");
29+
window.addEventListener("keydown", handleKeyDown, true);
1930
return () => {
20-
document.removeEventListener("keydown", handleKeyDown);
31+
window.removeEventListener("keydown", handleKeyDown, true);
2132
};
2233
}, [onOpen]);
2334

0 commit comments

Comments
 (0)