Visitar URL original
feat: Implemented registry refresh action feature by AdityaPatil22 · Pull Request #6657 · feast-dev/feast · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: Added registry refresh button in UI
Signed-off-by: Aditya Patil <adpatil@redhat.com>
  • Loading branch information
Aditya Patil
Aditya Patil committed Jul 31, 2026
commit 86a8dca03515e01653f1ad82a3f4cf0e51978e97
9 changes: 9 additions & 0 deletions docs/how-to-guides/online-server-performance-tuning.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,15 @@ The `registryTTLSeconds` field on the Operator CR (or `--registry_ttl_sec` CLI f
| Production (low-latency) | `thread` | 300 |
| Production (frequent schema changes) | `thread` | 60 |

### UI refresh

Comment thread
ntkathole marked this conversation as resolved.
Outdated
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.

To see new projects faster:

- **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.
- **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.

---

## Online store selection
Expand Down
7 changes: 6 additions & 1 deletion sdk/python/feast/ui_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def _build_projects_list(
registry_path_template = f"{root_path}/api/v1"

try:
projects = store.registry.list_projects(allow_cache=False)
projects = store.registry.list_projects(allow_cache=True)
for proj in projects:
discovered_projects.append(
{
Expand Down Expand Up @@ -895,6 +895,11 @@ def get_app(
def get_projects_list():
return _build_projects_list(store, project_id, root_path)

@app.post("/api/registry/refresh")
Comment thread
ntkathole marked this conversation as resolved.
Outdated
def refresh_registry():
store.refresh_registry()
return Response(status_code=status.HTTP_200_OK)

@app.get("/api/mlflow-runs")
def get_mlflow_runs(max_results: int = 50):
"""Return MLflow runs linked to this Feast project via auto-logging."""
Expand Down
16 changes: 16 additions & 0 deletions sdk/python/tests/unit/test_ui_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,19 @@ def test_projects_list_dynamic_refresh(mock_feature_store):
assertpy.assert_that(data["projects"][0]["id"]).is_equal_to("all")
assertpy.assert_that(data["projects"][1]["id"]).is_equal_to("picked_elk")
assertpy.assert_that(data["projects"][2]["id"]).is_equal_to("picked_elk2")


def test_registry_refresh_endpoint(mock_feature_store):
"""POST /api/registry/refresh calls store.refresh_registry()."""
mock_feature_store.refresh_registry = MagicMock()

with tempfile.TemporaryDirectory() as temp_dir:
_create_mock_ui_files(temp_dir)

with _setup_importlib_mocks(temp_dir):
app = get_app(mock_feature_store, TEST_PROJECT_NAME)

client = TestClient(app)
resp = client.post("/api/registry/refresh")
assertpy.assert_that(resp.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS)
mock_feature_store.refresh_registry.assert_called_once()
2 changes: 2 additions & 0 deletions ui/src/FeastUISansProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,12 @@ const FeastUISansProviders = ({
? {
projectsListPromise: feastUIConfigs?.projectListPromise,
isCustom: true,
basename,
}
: {
projectsListPromise: defaultProjectListPromise(basename),
isCustom: false,
basename,
};

return (
Expand Down
1 change: 1 addition & 0 deletions ui/src/contexts/ProjectListContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type ProjectsListType = z.infer<typeof ProjectsListSchema>;
interface ProjectsListContextInterface {
projectsListPromise: Promise<any>;
isCustom: boolean;
basename?: string;
}

const ProjectListContext = React.createContext<
Expand Down
50 changes: 45 additions & 5 deletions ui/src/pages/RootProjectSelectionPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { useEffect } from "react";
import React, { useContext, useEffect, useState } from "react";
import {
EuiButtonIcon,
EuiCard,
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSkeletonText,
Expand All @@ -10,13 +12,22 @@ import {
EuiTitle,
EuiHorizontalRule,
} from "@elastic/eui";
import { useLoadProjectsList } from "../contexts/ProjectListContext";
import {
useLoadProjectsList,
ProjectListContext,
ProjectsListSchema,
} from "../contexts/ProjectListContext";
import { useNavigate } from "react-router-dom";
import { useQueryClient } from "react-query";
import FeastIconBlue from "../graphics/FeastIconBlue";

const RootProjectSelectionPage = () => {
const { isLoading, isSuccess, data } = useLoadProjectsList();
const navigate = useNavigate();
const queryClient = useQueryClient();
const projectListCtx = useContext(ProjectListContext);
const basename = projectListCtx?.basename || "";
const [refreshing, setRefreshing] = useState(false);

useEffect(() => {
if (data && data.default) {
Expand All @@ -30,6 +41,21 @@ const RootProjectSelectionPage = () => {
}
}, [data, navigate]);

const handleRefresh = async () => {
setRefreshing(true);
try {
await fetch(`${basename}/api/registry/refresh`, { method: "POST" });
const res = await fetch(`${basename}/projects-list.json`, {
headers: { "Content-Type": "application/json" },
});
const json = await res.json();
const parsed = ProjectsListSchema.parse(json);
queryClient.setQueryData("feast-projects-list", parsed);
} finally {
setRefreshing(false);
}
};

const projectCards = data?.projects.map((item, index) => {
return (
<EuiFlexItem key={index}>
Expand All @@ -48,9 +74,23 @@ const RootProjectSelectionPage = () => {
return (
<EuiPageTemplate panelled>
<EuiPageTemplate.Section>
<EuiTitle size="s">
<h1>Welcome to Feast</h1>
</EuiTitle>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h1>Welcome to Feast</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
Comment thread
ntkathole marked this conversation as resolved.
Outdated
iconType="refresh"
aria-label="Refresh projects"
onClick={handleRefresh}
isLoading={refreshing}
display="base"
size="m"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiText>
<p>Select one of the projects.</p>
</EuiText>
Expand Down