GeezerVison is showing github.com — feat: Added support for filtering multi-projects (#5688) · feast-dev/feast@eb0a86e
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: sdk/python/feast/ui_server.py +33-4Lines changed: 33 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -61,16 +61,45 @@ def shutdown_event(): | 61 | 61 | with importlib_resources.as_file(ui_dir_ref) as ui_dir: | 62 | 62 | # Initialize with the projects-list.json file | 63 | 63 | 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( | 66 | 84 | { | 67 | 85 | "name": "Project", | 68 | 86 | "description": "Test project", | 69 | 87 | "id": project_id, | 70 | 88 | "registryPath": f"{root_path}/registry", | 71 | 89 | } | 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} | 74 | 103 | f.write(json.dumps(projects_dict)) | 75 | 104 |
| 76 | 105 | @app.get("/registry") | |
Collapse file Copy file name to clipboardExpand all lines: ui/src/components/CommandPalette.tsx +12-13Lines changed: 12 additions & 13 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -149,6 +149,7 @@ const CommandPalette: React.FC<CommandPaletteProps> = ({ | 149 | 149 | ? String(item.spec.description || "") | 150 | 150 | : "", | 151 | 151 | type: getItemType(item, name), | | 152 | + projectId: "projectId" in item ? String(item.projectId) : undefined, | 152 | 153 | }; | 153 | 154 | }); | 154 | 155 |
| @@ -158,15 +159,7 @@ const CommandPalette: React.FC<CommandPaletteProps> = ({ | 158 | 159 | }; | 159 | 160 | }); | 160 | 161 |
| 161 | | - console.log( | 162 | | - "CommandPalette isOpen:", | 163 | | - isOpen, | 164 | | - "categories:", | 165 | | - categories.length, | 166 | | - ); // Debug log | 167 | | - | 168 | 162 | if (!isOpen) { | 169 | | - console.log("CommandPalette not rendering due to isOpen=false"); | 170 | 163 | return null; | 171 | 164 | } | 172 | 165 |
| @@ -227,16 +220,11 @@ const CommandPalette: React.FC<CommandPaletteProps> = ({ | 227 | 220 | href={item.link} | 228 | 221 | onClick={(e) => { | 229 | 222 | e.preventDefault(); | 230 | | - console.log( | 231 | | - "Search result clicked:", | 232 | | - item.name, | 233 | | - ); | 234 | 223 |
| 235 | 224 | onClose(); | 236 | 225 |
| 237 | 226 | setSearchText(""); | 238 | 227 |
| 239 | | - console.log("Navigating to:", item.link); | 240 | 228 | navigate(item.link); | 241 | 229 | }} | 242 | 230 | style={{ | @@ -253,6 +241,17 @@ const CommandPalette: React.FC<CommandPaletteProps> = ({ | 253 | 241 | {item.description} | 254 | 242 | </div> | 255 | 243 | )} | | 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 | + )} | 256 | 255 | </EuiFlexItem> | 257 | 256 | {item.type && ( | 258 | 257 | <EuiFlexItem grow={false}> | |
Collapse file Copy file name to clipboardExpand all lines: ui/src/components/GlobalSearchShortcut.tsx -10Lines changed: 0 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -9,23 +9,13 @@ const GlobalSearchShortcut: React.FC<GlobalSearchShortcutProps> = ({ | 9 | 9 | }) => { | 10 | 10 | useEffect(() => { | 11 | 11 | const handleKeyDown = (event: KeyboardEvent) => { | 12 | | - console.log( | 13 | | - "Key pressed:", | 14 | | - event.key, | 15 | | - "metaKey:", | 16 | | - event.metaKey, | 17 | | - "ctrlKey:", | 18 | | - event.ctrlKey, | 19 | | - ); | 20 | 12 | if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { | 21 | | - console.log("Cmd+K detected, preventing default and calling onOpen"); | 22 | 13 | event.preventDefault(); | 23 | 14 | event.stopPropagation(); | 24 | 15 | onOpen(); | 25 | 16 | } | 26 | 17 | }; | 27 | 18 |
| 28 | | - console.log("Adding keydown event listener to window"); | 29 | 19 | window.addEventListener("keydown", handleKeyDown, true); | 30 | 20 | return () => { | 31 | 21 | window.removeEventListener("keydown", handleKeyDown, true); | |
Collapse file Copy file name to clipboardExpand all lines: ui/src/components/ProjectSelector.tsx +16-2Lines changed: 16 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | import { EuiSelect, useGeneratedHtmlId } from "@elastic/eui"; | 2 | 2 | import React from "react"; | 3 | | -import { useNavigate, useParams } from "react-router-dom"; | | 3 | +import { useNavigate, useParams, useLocation } from "react-router-dom"; | 4 | 4 | import { useLoadProjectsList } from "../contexts/ProjectListContext"; | 5 | 5 |
| 6 | 6 | const ProjectSelector = () => { | 7 | 7 | const { projectName } = useParams(); | 8 | 8 | const navigate = useNavigate(); | | 9 | + const location = useLocation(); | 9 | 10 |
| 10 | 11 | const { isLoading, data } = useLoadProjectsList(); | 11 | 12 |
| @@ -22,7 +23,20 @@ const ProjectSelector = () => { | 22 | 23 |
| 23 | 24 | const basicSelectId = useGeneratedHtmlId({ prefix: "basicSelect" }); | 24 | 25 | 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 | + } | 26 | 40 | }; | 27 | 41 |
| 28 | 42 | return ( | |
Collapse file Copy file name to clipboardExpand all lines: ui/src/components/RegistrySearch.tsx +12Lines changed: 12 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -112,6 +112,7 @@ const RegistrySearch = forwardRef<RegistrySearchRef, RegistrySearchProps>( | 112 | 112 | ? String(item.spec.description || "") | 113 | 113 | : "", | 114 | 114 | type: getItemType(item, name), | | 115 | + projectId: "projectId" in item ? String(item.projectId) : undefined, | 115 | 116 | }; | 116 | 117 | }); | 117 | 118 |
| @@ -187,6 +188,17 @@ const RegistrySearch = forwardRef<RegistrySearchRef, RegistrySearchProps>( | 187 | 188 | {item.description} | 188 | 189 | </div> | 189 | 190 | )} | | 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 | + )} | 190 | 202 | </EuiFlexItem> | 191 | 203 | {item.type && ( | 192 | 204 | <EuiFlexItem grow={false}> | |
Collapse file Copy file name to clipboardExpand all lines: ui/src/components/RegistryVisualizationTab.tsx +6-1Lines changed: 6 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | import React, { useContext, useState } from "react"; | | 2 | +import { useParams } from "react-router-dom"; | 2 | 3 | import { | 3 | 4 | EuiEmptyPrompt, | 4 | 5 | EuiLoadingSpinner, | @@ -16,7 +17,11 @@ import { filterPermissionsByAction } from "../utils/permissionUtils"; | 16 | 17 |
| 17 | 18 | const RegistryVisualizationTab = () => { | 18 | 19 | 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 | + ); | 20 | 25 | const [selectedObjectType, setSelectedObjectType] = useState(""); | 21 | 26 | const [selectedObjectName, setSelectedObjectName] = useState(""); | 22 | 27 | const [selectedPermissionAction, setSelectedPermissionAction] = useState(""); | |
Collapse file Copy file name to clipboardExpand all lines: ui/src/mocks/handlers.ts +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -8,12 +8,12 @@ const registry = readFileSync( | 8 | 8 |
| 9 | 9 | const projectsListWithDefaultProject = http.get("/projects-list.json", () => | 10 | 10 | HttpResponse.json({ | 11 | | - default: "credit_score_project", | | 11 | + default: "credit_scoring_aws", | 12 | 12 | projects: [ | 13 | 13 | { | 14 | 14 | name: "Credit Score Project", | 15 | 15 | description: "Project for credit scoring team and associated models.", | 16 | | - id: "credit_score_project", | | 16 | + id: "credit_scoring_aws", | 17 | 17 | registryPath: "/registry.db", // Changed to match what the test expects | 18 | 18 | }, | 19 | 19 | ], | |
Collapse file Copy file name to clipboardExpand all lines: ui/src/pages/Layout.tsx +90-15Lines changed: 90 additions & 15 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -42,8 +42,19 @@ const Layout = () => { | 42 | 42 | }); | 43 | 43 |
| 44 | 44 | const registryPath = currentProject?.registryPath || ""; | 45 | | - const { data } = useLoadRegistry(registryPath); | 46 | 45 |
| | 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) | 47 | 58 | const categories = data | 48 | 59 | ? [ | 49 | 60 | { | @@ -84,31 +95,92 @@ const Layout = () => { | 84 | 95 | ] | 85 | 96 | : []; | 86 | 97 |
| | 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 | + | 87 | 171 | const handleSearchOpen = () => { | 88 | | - console.log("Opening command palette - before state update"); // Debug log | 89 | 172 | setIsCommandPaletteOpen(true); | 90 | | - console.log("Command palette state should be updated to true"); | 91 | 173 | }; | 92 | 174 |
| 93 | 175 | useEffect(() => { | 94 | 176 | 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 | | - ); | 103 | 177 | if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { | 104 | | - console.log("Layout detected Cmd+K, preventing default"); | 105 | 178 | event.preventDefault(); | 106 | 179 | event.stopPropagation(); | 107 | 180 | handleSearchOpen(); | 108 | 181 | } | 109 | 182 | }; | 110 | 183 |
| 111 | | - console.log("Layout adding keydown event listener"); | 112 | 184 | window.addEventListener("keydown", handleKeyDown, true); | 113 | 185 | return () => { | 114 | 186 | window.removeEventListener("keydown", handleKeyDown, true); | @@ -121,7 +193,7 @@ const Layout = () => { | 121 | 193 | <CommandPalette | 122 | 194 | isOpen={isCommandPaletteOpen} | 123 | 195 | onClose={() => setIsCommandPaletteOpen(false)} | 124 | | - categories={categories} | | 196 | + categories={globalCategories} | 125 | 197 | /> | 126 | 198 | <EuiPage paddingSize="none" style={{ background: "transparent" }}> | 127 | 199 | <EuiPageSidebar | @@ -179,7 +251,10 @@ const Layout = () => { | 179 | 251 | grow={false} | 180 | 252 | style={{ width: "600px", maxWidth: "90%" }} | 181 | 253 | > | 182 | | - <RegistrySearch ref={searchRef} categories={categories} /> | | 254 | + <RegistrySearch | | 255 | + ref={searchRef} | | 256 | + categories={globalCategories} | | 257 | + /> | 183 | 258 | </EuiFlexItem> | 184 | 259 | </EuiFlexGroup> | 185 | 260 | </div> | |
|