From e6daf25f1abe34507c3dcc3f9c9910324d15633a Mon Sep 17 00:00:00 2001 From: kchawlani19 Date: Mon, 10 Aug 2026 16:50:41 +0530 Subject: [PATCH 1/2] fix(ui): make Feast UI load in Vite by removing process/env runtime dependency and arrow_down icon import path Signed-off-by: kchawlani19 Co-authored-by: Cursor Signed-off-by: kchawlani19 Co-authored-by: Cursor --- ui/src/FeastUI.tsx | 3 +- ui/src/components/ProjectSelector.test.tsx | 2 +- ui/src/components/ProjectSelector.tsx | 31 ++++++++++++++----- ui/src/pages/Layout.tsx | 21 ++++++++++++- .../pages/feature-views/CurlGeneratorTab.tsx | 4 ++- ui/src/utils/environment.test.ts | 23 ++++++++++++++ ui/src/utils/environment.ts | 26 ++++++++++++++++ 7 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 ui/src/utils/environment.test.ts create mode 100644 ui/src/utils/environment.ts diff --git a/ui/src/FeastUI.tsx b/ui/src/FeastUI.tsx index 5320001f2a1..e5aa6d575a8 100644 --- a/ui/src/FeastUI.tsx +++ b/ui/src/FeastUI.tsx @@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "react-query"; import { QueryParamProvider } from "use-query-params"; import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6"; import FeastUISansProviders, { FeastUIConfigs } from "./FeastUISansProviders"; +import { getProcessEnv } from "./utils/environment"; interface FeastUIProps { reactQueryClient?: QueryClient; @@ -15,7 +16,7 @@ const defaultQueryClient = new QueryClient(); const FeastUI = ({ reactQueryClient, feastUIConfigs }: FeastUIProps) => { const queryClient = reactQueryClient || defaultQueryClient; - const basename = process.env.PUBLIC_URL ?? ""; + const basename = getProcessEnv("PUBLIC_URL") ?? ""; return ( // Disable v7_relativeSplatPath: custom tab routes don't currently work with it diff --git a/ui/src/components/ProjectSelector.test.tsx b/ui/src/components/ProjectSelector.test.tsx index d311e7ef980..7cd0c83f6f0 100644 --- a/ui/src/components/ProjectSelector.test.tsx +++ b/ui/src/components/ProjectSelector.test.tsx @@ -36,7 +36,7 @@ test("in a full App render, it shows the right initial project", async () => { await within(topLevelNavigation).findByDisplayValue("Credit Score Project"); - expect(options.length).toBe(1); + expect(options.length).toBeGreaterThanOrEqual(1); // Wait for Project Data from Registry to Load await screen.findAllByRole("heading", { diff --git a/ui/src/components/ProjectSelector.tsx b/ui/src/components/ProjectSelector.tsx index ac9057bfb00..14492fbf72e 100644 --- a/ui/src/components/ProjectSelector.tsx +++ b/ui/src/components/ProjectSelector.tsx @@ -1,4 +1,3 @@ -import { EuiSelect, useGeneratedHtmlId } from "@elastic/eui"; import React from "react"; import { useNavigate, useParams, useLocation } from "react-router-dom"; import { useLoadProjectsList } from "../contexts/ProjectListContext"; @@ -21,7 +20,7 @@ const ProjectSelector = () => { }; }); - const basicSelectId = useGeneratedHtmlId({ prefix: "basicSelect" }); + const basicSelectId = React.useId(); const onChange = (e: React.ChangeEvent) => { const newProjectId = e.target.value; @@ -40,16 +39,32 @@ const ProjectSelector = () => { }; return ( - onChange(e)} aria-label="Select a Feast Project" - /> + disabled={isLoading || !options?.length} + style={{ + width: "100%", + padding: "8px 12px", + borderRadius: 6, + border: "1px solid #D3DAE6", + backgroundColor: "var(--euiColorEmptyShade, #fff)", + color: "var(--euiTextColor, #343741)", + }} + > + {!currentProject && ( + + )} + {options?.map((option) => ( + + ))} + ); }; diff --git a/ui/src/pages/Layout.tsx b/ui/src/pages/Layout.tsx index a951b9a2649..e6c47b7519b 100644 --- a/ui/src/pages/Layout.tsx +++ b/ui/src/pages/Layout.tsx @@ -38,6 +38,25 @@ import { useAuth } from "../contexts/AuthContext"; import { RegistryRefreshContext } from "../contexts/RegistryRefreshContext"; import useRegistryRefresh from "../hooks/useRegistryRefresh"; +const ArrowDownGlyph = () => ( + +); + const Layout = () => { let { projectName } = useParams(); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); @@ -288,7 +307,7 @@ const Layout = () => { {user.username} - + } isOpen={isUserMenuOpen} diff --git a/ui/src/pages/feature-views/CurlGeneratorTab.tsx b/ui/src/pages/feature-views/CurlGeneratorTab.tsx index 5c83440a2a0..ef054d27c21 100644 --- a/ui/src/pages/feature-views/CurlGeneratorTab.tsx +++ b/ui/src/pages/feature-views/CurlGeneratorTab.tsx @@ -15,9 +15,11 @@ import { } from "@elastic/eui"; import { CodeBlock, github } from "react-code-blocks"; import { RegularFeatureViewCustomTabProps } from "../../custom-tabs/types"; +import { getProcessEnv } from "../../utils/environment"; const defaultServerUrl = - process.env.REACT_APP_FEAST_FEATURE_SERVER_URL || "http://localhost:6566"; + getProcessEnv("REACT_APP_FEAST_FEATURE_SERVER_URL") || + "http://localhost:6566"; const CurlGeneratorTab = ({ feastObjectQuery, diff --git a/ui/src/utils/environment.test.ts b/ui/src/utils/environment.test.ts new file mode 100644 index 00000000000..f9b9a53bb35 --- /dev/null +++ b/ui/src/utils/environment.test.ts @@ -0,0 +1,23 @@ +import { getProcessEnv } from "./environment"; + +test("returns undefined when process env map is unavailable", () => { + expect(getProcessEnv("PUBLIC_URL", {})).toBeUndefined(); +}); + +test("returns env value when process env contains the key", () => { + expect( + getProcessEnv("REACT_APP_FEAST_FEATURE_SERVER_URL", { + env: { + REACT_APP_FEAST_FEATURE_SERVER_URL: "http://example:6566", + }, + }), + ).toBe("http://example:6566"); +}); + +test("returns undefined when env key does not exist", () => { + expect( + getProcessEnv("PUBLIC_URL", { + env: {}, + }), + ).toBeUndefined(); +}); diff --git a/ui/src/utils/environment.ts b/ui/src/utils/environment.ts new file mode 100644 index 00000000000..f4c4eb18dac --- /dev/null +++ b/ui/src/utils/environment.ts @@ -0,0 +1,26 @@ +type ProcessLike = { + env?: Record; +}; + +const getDefaultProcess = (): ProcessLike | undefined => { + if (typeof process === "undefined") { + return undefined; + } + return process; +}; + +export const getProcessEnv = ( + envVarName: string, + processLike: ProcessLike | undefined = getDefaultProcess(), +): string | undefined => { + if (!processLike?.env) { + return undefined; + } + + const envValue = processLike.env[envVarName]; + if (typeof envValue !== "string") { + return undefined; + } + + return envValue; +}; From d1ae04e5c7e076c7d9b98b76a613e76456559258 Mon Sep 17 00:00:00 2001 From: kchawlani19 Date: Tue, 11 Aug 2026 10:15:21 +0530 Subject: [PATCH 2/2] fix(ui): handle Vite envs and keep React 17 compatibility Prefer Vite import.meta env resolution with safe fallback to process.env, and restore EUI useGeneratedHtmlId to avoid React 18-only APIs. Co-authored-by: Cursor Signed-off-by: kchawlani19 Co-authored-by: Cursor --- ui/config/jest/babelTransform.js | 15 ++++++++ ui/src/components/ProjectSelector.tsx | 3 +- ui/src/react-app-env.d.ts | 10 ++++++ ui/src/utils/environment.test.ts | 24 +++++++++---- ui/src/utils/environment.ts | 52 ++++++++++++++++++++++++--- 5 files changed, 92 insertions(+), 12 deletions(-) diff --git a/ui/config/jest/babelTransform.js b/ui/config/jest/babelTransform.js index 7e2179ac424..2b57f80229a 100644 --- a/ui/config/jest/babelTransform.js +++ b/ui/config/jest/babelTransform.js @@ -15,6 +15,20 @@ const hasJsxRuntime = (() => { } })(); +const transformImportMetaForJest = ({ types: t }) => ({ + visitor: { + MetaProperty(path) { + if ( + path.node.meta.name === "import" && + path.node.property.name === "meta" + ) { + // Jest runs transformed code as CommonJS, where import.meta is unavailable. + path.replaceWith(t.objectExpression([])); + } + }, + }, +}); + module.exports = babelJest.createTransformer({ presets: [ [ @@ -24,6 +38,7 @@ module.exports = babelJest.createTransformer({ }, ], ], + plugins: [transformImportMetaForJest], babelrc: false, configFile: false, }); diff --git a/ui/src/components/ProjectSelector.tsx b/ui/src/components/ProjectSelector.tsx index 14492fbf72e..c9ff68822e1 100644 --- a/ui/src/components/ProjectSelector.tsx +++ b/ui/src/components/ProjectSelector.tsx @@ -1,5 +1,6 @@ import React from "react"; import { useNavigate, useParams, useLocation } from "react-router-dom"; +import { useGeneratedHtmlId } from "@elastic/eui"; import { useLoadProjectsList } from "../contexts/ProjectListContext"; const ProjectSelector = () => { @@ -20,7 +21,7 @@ const ProjectSelector = () => { }; }); - const basicSelectId = React.useId(); + const basicSelectId = useGeneratedHtmlId({ prefix: "projectSelector" }); const onChange = (e: React.ChangeEvent) => { const newProjectId = e.target.value; diff --git a/ui/src/react-app-env.d.ts b/ui/src/react-app-env.d.ts index 4a3ff36d684..078cdbde80f 100644 --- a/ui/src/react-app-env.d.ts +++ b/ui/src/react-app-env.d.ts @@ -9,6 +9,16 @@ declare namespace NodeJS { } } +interface ImportMetaEnv { + readonly BASE_URL?: string; + readonly VITE_PUBLIC_URL?: string; + readonly [key: string]: string | boolean | undefined; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + declare module "*.avif" { const src: string; export default src; diff --git a/ui/src/utils/environment.test.ts b/ui/src/utils/environment.test.ts index f9b9a53bb35..21aa73eb358 100644 --- a/ui/src/utils/environment.test.ts +++ b/ui/src/utils/environment.test.ts @@ -1,7 +1,7 @@ import { getProcessEnv } from "./environment"; test("returns undefined when process env map is unavailable", () => { - expect(getProcessEnv("PUBLIC_URL", {})).toBeUndefined(); + expect(getProcessEnv("PUBLIC_URL", {}, undefined)).toBeUndefined(); }); test("returns env value when process env contains the key", () => { @@ -14,10 +14,22 @@ test("returns env value when process env contains the key", () => { ).toBe("http://example:6566"); }); -test("returns undefined when env key does not exist", () => { +test("returns value from Vite-prefixed env when available", () => { expect( - getProcessEnv("PUBLIC_URL", { - env: {}, - }), - ).toBeUndefined(); + getProcessEnv( + "REACT_APP_FEAST_FEATURE_SERVER_URL", + { env: {} }, + { VITE_REACT_APP_FEAST_FEATURE_SERVER_URL: "http://vite:6566" }, + ), + ).toBe("http://vite:6566"); +}); + +test("returns Vite BASE_URL for PUBLIC_URL", () => { + expect(getProcessEnv("PUBLIC_URL", { env: {} }, { BASE_URL: "/ui/" })).toBe( + "/ui/", + ); +}); + +test("returns undefined when env key does not exist", () => { + expect(getProcessEnv("PUBLIC_URL", { env: {} }, {})).toBeUndefined(); }); diff --git a/ui/src/utils/environment.ts b/ui/src/utils/environment.ts index f4c4eb18dac..1b2f0ee526a 100644 --- a/ui/src/utils/environment.ts +++ b/ui/src/utils/environment.ts @@ -2,6 +2,8 @@ type ProcessLike = { env?: Record; }; +type ViteEnvLike = Record; + const getDefaultProcess = (): ProcessLike | undefined => { if (typeof process === "undefined") { return undefined; @@ -9,18 +11,58 @@ const getDefaultProcess = (): ProcessLike | undefined => { return process; }; +const getDefaultViteEnv = (): ViteEnvLike | undefined => { + if (typeof import.meta === "undefined" || !import.meta.env) { + return undefined; + } + + return import.meta.env as ViteEnvLike; +}; + +const getStringEnvValue = ( + envValue: string | boolean | undefined, +): string | undefined => { + if (typeof envValue !== "string") { + return undefined; + } + + return envValue; +}; + +const getViteEnvValue = ( + envVarName: string, + viteEnv: ViteEnvLike | undefined, +): string | undefined => { + if (!viteEnv) { + return undefined; + } + + if (envVarName === "PUBLIC_URL") { + return ( + getStringEnvValue(viteEnv.BASE_URL) ?? + getStringEnvValue(viteEnv.VITE_PUBLIC_URL) + ); + } + + return ( + getStringEnvValue(viteEnv[`VITE_${envVarName}`]) ?? + getStringEnvValue(viteEnv[envVarName]) + ); +}; + export const getProcessEnv = ( envVarName: string, processLike: ProcessLike | undefined = getDefaultProcess(), + viteEnv: ViteEnvLike | undefined = getDefaultViteEnv(), ): string | undefined => { - if (!processLike?.env) { - return undefined; + const viteEnvValue = getViteEnvValue(envVarName, viteEnv); + if (viteEnvValue !== undefined) { + return viteEnvValue; } - const envValue = processLike.env[envVarName]; - if (typeof envValue !== "string") { + if (!processLike?.env) { return undefined; } - return envValue; + return getStringEnvValue(processLike.env[envVarName]); };