Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions ui/src/components/GlobalSearchShortcut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useEffect } from "react";

interface GlobalSearchShortcutProps {
onOpen: () => void;
}

const GlobalSearchShortcut: React.FC<GlobalSearchShortcutProps> = ({
onOpen,
}) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
event.preventDefault();
onOpen();
}
};

document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [onOpen]);

return null; // This component doesn't render anything
};

export default GlobalSearchShortcut;
193 changes: 123 additions & 70 deletions ui/src/components/RegistrySearch.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import React, { useState } from "react";
import { EuiText, EuiFieldSearch, EuiSpacer } from "@elastic/eui";
import React, {
useState,
useRef,
forwardRef,
useImperativeHandle,
} from "react";
import {
EuiText,
EuiFieldSearch,
EuiSpacer,
EuiHorizontalRule,
} from "@elastic/eui";
import EuiCustomLink from "./EuiCustomLink";

interface RegistrySearchProps {
Expand All @@ -10,81 +20,124 @@ interface RegistrySearchProps {
}[];
}

const RegistrySearch: React.FC<RegistrySearchProps> = ({ categories }) => {
const [searchText, setSearchText] = useState("");
export interface RegistrySearchRef {
focusSearchInput: () => void;
}

const RegistrySearch = forwardRef<RegistrySearchRef, RegistrySearchProps>(
({ categories }, ref) => {
const [searchText, setSearchText] = useState("");
const inputRef = useRef<HTMLInputElement | null>(null);

const focusSearchInput = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};

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";
useImperativeHandle(
ref,
() => ({
focusSearchInput,
}),
[focusSearchInput],
);

return itemName.toLowerCase().includes(searchText.toLowerCase());
})
: [];
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 { name, items: filteredItems, getLink };
});
return itemName.toLowerCase().includes(searchText.toLowerCase());
})
: [];

return (
<>
<EuiSpacer size="l" />
<EuiText>
<h3>Search in registry</h3>
</EuiText>
<EuiSpacer size="s" />
<EuiFieldSearch
placeholder="Search across Feature Views, Features, Entities, etc."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
isClearable
fullWidth
/>
<EuiSpacer size="m" />
return { name, items: filteredItems, getLink };
});

{searchText && (
<EuiText>
<h3>Search Results</h3>
{searchResults.some(({ items }) => items.length > 0) ? (
searchResults.map(({ name, items, getLink }, index) =>
items.length > 0 ? (
<div key={index}>
<h4>{name}</h4>
<ul>
{items.map((item, idx) => {
const itemName =
"name" in item
? item.name
: "spec" in item
? item.spec?.name
: "Unknown";
return (
<>
<EuiFieldSearch
placeholder="Search across Feature Views, Features, Entities, etc."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
isClearable
fullWidth
inputRef={(node) => {
inputRef.current = node;
}}
aria-label="Search registry"
compressed
append={
<EuiText size="xs" color="subdued">
<span style={{ whiteSpace: "nowrap" }}>⌘K</span>
</EuiText>
}
/>
<EuiSpacer size="s" />
{searchText && (
<>
<EuiText>
<h4>Search Results</h4>
</EuiText>
<EuiSpacer size="xs" />
{searchResults.some(({ items }) => items.length > 0) ? (
<div className="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow">
{searchResults.map(({ name, items, getLink }, index) =>
items.length > 0 ? (
<div key={index} className="euiPanel__body">
<EuiText>
<h5>{name}</h5>
</EuiText>
<EuiSpacer size="xs" />
<ul
style={{ listStyleType: "none", padding: 0, margin: 0 }}
>
{items.map((item, idx) => {
const itemName =
"name" in item
? item.name
: "spec" in item
? item.spec?.name
: "Unknown";

const itemLink = getLink(item);
const itemLink = getLink(item);

return (
<li key={idx}>
<EuiCustomLink to={itemLink}>
{itemName}
</EuiCustomLink>
</li>
);
})}
</ul>
<EuiSpacer size="m" />
return (
<li key={idx} style={{ margin: "8px 0" }}>
<EuiCustomLink to={itemLink}>
{itemName}
</EuiCustomLink>
</li>
);
})}
</ul>
{index <
searchResults.filter(
(result) => result.items.length > 0,
).length -
1 && <EuiHorizontalRule margin="m" />}
</div>
) : null,
)}
</div>
) : (
<div className="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow">
<div className="euiPanel__body">
<p>No matches found.</p>
</div>
) : null,
)
) : (
<p>No matches found.</p>
)}
</EuiText>
)}
</>
);
};
</div>
)}
</>
)}
</>
);
},
);

export default RegistrySearch;
97 changes: 93 additions & 4 deletions ui/src/pages/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState, useRef } from "react";

import {
EuiPage,
Expand All @@ -7,34 +7,95 @@ import {
EuiErrorBoundary,
EuiHorizontalRule,
EuiSpacer,
EuiPageHeader,
EuiFlexGroup,
EuiFlexItem,
} from "@elastic/eui";
import { Outlet } from "react-router-dom";

import RegistryPathContext from "../contexts/RegistryPathContext";
import { useParams } from "react-router-dom";
import { useLoadProjectsList } from "../contexts/ProjectListContext";
import useLoadRegistry from "../queries/useLoadRegistry";

import ProjectSelector from "../components/ProjectSelector";
import Sidebar from "./Sidebar";
import FeastWordMark from "../graphics/FeastWordMark";
import ThemeToggle from "../components/ThemeToggle";
import RegistrySearch, {
RegistrySearchRef,
} from "../components/RegistrySearch";
import GlobalSearchShortcut from "../components/GlobalSearchShortcut";

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 [isSearchOpen, setIsSearchOpen] = useState(false);
const searchRef = useRef<RegistrySearchRef>(null);

const { data } = useLoadProjectsList();
const { data: projectsData } = useLoadProjectsList();

const currentProject = data?.projects.find((project) => {
const currentProject = projectsData?.projects.find((project) => {
return project.id === projectName;
});

const registryPath = currentProject?.registryPath || "";
const { data } = useLoadRegistry(registryPath);

const categories = data
? [
{
name: "Data Sources",
data: data.objects.dataSources || [],
getLink: (item: any) => `/p/${projectName}/data-source/${item.name}`,
},
{
name: "Entities",
data: data.objects.entities || [],
getLink: (item: any) => `/p/${projectName}/entity/${item.name}`,
},
{
name: "Features",
data: data.allFeatures || [],
getLink: (item: any) => {
const featureView = item?.featureView;
return featureView
? `/p/${projectName}/feature-view/${featureView}/feature/${item.name}`
: "#";
},
},
{
name: "Feature Views",
data: data.mergedFVList || [],
getLink: (item: any) => `/p/${projectName}/feature-view/${item.name}`,
},
{
name: "Feature Services",
data: data.objects.featureServices || [],
getLink: (item: any) => {
const serviceName = item?.name || item?.spec?.name;
return serviceName
? `/p/${projectName}/feature-service/${serviceName}`
: "#";
},
},
]
: [];

const handleSearchOpen = () => {
setIsSearchOpen(true);
setTimeout(() => {
if (searchRef.current) {
searchRef.current.focusSearchInput();
}
}, 100);
};

return (
<RegistryPathContext.Provider value={registryPath}>
<GlobalSearchShortcut onOpen={handleSearchOpen} />
<EuiPage paddingSize="none" style={{ background: "transparent" }}>
<EuiPageSidebar
paddingSize="l"
Expand All @@ -51,13 +112,41 @@ const Layout = () => {
<Sidebar />
<EuiSpacer size="l" />
<EuiHorizontalRule margin="s" />
<ThemeToggle />
<div
style={{
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
}}
>
<ThemeToggle />
</div>
</React.Fragment>
)}
</EuiPageSidebar>

<EuiPageBody>
<EuiErrorBoundary>
{isSearchOpen && data && (
<EuiPageHeader
paddingSize="l"
style={{
position: "sticky",
top: 0,
zIndex: 100,
borderBottom: "1px solid #D3DAE6",
}}
>
<EuiFlexGroup justifyContent="center">
<EuiFlexItem
grow={false}
style={{ width: "600px", maxWidth: "90%" }}
>
<RegistrySearch ref={searchRef} categories={categories} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageHeader>
)}
<Outlet />
</EuiErrorBoundary>
</EuiPageBody>
Expand Down
3 changes: 0 additions & 3 deletions ui/src/pages/ProjectOverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,6 @@ const ProjectOverviewPage = () => {

{selectedTabId === "visualization" && <RegistryVisualizationTab />}
</EuiPageTemplate.Section>
<EuiPageTemplate.Section>
{isSuccess && <RegistrySearch categories={categories} />}
</EuiPageTemplate.Section>
</EuiPageTemplate>
);
};
Expand Down