Skip to content

Commit eb0a86e

Browse files
authored
feat: Added support for filtering multi-projects (#5688)
Signed-off-by: ntkathole <[email protected]>
1 parent 949ba3d commit eb0a86e

30 files changed

+631
-109
lines changed

sdk/python/feast/ui_server.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,45 @@ def shutdown_event():
6161
with importlib_resources.as_file(ui_dir_ref) as ui_dir:
6262
# Initialize with the projects-list.json file
6363
with ui_dir.joinpath("projects-list.json").open(mode="w") as f:
64-
projects_dict = {
65-
"projects": [
64+
# Get all projects from the registry
65+
discovered_projects = []
66+
registry = store.registry.proto()
67+
68+
# Use the projects list from the registry
69+
if registry and registry.projects and len(registry.projects) > 0:
70+
for proj in registry.projects:
71+
if proj.spec and proj.spec.name:
72+
discovered_projects.append(
73+
{
74+
"name": proj.spec.name.replace("_", " ").title(),
75+
"description": proj.spec.description
76+
or f"Project: {proj.spec.name}",
77+
"id": proj.spec.name,
78+
"registryPath": f"{root_path}/registry",
79+
}
80+
)
81+
else:
82+
# If no projects in registry, use the current project from feature_store.yaml
83+
discovered_projects.append(
6684
{
6785
"name": "Project",
6886
"description": "Test project",
6987
"id": project_id,
7088
"registryPath": f"{root_path}/registry",
7189
}
72-
]
73-
}
90+
)
91+
92+
# Add "All Projects" option at the beginning if there are multiple projects
93+
if len(discovered_projects) > 1:
94+
all_projects_entry = {
95+
"name": "All Projects",
96+
"description": "View data across all projects",
97+
"id": "all",
98+
"registryPath": f"{root_path}/registry",
99+
}
100+
discovered_projects.insert(0, all_projects_entry)
101+
102+
projects_dict = {"projects": discovered_projects}
74103
f.write(json.dumps(projects_dict))
75104

76105
@app.get("/registry")

ui/src/components/CommandPalette.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ const CommandPalette: React.FC<CommandPaletteProps> = ({
149149
? String(item.spec.description || "")
150150
: "",
151151
type: getItemType(item, name),
152+
projectId: "projectId" in item ? String(item.projectId) : undefined,
152153
};
153154
});
154155

@@ -158,15 +159,7 @@ const CommandPalette: React.FC<CommandPaletteProps> = ({
158159
};
159160
});
160161

161-
console.log(
162-
"CommandPalette isOpen:",
163-
isOpen,
164-
"categories:",
165-
categories.length,
166-
); // Debug log
167-
168162
if (!isOpen) {
169-
console.log("CommandPalette not rendering due to isOpen=false");
170163
return null;
171164
}
172165

@@ -227,16 +220,11 @@ const CommandPalette: React.FC<CommandPaletteProps> = ({
227220
href={item.link}
228221
onClick={(e) => {
229222
e.preventDefault();
230-
console.log(
231-
"Search result clicked:",
232-
item.name,
233-
);
234223

235224
onClose();
236225

237226
setSearchText("");
238227

239-
console.log("Navigating to:", item.link);
240228
navigate(item.link);
241229
}}
242230
style={{
@@ -253,6 +241,17 @@ const CommandPalette: React.FC<CommandPaletteProps> = ({
253241
{item.description}
254242
</div>
255243
)}
244+
{item.projectId && (
245+
<div
246+
style={{
247+
fontSize: "0.85em",
248+
color: "#69707D",
249+
marginTop: "4px",
250+
}}
251+
>
252+
Project: {item.projectId}
253+
</div>
254+
)}
256255
</EuiFlexItem>
257256
{item.type && (
258257
<EuiFlexItem grow={false}>

ui/src/components/GlobalSearchShortcut.tsx

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

28-
console.log("Adding keydown event listener to window");
2919
window.addEventListener("keydown", handleKeyDown, true);
3020
return () => {
3121
window.removeEventListener("keydown", handleKeyDown, true);

ui/src/components/ProjectSelector.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { EuiSelect, useGeneratedHtmlId } from "@elastic/eui";
22
import React from "react";
3-
import { useNavigate, useParams } from "react-router-dom";
3+
import { useNavigate, useParams, useLocation } from "react-router-dom";
44
import { useLoadProjectsList } from "../contexts/ProjectListContext";
55

66
const ProjectSelector = () => {
77
const { projectName } = useParams();
88
const navigate = useNavigate();
9+
const location = useLocation();
910

1011
const { isLoading, data } = useLoadProjectsList();
1112

@@ -22,7 +23,20 @@ const ProjectSelector = () => {
2223

2324
const basicSelectId = useGeneratedHtmlId({ prefix: "basicSelect" });
2425
const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
25-
navigate(`/p/${e.target.value}`);
26+
const newProjectId = e.target.value;
27+
28+
// If we're on a project page, maintain the current path context
29+
if (projectName && location.pathname.startsWith(`/p/${projectName}`)) {
30+
// Replace the old project name with the new one in the current path
31+
const newPath = location.pathname.replace(
32+
`/p/${projectName}`,
33+
`/p/${newProjectId}`,
34+
);
35+
navigate(newPath);
36+
} else {
37+
// Otherwise, just navigate to the project home
38+
navigate(`/p/${newProjectId}`);
39+
}
2640
};
2741

2842
return (

ui/src/components/RegistrySearch.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const RegistrySearch = forwardRef<RegistrySearchRef, RegistrySearchProps>(
112112
? String(item.spec.description || "")
113113
: "",
114114
type: getItemType(item, name),
115+
projectId: "projectId" in item ? String(item.projectId) : undefined,
115116
};
116117
});
117118

@@ -187,6 +188,17 @@ const RegistrySearch = forwardRef<RegistrySearchRef, RegistrySearchProps>(
187188
{item.description}
188189
</div>
189190
)}
191+
{item.projectId && (
192+
<div
193+
style={{
194+
fontSize: "0.85em",
195+
color: "#69707D",
196+
marginTop: "4px",
197+
}}
198+
>
199+
Project: {item.projectId}
200+
</div>
201+
)}
190202
</EuiFlexItem>
191203
{item.type && (
192204
<EuiFlexItem grow={false}>

ui/src/components/RegistryVisualizationTab.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useContext, useState } from "react";
2+
import { useParams } from "react-router-dom";
23
import {
34
EuiEmptyPrompt,
45
EuiLoadingSpinner,
@@ -16,7 +17,11 @@ import { filterPermissionsByAction } from "../utils/permissionUtils";
1617

1718
const RegistryVisualizationTab = () => {
1819
const registryUrl = useContext(RegistryPathContext);
19-
const { isLoading, isSuccess, isError, data } = useLoadRegistry(registryUrl);
20+
const { projectName } = useParams();
21+
const { isLoading, isSuccess, isError, data } = useLoadRegistry(
22+
registryUrl,
23+
projectName,
24+
);
2025
const [selectedObjectType, setSelectedObjectType] = useState("");
2126
const [selectedObjectName, setSelectedObjectName] = useState("");
2227
const [selectedPermissionAction, setSelectedPermissionAction] = useState("");

ui/src/mocks/handlers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ const registry = readFileSync(
88

99
const projectsListWithDefaultProject = http.get("/projects-list.json", () =>
1010
HttpResponse.json({
11-
default: "credit_score_project",
11+
default: "credit_scoring_aws",
1212
projects: [
1313
{
1414
name: "Credit Score Project",
1515
description: "Project for credit scoring team and associated models.",
16-
id: "credit_score_project",
16+
id: "credit_scoring_aws",
1717
registryPath: "/registry.db", // Changed to match what the test expects
1818
},
1919
],

ui/src/pages/Layout.tsx

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,19 @@ const Layout = () => {
4242
});
4343

4444
const registryPath = currentProject?.registryPath || "";
45-
const { data } = useLoadRegistry(registryPath);
4645

46+
// For global search, use the first available registry path (typically all projects share the same registry)
47+
// If projects have different registries, we use the first one as the "global" registry
48+
const globalRegistryPath =
49+
projectsData?.projects?.[0]?.registryPath || registryPath;
50+
51+
// Load filtered data for current project (for sidebar and page-level search)
52+
const { data } = useLoadRegistry(registryPath, projectName);
53+
54+
// Load unfiltered data for global search (across all projects)
55+
const { data: globalData } = useLoadRegistry(globalRegistryPath);
56+
57+
// Categories for page-level search (filtered to current project)
4758
const categories = data
4859
? [
4960
{
@@ -84,31 +95,92 @@ const Layout = () => {
8495
]
8596
: [];
8697

98+
// Helper function to extract project ID from an item
99+
const getProjectId = (item: any): string => {
100+
// Try different possible locations for the project field
101+
return item?.spec?.project || item?.project || projectName || "unknown";
102+
};
103+
104+
// Categories for global search (includes all projects)
105+
const globalCategories = globalData
106+
? [
107+
{
108+
name: "Data Sources",
109+
data: (globalData.objects.dataSources || []).map((item: any) => ({
110+
...item,
111+
projectId: getProjectId(item),
112+
})),
113+
getLink: (item: any) => {
114+
const project = item?.projectId || getProjectId(item);
115+
return `/p/${project}/data-source/${item.name}`;
116+
},
117+
},
118+
{
119+
name: "Entities",
120+
data: (globalData.objects.entities || []).map((item: any) => ({
121+
...item,
122+
projectId: getProjectId(item),
123+
})),
124+
getLink: (item: any) => {
125+
const project = item?.projectId || getProjectId(item);
126+
return `/p/${project}/entity/${item.name}`;
127+
},
128+
},
129+
{
130+
name: "Features",
131+
data: (globalData.allFeatures || []).map((item: any) => ({
132+
...item,
133+
projectId: getProjectId(item),
134+
})),
135+
getLink: (item: any) => {
136+
const featureView = item?.featureView;
137+
const project = item?.projectId || getProjectId(item);
138+
return featureView
139+
? `/p/${project}/feature-view/${featureView}/feature/${item.name}`
140+
: "#";
141+
},
142+
},
143+
{
144+
name: "Feature Views",
145+
data: (globalData.mergedFVList || []).map((item: any) => ({
146+
...item,
147+
projectId: getProjectId(item),
148+
})),
149+
getLink: (item: any) => {
150+
const project = item?.projectId || getProjectId(item);
151+
return `/p/${project}/feature-view/${item.name}`;
152+
},
153+
},
154+
{
155+
name: "Feature Services",
156+
data: (globalData.objects.featureServices || []).map((item: any) => ({
157+
...item,
158+
projectId: getProjectId(item),
159+
})),
160+
getLink: (item: any) => {
161+
const serviceName = item?.name || item?.spec?.name;
162+
const project = item?.projectId || getProjectId(item);
163+
return serviceName
164+
? `/p/${project}/feature-service/${serviceName}`
165+
: "#";
166+
},
167+
},
168+
]
169+
: [];
170+
87171
const handleSearchOpen = () => {
88-
console.log("Opening command palette - before state update"); // Debug log
89172
setIsCommandPaletteOpen(true);
90-
console.log("Command palette state should be updated to true");
91173
};
92174

93175
useEffect(() => {
94176
const handleKeyDown = (event: KeyboardEvent) => {
95-
console.log(
96-
"Layout key pressed:",
97-
event.key,
98-
"metaKey:",
99-
event.metaKey,
100-
"ctrlKey:",
101-
event.ctrlKey,
102-
);
103177
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
104-
console.log("Layout detected Cmd+K, preventing default");
105178
event.preventDefault();
106179
event.stopPropagation();
107180
handleSearchOpen();
108181
}
109182
};
110183

111-
console.log("Layout adding keydown event listener");
112184
window.addEventListener("keydown", handleKeyDown, true);
113185
return () => {
114186
window.removeEventListener("keydown", handleKeyDown, true);
@@ -121,7 +193,7 @@ const Layout = () => {
121193
<CommandPalette
122194
isOpen={isCommandPaletteOpen}
123195
onClose={() => setIsCommandPaletteOpen(false)}
124-
categories={categories}
196+
categories={globalCategories}
125197
/>
126198
<EuiPage paddingSize="none" style={{ background: "transparent" }}>
127199
<EuiPageSidebar
@@ -179,7 +251,10 @@ const Layout = () => {
179251
grow={false}
180252
style={{ width: "600px", maxWidth: "90%" }}
181253
>
182-
<RegistrySearch ref={searchRef} categories={categories} />
254+
<RegistrySearch
255+
ref={searchRef}
256+
categories={globalCategories}
257+
/>
183258
</EuiFlexItem>
184259
</EuiFlexGroup>
185260
</div>

0 commit comments

Comments
 (0)