Skip to content

Commit aa3e43a

Browse files
author
Aditya Patil
committed
feat: Added registry refresh button in UI
Signed-off-by: Aditya Patil <adpatil@redhat.com>
1 parent 56e8919 commit aa3e43a

6 files changed

Lines changed: 79 additions & 6 deletions

File tree

docs/how-to-guides/online-server-performance-tuning.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,15 @@ The `registryTTLSeconds` field on the Operator CR (or `--registry_ttl_sec` CLI f
266266
| Production (low-latency) | `thread` | 300 |
267267
| Production (frequent schema changes) | `thread` | 60 |
268268

269+
### UI refresh
270+
271+
The Feast UI caches the project list using the same registry cache. After running `feast apply` to add a new project, it may take up to `cache_ttl_seconds` before the project appears in the UI.
272+
273+
To see new projects faster:
274+
275+
- **Lower the TTL**: Set `cache_ttl_seconds: 10` (or similar) in your `feature_store.yaml` registry config. This makes all registry consumers — including the UI — pick up changes within 10 seconds.
276+
- **Refresh on demand**: The project selection page has a **Refresh** button that explicitly invalidates the server-side registry cache (`POST /api/registry/refresh`) and reloads the project list without a full page refresh.
277+
269278
---
270279

271280
## Online store selection

sdk/python/feast/ui_server.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def _build_projects_list(
6262
registry_path_template = f"{root_path}/api/v1"
6363

6464
try:
65-
projects = store.registry.list_projects(allow_cache=False)
65+
projects = store.registry.list_projects(allow_cache=True)
6666
for proj in projects:
6767
discovered_projects.append(
6868
{
@@ -895,6 +895,11 @@ def get_app(
895895
def get_projects_list():
896896
return _build_projects_list(store, project_id, root_path)
897897

898+
@app.post("/api/registry/refresh")
899+
def refresh_registry():
900+
store.refresh_registry()
901+
return Response(status_code=status.HTTP_200_OK)
902+
898903
@app.get("/api/mlflow-runs")
899904
def get_mlflow_runs(max_results: int = 50):
900905
"""Return MLflow runs linked to this Feast project via auto-logging."""

sdk/python/tests/unit/test_ui_server.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,19 @@ def test_projects_list_dynamic_refresh(mock_feature_store):
280280
assertpy.assert_that(data["projects"][0]["id"]).is_equal_to("all")
281281
assertpy.assert_that(data["projects"][1]["id"]).is_equal_to("picked_elk")
282282
assertpy.assert_that(data["projects"][2]["id"]).is_equal_to("picked_elk2")
283+
284+
285+
def test_registry_refresh_endpoint(mock_feature_store):
286+
"""POST /api/registry/refresh calls store.refresh_registry()."""
287+
mock_feature_store.refresh_registry = MagicMock()
288+
289+
with tempfile.TemporaryDirectory() as temp_dir:
290+
_create_mock_ui_files(temp_dir)
291+
292+
with _setup_importlib_mocks(temp_dir):
293+
app = get_app(mock_feature_store, TEST_PROJECT_NAME)
294+
295+
client = TestClient(app)
296+
resp = client.post("/api/registry/refresh")
297+
assertpy.assert_that(resp.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS)
298+
mock_feature_store.refresh_registry.assert_called_once()

ui/src/FeastUISansProviders.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,12 @@ const FeastUISansProviders = ({
7979
? {
8080
projectsListPromise: feastUIConfigs?.projectListPromise,
8181
isCustom: true,
82+
basename,
8283
}
8384
: {
8485
projectsListPromise: defaultProjectListPromise(basename),
8586
isCustom: false,
87+
basename,
8688
};
8789

8890
return (

ui/src/contexts/ProjectListContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type ProjectsListType = z.infer<typeof ProjectsListSchema>;
2020
interface ProjectsListContextInterface {
2121
projectsListPromise: Promise<any>;
2222
isCustom: boolean;
23+
basename?: string;
2324
}
2425

2526
const ProjectListContext = React.createContext<

ui/src/pages/RootProjectSelectionPage.tsx

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import React, { useEffect } from "react";
1+
import React, { useContext, useEffect, useState } from "react";
22
import {
3+
EuiButtonIcon,
34
EuiCard,
45
EuiFlexGrid,
6+
EuiFlexGroup,
57
EuiFlexItem,
68
EuiIcon,
79
EuiSkeletonText,
@@ -10,13 +12,22 @@ import {
1012
EuiTitle,
1113
EuiHorizontalRule,
1214
} from "@elastic/eui";
13-
import { useLoadProjectsList } from "../contexts/ProjectListContext";
15+
import {
16+
useLoadProjectsList,
17+
ProjectListContext,
18+
ProjectsListSchema,
19+
} from "../contexts/ProjectListContext";
1420
import { useNavigate } from "react-router-dom";
21+
import { useQueryClient } from "react-query";
1522
import FeastIconBlue from "../graphics/FeastIconBlue";
1623

1724
const RootProjectSelectionPage = () => {
1825
const { isLoading, isSuccess, data } = useLoadProjectsList();
1926
const navigate = useNavigate();
27+
const queryClient = useQueryClient();
28+
const projectListCtx = useContext(ProjectListContext);
29+
const basename = projectListCtx?.basename || "";
30+
const [refreshing, setRefreshing] = useState(false);
2031

2132
useEffect(() => {
2233
if (data && data.default) {
@@ -30,6 +41,21 @@ const RootProjectSelectionPage = () => {
3041
}
3142
}, [data, navigate]);
3243

44+
const handleRefresh = async () => {
45+
setRefreshing(true);
46+
try {
47+
await fetch(`${basename}/api/registry/refresh`, { method: "POST" });
48+
const res = await fetch(`${basename}/projects-list.json`, {
49+
headers: { "Content-Type": "application/json" },
50+
});
51+
const json = await res.json();
52+
const parsed = ProjectsListSchema.parse(json);
53+
queryClient.setQueryData("feast-projects-list", parsed);
54+
} finally {
55+
setRefreshing(false);
56+
}
57+
};
58+
3359
const projectCards = data?.projects.map((item, index) => {
3460
return (
3561
<EuiFlexItem key={index}>
@@ -48,9 +74,23 @@ const RootProjectSelectionPage = () => {
4874
return (
4975
<EuiPageTemplate panelled>
5076
<EuiPageTemplate.Section>
51-
<EuiTitle size="s">
52-
<h1>Welcome to Feast</h1>
53-
</EuiTitle>
77+
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
78+
<EuiFlexItem grow={false}>
79+
<EuiTitle size="s">
80+
<h1>Welcome to Feast</h1>
81+
</EuiTitle>
82+
</EuiFlexItem>
83+
<EuiFlexItem grow={false}>
84+
<EuiButtonIcon
85+
iconType="refresh"
86+
aria-label="Refresh projects"
87+
onClick={handleRefresh}
88+
isLoading={refreshing}
89+
display="base"
90+
size="m"
91+
/>
92+
</EuiFlexItem>
93+
</EuiFlexGroup>
5494
<EuiText>
5595
<p>Select one of the projects.</p>
5696
</EuiText>

0 commit comments

Comments
 (0)