Skip to content

Commit f09ea49

Browse files
authored
feat: Added global registry search support in Feast UI (#5195)
* feat: Added global search feature at UI homepage Signed-off-by: ntkathole <[email protected]> * feat: Made searched items linkable Signed-off-by: ntkathole <[email protected]> * feat: RegistrySearch component Signed-off-by: ntkathole <[email protected]> * fix: fix load elements only after registry load Signed-off-by: ntkathole <[email protected]> --------- Signed-off-by: ntkathole <[email protected]>
1 parent 66ddd3e commit f09ea49

File tree

3 files changed

+171
-24
lines changed

3 files changed

+171
-24
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { useState } from "react";
2+
import { EuiText, EuiFieldSearch, EuiSpacer } from "@elastic/eui";
3+
import EuiCustomLink from "./EuiCustomLink";
4+
5+
interface RegistrySearchProps {
6+
categories: {
7+
name: string;
8+
data: any[];
9+
getLink: (item: any) => string;
10+
}[];
11+
}
12+
13+
const RegistrySearch: React.FC<RegistrySearchProps> = ({ categories }) => {
14+
const [searchText, setSearchText] = useState("");
15+
16+
const searchResults = categories.map(({ name, data, getLink }) => {
17+
const filteredItems = searchText
18+
? data.filter((item) => {
19+
const itemName =
20+
"name" in item
21+
? String(item.name)
22+
: "spec" in item && item.spec && "name" in item.spec
23+
? String(item.spec.name ?? "Unknown")
24+
: "Unknown";
25+
26+
return itemName.toLowerCase().includes(searchText.toLowerCase());
27+
})
28+
: [];
29+
30+
return { name, items: filteredItems, getLink };
31+
});
32+
33+
return (
34+
<>
35+
<EuiSpacer size="l" />
36+
<EuiText>
37+
<h3>Search in registry</h3>
38+
</EuiText>
39+
<EuiSpacer size="s" />
40+
<EuiFieldSearch
41+
placeholder="Search across Feature Views, Features, Entities, etc."
42+
value={searchText}
43+
onChange={(e) => setSearchText(e.target.value)}
44+
isClearable
45+
fullWidth
46+
/>
47+
<EuiSpacer size="m" />
48+
49+
{searchText && (
50+
<EuiText>
51+
<h3>Search Results</h3>
52+
{searchResults.some(({ items }) => items.length > 0) ? (
53+
searchResults.map(({ name, items, getLink }, index) =>
54+
items.length > 0 ? (
55+
<div key={index}>
56+
<h4>{name}</h4>
57+
<ul>
58+
{items.map((item, idx) => {
59+
const itemName =
60+
"name" in item
61+
? item.name
62+
: "spec" in item
63+
? item.spec?.name
64+
: "Unknown";
65+
66+
const itemLink = getLink(item);
67+
68+
return (
69+
<li key={idx}>
70+
<EuiCustomLink to={itemLink}>
71+
{itemName}
72+
</EuiCustomLink>
73+
</li>
74+
);
75+
})}
76+
</ul>
77+
<EuiSpacer size="m" />
78+
</div>
79+
) : null,
80+
)
81+
) : (
82+
<p>No matches found.</p>
83+
)}
84+
</EuiText>
85+
)}
86+
</>
87+
);
88+
};
89+
90+
export default RegistrySearch;

ui/src/pages/ProjectOverviewPage.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import React, { useContext } from "react";
2-
1+
import React, { useContext, useState } from "react";
32
import {
43
EuiPageTemplate,
54
EuiText,
@@ -9,19 +8,64 @@ import {
98
EuiSpacer,
109
EuiSkeletonText,
1110
EuiEmptyPrompt,
11+
EuiFieldSearch,
1212
} from "@elastic/eui";
1313

1414
import { useDocumentTitle } from "../hooks/useDocumentTitle";
1515
import ObjectsCountStats from "../components/ObjectsCountStats";
1616
import ExplorePanel from "../components/ExplorePanel";
1717
import useLoadRegistry from "../queries/useLoadRegistry";
1818
import RegistryPathContext from "../contexts/RegistryPathContext";
19+
import RegistrySearch from "../components/RegistrySearch";
20+
import { useParams } from "react-router-dom";
1921

2022
const ProjectOverviewPage = () => {
2123
useDocumentTitle("Feast Home");
2224
const registryUrl = useContext(RegistryPathContext);
2325
const { isLoading, isSuccess, isError, data } = useLoadRegistry(registryUrl);
2426

27+
const [searchText, setSearchText] = useState("");
28+
29+
const { projectName } = useParams<{ projectName: string }>();
30+
31+
const categories = [
32+
{
33+
name: "Data Sources",
34+
data: data?.objects.dataSources || [],
35+
getLink: (item: any) => `/p/${projectName}/data-source/${item.name}`,
36+
},
37+
{
38+
name: "Entities",
39+
data: data?.objects.entities || [],
40+
getLink: (item: any) => `/p/${projectName}/entity/${item.name}`,
41+
},
42+
{
43+
name: "Features",
44+
data: data?.allFeatures || [],
45+
getLink: (item: any) => {
46+
const featureView = item?.featureView;
47+
return featureView
48+
? `/p/${projectName}/feature-view/${featureView}/feature/${item.name}`
49+
: "#";
50+
},
51+
},
52+
{
53+
name: "Feature Views",
54+
data: data?.mergedFVList || [],
55+
getLink: (item: any) => `/p/${projectName}/feature-view/${item.name}`,
56+
},
57+
{
58+
name: "Feature Services",
59+
data: data?.objects.featureServices || [],
60+
getLink: (item: any) => {
61+
const serviceName = item?.name || item?.spec?.name;
62+
return serviceName
63+
? `/p/${projectName}/feature-service/${serviceName}`
64+
: "#";
65+
},
66+
},
67+
];
68+
2569
return (
2670
<EuiPageTemplate panelled>
2771
<EuiPageTemplate.Section>
@@ -59,8 +103,8 @@ const ProjectOverviewPage = () => {
59103
<EuiText>
60104
<p>
61105
Welcome to your new Feast project. In this UI, you can see
62-
Data Sources, Entities, Feature Views and Feature Services
63-
registered in Feast.
106+
Data Sources, Entities, Features, Feature Views, and Feature
107+
Services registered in Feast.
64108
</p>
65109
<p>
66110
It looks like this project already has some objects
@@ -85,6 +129,9 @@ const ProjectOverviewPage = () => {
85129
</EuiFlexItem>
86130
</EuiFlexGroup>
87131
</EuiPageTemplate.Section>
132+
<EuiPageTemplate.Section>
133+
{isSuccess && <RegistrySearch categories={categories} />}
134+
</EuiPageTemplate.Section>
88135
</EuiPageTemplate>
89136
);
90137
};

ui/src/pages/features/FeatureListPage.tsx

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import EuiCustomLink from "../../components/EuiCustomLink";
1212
import { useParams } from "react-router-dom";
1313
import useLoadRegistry from "../../queries/useLoadRegistry";
1414
import RegistryPathContext from "../../contexts/RegistryPathContext";
15+
import { FeatureIcon } from "../../graphics/FeatureIcon";
1516

1617
interface Feature {
1718
name: string;
@@ -35,9 +36,6 @@ const FeatureListPage = () => {
3536
const [pageIndex, setPageIndex] = useState(0);
3637
const [pageSize, setPageSize] = useState(100);
3738

38-
if (isLoading) return <p>Loading...</p>;
39-
if (isError) return <p>Error loading features.</p>;
40-
4139
const features: Feature[] = data?.allFeatures || [];
4240

4341
const filteredFeatures = features.filter((feature) =>
@@ -107,24 +105,36 @@ const FeatureListPage = () => {
107105

108106
return (
109107
<EuiPageTemplate panelled>
110-
<EuiPageTemplate.Header pageTitle="Feature List" />
108+
<EuiPageTemplate.Header
109+
restrictWidth
110+
iconType={FeatureIcon}
111+
pageTitle="Feature List"
112+
/>
111113
<EuiPageTemplate.Section>
112-
<EuiFieldSearch
113-
placeholder="Search features"
114-
value={searchText}
115-
onChange={(e) => setSearchText(e.target.value)}
116-
fullWidth
117-
/>
118-
<EuiBasicTable
119-
columns={columns}
120-
items={paginatedFeatures}
121-
rowProps={getRowProps}
122-
sorting={{
123-
sort: { field: sortField, direction: sortDirection },
124-
}}
125-
onChange={onTableChange}
126-
pagination={pagination}
127-
/>
114+
{isLoading ? (
115+
<p>Loading...</p>
116+
) : isError ? (
117+
<p>We encountered an error while loading.</p>
118+
) : (
119+
<>
120+
<EuiFieldSearch
121+
placeholder="Search features"
122+
value={searchText}
123+
onChange={(e) => setSearchText(e.target.value)}
124+
fullWidth
125+
/>
126+
<EuiBasicTable
127+
columns={columns}
128+
items={paginatedFeatures}
129+
rowProps={getRowProps}
130+
sorting={{
131+
sort: { field: sortField, direction: sortDirection },
132+
}}
133+
onChange={onTableChange}
134+
pagination={pagination}
135+
/>
136+
</>
137+
)}
128138
</EuiPageTemplate.Section>
129139
</EuiPageTemplate>
130140
);

0 commit comments

Comments
 (0)