Skip to content

Commit 6ae80af

Browse files
authored
feat: REST API-backed UI for RBAC compatibility and per-page lazy loading (#6414)
* feat: Phase 1 - Added different data modes to UI server Signed-off-by: ntkathole <[email protected]> * feat: Phase 2 - REST Data Fetching in UI Signed-off-by: ntkathole <[email protected]> * feat: Phase 3 - Per-Page Lazy UI Loading when using rest mode Signed-off-by: ntkathole <[email protected]> * feat: Phase 4 - Default rest mode Signed-off-by: ntkathole <[email protected]> --------- Signed-off-by: ntkathole <[email protected]>
1 parent 00dea50 commit 6ae80af

36 files changed

Lines changed: 1454 additions & 1011 deletions

‎sdk/python/feast/cli/ui.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ def ui(
7070
"Please configure --key and --cert args to start the feature server in SSL mode."
7171
)
7272
store = create_feature_store(ctx)
73-
# Pass in the registry_dump method to get around a circular dependency
7473
store.serve_ui(
7574
host=host,
7675
port=port,

‎sdk/python/feast/ui_server.py‎

Lines changed: 82 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23
import threading
34
from importlib import resources as importlib_resources
45
from typing import Callable, Optional
@@ -10,24 +11,59 @@
1011

1112
import feast
1213

14+
logger = logging.getLogger(__name__)
1315

14-
def get_app(
16+
17+
def _build_projects_list(
1518
store: "feast.FeatureStore",
1619
project_id: str,
17-
registry_ttl_secs: int,
18-
root_path: str = "",
20+
root_path: str,
1921
):
20-
app = FastAPI()
22+
"""Build the projects list for the UI."""
23+
discovered_projects = []
24+
registry = store.registry.proto()
2125

22-
app.add_middleware(
23-
CORSMiddleware,
24-
allow_origins=["*"],
25-
allow_credentials=True,
26-
allow_methods=["*"],
27-
allow_headers=["*"],
28-
)
26+
registry_path_template = f"{root_path}/api/v1"
27+
28+
if registry and registry.projects and len(registry.projects) > 0:
29+
for proj in registry.projects:
30+
if proj.spec and proj.spec.name:
31+
discovered_projects.append(
32+
{
33+
"name": proj.spec.name.replace("_", " ").title(),
34+
"description": proj.spec.description
35+
or f"Project: {proj.spec.name}",
36+
"id": proj.spec.name,
37+
"registryPath": registry_path_template,
38+
}
39+
)
40+
else:
41+
discovered_projects.append(
42+
{
43+
"name": "Project",
44+
"description": "Test project",
45+
"id": project_id,
46+
"registryPath": registry_path_template,
47+
}
48+
)
49+
50+
if len(discovered_projects) > 1:
51+
all_projects_entry = {
52+
"name": "All Projects",
53+
"description": "View data across all projects",
54+
"id": "all",
55+
"registryPath": registry_path_template,
56+
}
57+
discovered_projects.insert(0, all_projects_entry)
58+
59+
return {"projects": discovered_projects}
60+
61+
62+
def _setup_rest_mode(app: FastAPI, store: "feast.FeatureStore", registry_ttl_secs: int):
63+
"""Mount the REST registry API routes on the UI server under /api/v1."""
64+
from feast.api.registry.rest import register_all_routes
65+
from feast.registry_server import RegistryServer
2966

30-
# Asynchronously refresh registry, notifying shutdown and canceling the active timer if the app is shutting down
3167
registry_proto = None
3268
shutting_down = False
3369
active_timer: Optional[threading.Timer] = None
@@ -51,61 +87,11 @@ def shutdown_event():
5187

5288
async_refresh()
5389

54-
ui_dir_ref = importlib_resources.files(__spec__.parent) / "ui/build/" # type: ignore[name-defined, arg-type]
55-
with importlib_resources.as_file(ui_dir_ref) as ui_dir:
56-
# Initialize with the projects-list.json file
57-
with ui_dir.joinpath("projects-list.json").open(mode="w") as f:
58-
# Get all projects from the registry
59-
discovered_projects = []
60-
registry = store.registry.proto()
61-
62-
# Use the projects list from the registry
63-
if registry and registry.projects and len(registry.projects) > 0:
64-
for proj in registry.projects:
65-
if proj.spec and proj.spec.name:
66-
discovered_projects.append(
67-
{
68-
"name": proj.spec.name.replace("_", " ").title(),
69-
"description": proj.spec.description
70-
or f"Project: {proj.spec.name}",
71-
"id": proj.spec.name,
72-
"registryPath": f"{root_path}/registry",
73-
}
74-
)
75-
else:
76-
# If no projects in registry, use the current project from feature_store.yaml
77-
discovered_projects.append(
78-
{
79-
"name": "Project",
80-
"description": "Test project",
81-
"id": project_id,
82-
"registryPath": f"{root_path}/registry",
83-
}
84-
)
90+
grpc_handler = RegistryServer(store.registry)
8591

86-
# Add "All Projects" option at the beginning if there are multiple projects
87-
if len(discovered_projects) > 1:
88-
all_projects_entry = {
89-
"name": "All Projects",
90-
"description": "View data across all projects",
91-
"id": "all",
92-
"registryPath": f"{root_path}/registry",
93-
}
94-
discovered_projects.insert(0, all_projects_entry)
95-
96-
projects_dict = {"projects": discovered_projects}
97-
f.write(json.dumps(projects_dict))
98-
99-
@app.get("/registry")
100-
def read_registry():
101-
if registry_proto is None:
102-
return Response(
103-
status_code=status.HTTP_503_SERVICE_UNAVAILABLE
104-
) # Service Unavailable
105-
return Response(
106-
content=registry_proto.SerializeToString(),
107-
media_type="application/octet-stream",
108-
)
92+
rest_app = FastAPI(root_path="/api/v1")
93+
register_all_routes(rest_app, grpc_handler)
94+
app.mount("/api/v1", rest_app)
10995

11096
@app.get("/health")
11197
def health():
@@ -115,14 +101,38 @@ def health():
115101
else Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
116102
)
117103

118-
# For all other paths (such as paths that would otherwise be handled by react router), pass to React
104+
logger.info("REST registry API mounted at /api/v1")
105+
106+
107+
def get_app(
108+
store: "feast.FeatureStore",
109+
project_id: str,
110+
registry_ttl_secs: int,
111+
root_path: str = "",
112+
):
113+
app = FastAPI()
114+
115+
app.add_middleware(
116+
CORSMiddleware,
117+
allow_origins=["*"],
118+
allow_credentials=True,
119+
allow_methods=["*"],
120+
allow_headers=["*"],
121+
)
122+
123+
_setup_rest_mode(app, store, registry_ttl_secs)
124+
125+
ui_dir_ref = importlib_resources.files(__spec__.parent) / "ui/build/" # type: ignore[name-defined, arg-type]
126+
with importlib_resources.as_file(ui_dir_ref) as ui_dir:
127+
projects_dict = _build_projects_list(store, project_id, root_path)
128+
with ui_dir.joinpath("projects-list.json").open(mode="w") as f:
129+
f.write(json.dumps(projects_dict))
130+
119131
@app.api_route("/p/{path_name:path}", methods=["GET"])
120132
def catch_all():
121133
filename = ui_dir.joinpath("index.html")
122-
123134
with open(filename) as f:
124135
content = f.read()
125-
126136
return Response(content, media_type="text/html")
127137

128138
app.mount(
@@ -151,6 +161,9 @@ def start_server(
151161
registry_ttl_sec,
152162
root_path,
153163
)
164+
165+
logger.info(f"Starting Feast UI server on {host}:{port}")
166+
154167
if tls_key_path and tls_cert_path:
155168
uvicorn.run(
156169
app,

0 commit comments

Comments
 (0)