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; +};