diff --git a/ui/src/components/CommandPalette.tsx b/ui/src/components/CommandPalette.tsx new file mode 100644 index 00000000000..9750d73e2d9 --- /dev/null +++ b/ui/src/components/CommandPalette.tsx @@ -0,0 +1,288 @@ +import React, { useRef, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { + EuiFieldSearch, + EuiText, + EuiSpacer, + EuiHorizontalRule, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiTitle, +} from "@elastic/eui"; + +const commandPaletteStyles: Record = { + overlay: { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.7)", + zIndex: 9999, + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + modal: { + width: "600px", + maxWidth: "90vw", + maxHeight: "80vh", + backgroundColor: "white", + borderRadius: "8px", + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", + overflow: "hidden", + display: "flex", + flexDirection: "column", + }, + modalHeader: { + padding: "16px", + borderBottom: "1px solid #D3DAE6", + position: "sticky", + top: 0, + backgroundColor: "white", + zIndex: 1, + }, + modalBody: { + padding: "0 16px 16px", + maxHeight: "calc(80vh - 60px)", + overflowY: "auto", + }, + searchResults: { + marginTop: "8px", + }, + categoryGroup: { + marginBottom: "8px", + }, + searchResultItem: { + padding: "8px 0", + borderBottom: "1px solid #eee", + }, + searchResultItemLast: { + padding: "8px 0", + borderBottom: "none", + }, + itemDescription: { + fontSize: "0.85em", + color: "#666", + marginTop: "4px", + }, +}; + +interface CommandPaletteProps { + isOpen: boolean; + onClose: () => void; + categories: { + name: string; + data: any[]; + getLink: (item: any) => string; + }[]; +} + +const getItemType = (item: any, category: string): string | undefined => { + if (category === "Features" && "valueType" in item) { + return item.valueType; + } + if (category === "Feature Views" && "type" in item) { + return item.type; + } + return undefined; +}; + +const CommandPalette: React.FC = ({ + isOpen, + onClose, + categories, +}) => { + const [searchText, setSearchText] = React.useState(""); + const inputRef = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + if (isOpen && inputRef.current) { + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + } + }, [isOpen]); + + useEffect(() => { + if (!isOpen) { + setSearchText(""); + } + }, [isOpen]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + const searchResults = categories.map(({ name, data, getLink }) => { + const filteredItems = searchText + ? data.filter((item) => { + const itemName = + "name" in item + ? String(item.name) + : "spec" in item && item.spec && "name" in item.spec + ? String(item.spec.name ?? "Unknown") + : "Unknown"; + + return itemName.toLowerCase().includes(searchText.toLowerCase()); + }) + : []; + + const items = filteredItems.map((item) => { + const itemName = + "name" in item + ? String(item.name) + : "spec" in item && item.spec && "name" in item.spec + ? String(item.spec.name ?? "Unknown") + : "Unknown"; + + return { + name: itemName, + link: getLink(item), + description: + "spec" in item && item.spec && "description" in item.spec + ? String(item.spec.description || "") + : "", + type: getItemType(item, name), + }; + }); + + return { + title: name, + items, + }; + }); + + console.log( + "CommandPalette isOpen:", + isOpen, + "categories:", + categories.length, + ); // Debug log + + if (!isOpen) { + console.log("CommandPalette not rendering due to isOpen=false"); + return null; + } + + return ( +
+
e.stopPropagation()} + onKeyDown={handleKeyDown} + > +
+

Search Registry

+
+
+ setSearchText(e.target.value)} + isClearable + fullWidth + inputRef={(node) => { + inputRef.current = node; + }} + aria-label="Search registry" + autoFocus + /> + + {searchText ? ( +
+ {searchResults.filter((result) => result.items.length > 0) + .length > 0 ? ( + searchResults + .filter((result) => result.items.length > 0) + .map((result) => ( +
+ + +

+ {result.title} ({result.items.length}) +

+
+ + {result.items.map((item, idx) => ( + + ))} +
+ +
+ )) + ) : ( + + +

No matches found for "{searchText}"

+
+
+ )} +
+ ) : ( + +

Start typing to search...

+
+ )} +
+
+
+ ); +}; + +export default CommandPalette; diff --git a/ui/src/components/GlobalSearchShortcut.tsx b/ui/src/components/GlobalSearchShortcut.tsx index 2c116c6dd63..28e55454f30 100644 --- a/ui/src/components/GlobalSearchShortcut.tsx +++ b/ui/src/components/GlobalSearchShortcut.tsx @@ -9,15 +9,26 @@ const GlobalSearchShortcut: React.FC = ({ }) => { useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - if ((event.metaKey || event.ctrlKey) && event.key === "k") { + console.log( + "Key pressed:", + event.key, + "metaKey:", + event.metaKey, + "ctrlKey:", + event.ctrlKey, + ); + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { + console.log("Cmd+K detected, preventing default and calling onOpen"); event.preventDefault(); + event.stopPropagation(); onOpen(); } }; - document.addEventListener("keydown", handleKeyDown); + console.log("Adding keydown event listener to window"); + window.addEventListener("keydown", handleKeyDown, true); return () => { - document.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keydown", handleKeyDown, true); }; }, [onOpen]); diff --git a/ui/src/components/RegistrySearch.tsx b/ui/src/components/RegistrySearch.tsx index 24d6886d4d5..d9d72a20b1a 100644 --- a/ui/src/components/RegistrySearch.tsx +++ b/ui/src/components/RegistrySearch.tsx @@ -9,9 +9,38 @@ import { EuiFieldSearch, EuiSpacer, EuiHorizontalRule, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiTitle, } from "@elastic/eui"; import EuiCustomLink from "./EuiCustomLink"; +import { css } from "@emotion/react"; + +const searchResultsStyles = { + searchResults: { + marginTop: "8px", + }, + categoryGroup: { + marginBottom: "8px", + }, + searchResultItem: { + padding: "8px 0", + borderBottom: "1px solid #eee", + }, + searchResultItemLast: { + padding: "8px 0", + borderBottom: "none", + }, + itemDescription: { + fontSize: "0.85em", + color: "#666", + marginTop: "4px", + }, +}; + interface RegistrySearchProps { categories: { name: string; @@ -24,6 +53,16 @@ export interface RegistrySearchRef { focusSearchInput: () => void; } +const getItemType = (item: any, category: string): string | undefined => { + if (category === "Features" && "valueType" in item) { + return item.valueType; + } + if (category === "Feature Views" && "type" in item) { + return item.type; + } + return undefined; +}; + const RegistrySearch = forwardRef( ({ categories }, ref) => { const [searchText, setSearchText] = useState(""); @@ -57,7 +96,29 @@ const RegistrySearch = forwardRef( }) : []; - return { name, items: filteredItems, getLink }; + const items = filteredItems.map((item) => { + const itemName = + "name" in item + ? String(item.name) + : "spec" in item && item.spec && "name" in item.spec + ? String(item.spec.name ?? "Unknown") + : "Unknown"; + + return { + name: itemName, + link: getLink(item), + description: + "spec" in item && item.spec && "description" in item.spec + ? String(item.spec.description || "") + : "", + type: getItemType(item, name), + }; + }); + + return { + title: name, + items, + }; }); return ( @@ -81,59 +142,72 @@ const RegistrySearch = forwardRef( /> {searchText && ( - <> +

Search Results

- {searchResults.some(({ items }) => items.length > 0) ? ( -
- {searchResults.map(({ name, items, getLink }, index) => - items.length > 0 ? ( -
- -
{name}
-
- -
    - {items.map((item, idx) => { - const itemName = - "name" in item - ? item.name - : "spec" in item - ? item.spec?.name - : "Unknown"; - - const itemLink = getLink(item); - - return ( -
  • - - {itemName} + {searchResults.filter((result) => result.items.length > 0).length > + 0 ? ( + searchResults + .filter((result) => result.items.length > 0) + .map((result) => ( +
    + + +

    + {result.title} ({result.items.length}) +

    +
    + + {result.items.map((item, idx) => ( +
    + + + setSearchText("")} + > + {item.name} -
  • - ); - })} -
- {index < - searchResults.filter( - (result) => result.items.length > 0, - ).length - - 1 && } -
- ) : null, - )} -
+ {item.description && ( +
+ {item.description} +
+ )} + + {item.type && ( + + {item.type} + + )} + +
+ ))} + + + + )) ) : ( -
-
-

No matches found.

-
-
+ + +

No matches found for "{searchText}"

+
+
)} - + )} ); diff --git a/ui/src/pages/Layout.tsx b/ui/src/pages/Layout.tsx index 634c02377fa..5af751e0acf 100644 --- a/ui/src/pages/Layout.tsx +++ b/ui/src/pages/Layout.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { EuiPage, @@ -26,13 +26,14 @@ import RegistrySearch, { RegistrySearchRef, } from "../components/RegistrySearch"; import GlobalSearchShortcut from "../components/GlobalSearchShortcut"; +import CommandPalette from "../components/CommandPalette"; const Layout = () => { // Registry Path Context has to be inside Layout // because it has to be under routes // in order to use useParams let { projectName } = useParams(); - const setIsSearchOpen = useState(false); + const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); const searchRef = useRef(null); const { data: projectsData } = useLoadProjectsList(); @@ -85,16 +86,44 @@ const Layout = () => { : []; const handleSearchOpen = () => { - setTimeout(() => { - if (searchRef.current) { - searchRef.current.focusSearchInput(); - } - }, 100); + console.log("Opening command palette - before state update"); // Debug log + setIsCommandPaletteOpen(true); + console.log("Command palette state should be updated to true"); }; + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + console.log( + "Layout key pressed:", + event.key, + "metaKey:", + event.metaKey, + "ctrlKey:", + event.ctrlKey, + ); + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { + console.log("Layout detected Cmd+K, preventing default"); + event.preventDefault(); + event.stopPropagation(); + handleSearchOpen(); + } + }; + + console.log("Layout adding keydown event listener"); + window.addEventListener("keydown", handleKeyDown, true); + return () => { + window.removeEventListener("keydown", handleKeyDown, true); + }; + }, []); + return ( + setIsCommandPaletteOpen(false)} + categories={categories} + /> {