From 1754d33c5cf32b6fd2b61e3e0a090eab9f9a88c6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 02:40:27 +0000 Subject: [PATCH 1/5] Add dark mode toggle to UI Co-Authored-By: Francisco Javier Arceo --- ui/src/App.css | 5 ++++ ui/src/FeastUISansProviders.tsx | 34 +++++++++++++++++++-- ui/src/components/ThemeToggle.tsx | 29 ++++++++++++++++++ ui/src/contexts/ThemeContext.tsx | 50 +++++++++++++++++++++++++++++++ ui/src/index.css | 7 +++++ ui/src/pages/Layout.tsx | 4 +++ 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 ui/src/components/ThemeToggle.tsx create mode 100644 ui/src/contexts/ThemeContext.tsx diff --git a/ui/src/App.css b/ui/src/App.css index 4577c6f3334..37db5c86182 100644 --- a/ui/src/App.css +++ b/ui/src/App.css @@ -2,3 +2,8 @@ html { background: url("assets/feast-icon-white.svg") no-repeat bottom left; background-size: 20vh; } + +body.euiTheme--dark html { + background: url("assets/feast-icon-white.svg") no-repeat bottom left; + background-size: 20vh; +} diff --git a/ui/src/FeastUISansProviders.tsx b/ui/src/FeastUISansProviders.tsx index 0ab9d3479eb..f33ba91780d 100644 --- a/ui/src/FeastUISansProviders.tsx +++ b/ui/src/FeastUISansProviders.tsx @@ -1,10 +1,10 @@ import React from "react"; -import "@elastic/eui/dist/eui_theme_light.css"; import "./index.css"; import { Routes, Route } from "react-router-dom"; import { EuiProvider, EuiErrorBoundary } from "@elastic/eui"; +import { ThemeProvider, useTheme } from "./contexts/ThemeContext"; import ProjectOverviewPage from "./pages/ProjectOverviewPage"; import Layout from "./pages/Layout"; @@ -70,7 +70,37 @@ const FeastUISansProviders = ({ }; return ( - + + + + ); +}; + +const FeastUISansProvidersInner = ({ + basename, + projectListContext, + feastUIConfigs, +}: { + basename: string; + projectListContext: ProjectsListContextInterface; + feastUIConfigs?: FeastUIConfigs; +}) => { + const { colorMode } = useTheme(); + + React.useEffect(() => { + if (colorMode === "dark") { + import("@elastic/eui/dist/eui_theme_dark.css"); + } else { + import("@elastic/eui/dist/eui_theme_light.css"); + } + }, [colorMode]); + + return ( + { + const { colorMode, toggleColorMode } = useTheme(); + const buttonId = useGeneratedHtmlId({ prefix: "themeToggle" }); + + return ( + + + + ); +}; + +export default ThemeToggle; diff --git a/ui/src/contexts/ThemeContext.tsx b/ui/src/contexts/ThemeContext.tsx new file mode 100644 index 00000000000..d4b76868590 --- /dev/null +++ b/ui/src/contexts/ThemeContext.tsx @@ -0,0 +1,50 @@ +import React, { createContext, useState, useContext, useEffect } from "react"; + +type ThemeMode = "light" | "dark"; + +interface ThemeContextType { + colorMode: ThemeMode; + setColorMode: (mode: ThemeMode) => void; + toggleColorMode: () => void; +} + +const ThemeContext = createContext({ + colorMode: "light", + setColorMode: () => {}, + toggleColorMode: () => {}, +}); + +export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ + children +}) => { + const [colorMode, setColorMode] = useState(() => { + const savedTheme = localStorage.getItem("feast-theme"); + return (savedTheme === "dark" ? "dark" : "light") as ThemeMode; + }); + + useEffect(() => { + localStorage.setItem("feast-theme", colorMode); + + if (colorMode === "dark") { + document.body.classList.add("euiTheme--dark"); + } else { + document.body.classList.remove("euiTheme--dark"); + } + }, [colorMode]); + + const toggleColorMode = () => { + setColorMode(prevMode => (prevMode === "light" ? "dark" : "light")); + }; + + return ( + + {children} + + ); +}; + +export const useTheme = () => useContext(ThemeContext); + +export default ThemeContext; diff --git a/ui/src/index.css b/ui/src/index.css index 46ca3ba295e..d1d4cc20e2b 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -10,6 +10,13 @@ html { background-attachment: fixed; } +/* Add dark mode specific styles */ +body.euiTheme--dark html { + background: url("assets/feast-icon-grey.svg") no-repeat -6vh 56vh; + background-size: 50vh; + filter: brightness(0.7); /* Darken the background image for dark mode */ +} + body { margin: 0; font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", diff --git a/ui/src/pages/Layout.tsx b/ui/src/pages/Layout.tsx index 2aee2904aee..24251be4483 100644 --- a/ui/src/pages/Layout.tsx +++ b/ui/src/pages/Layout.tsx @@ -17,6 +17,7 @@ import { useLoadProjectsList } from "../contexts/ProjectListContext"; import ProjectSelector from "../components/ProjectSelector"; import Sidebar from "./Sidebar"; import FeastWordMark from "../graphics/FeastWordMark"; +import ThemeToggle from "../components/ThemeToggle"; const Layout = () => { // Registry Path Context has to be inside Layout @@ -48,6 +49,9 @@ const Layout = () => { + + + )} From 9c85a02240579f5912040a0de0d7b7fc0e262b4d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 02:50:25 +0000 Subject: [PATCH 2/5] Fix TypeScript errors with theme CSS imports Co-Authored-By: Francisco Javier Arceo --- ui/src/FeastUISansProviders.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ui/src/FeastUISansProviders.tsx b/ui/src/FeastUISansProviders.tsx index f33ba91780d..ba0afb4ff7f 100644 --- a/ui/src/FeastUISansProviders.tsx +++ b/ui/src/FeastUISansProviders.tsx @@ -1,5 +1,6 @@ import React from "react"; +import "@elastic/eui/dist/eui_theme_light.css"; import "./index.css"; import { Routes, Route } from "react-router-dom"; @@ -91,13 +92,6 @@ const FeastUISansProvidersInner = ({ }) => { const { colorMode } = useTheme(); - React.useEffect(() => { - if (colorMode === "dark") { - import("@elastic/eui/dist/eui_theme_dark.css"); - } else { - import("@elastic/eui/dist/eui_theme_light.css"); - } - }, [colorMode]); return ( From e9b332cd879f43600e7d5ed2c147f38af985b6f7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 02:55:45 +0000 Subject: [PATCH 3/5] Fix legend visibility in dark mode Co-Authored-By: Francisco Javier Arceo --- ui/src/components/RegistryVisualization.tsx | 25 ++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ui/src/components/RegistryVisualization.tsx b/ui/src/components/RegistryVisualization.tsx index 72777218b45..cfd371a03dc 100644 --- a/ui/src/components/RegistryVisualization.tsx +++ b/ui/src/components/RegistryVisualization.tsx @@ -19,6 +19,7 @@ import { EuiPanel, EuiTitle, EuiSpacer, EuiLoadingSpinner } from "@elastic/eui"; import { FEAST_FCO_TYPES } from "../parsers/types"; import { EntityRelation } from "../parsers/parseEntityRelationships"; import { feast } from "../protos"; +import { useTheme } from "../contexts/ThemeContext"; const edgeAnimationStyle = ` @keyframes dashdraw { @@ -369,6 +370,7 @@ const getLayoutedElements = ( }; }; const Legend = () => { + const { colorMode } = useTheme(); const types = [ { type: FEAST_FCO_TYPES.featureService, label: "Feature Service" }, { type: FEAST_FCO_TYPES.featureView, label: "Feature View" }, @@ -376,21 +378,34 @@ const Legend = () => { { type: FEAST_FCO_TYPES.dataSource, label: "Data Source" }, ]; + const isDarkMode = colorMode === "dark"; + const backgroundColor = isDarkMode ? "#1D1E24" : "white"; + const borderColor = isDarkMode ? "#343741" : "#ddd"; + const textColor = isDarkMode ? "#DFE5EF" : "#333"; + const boxShadow = isDarkMode + ? "0 2px 5px rgba(0,0,0,0.3)" + : "0 2px 5px rgba(0,0,0,0.1)"; + return (
-
+
Legend
{types.map((item) => ( @@ -414,7 +429,7 @@ const Legend = () => { > {getNodeIcon(item.type)}
-
{item.label}
+
{item.label}
))} From 732aeace6cc0425af3e77a9387492b049d94dae8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 02:59:15 +0000 Subject: [PATCH 4/5] Fix lineage filtering to show complete dependency chains Co-Authored-By: Francisco Javier Arceo --- ui/src/components/RegistryVisualization.tsx | 36 +++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/ui/src/components/RegistryVisualization.tsx b/ui/src/components/RegistryVisualization.tsx index cfd371a03dc..6be906ceeda 100644 --- a/ui/src/components/RegistryVisualization.tsx +++ b/ui/src/components/RegistryVisualization.tsx @@ -615,13 +615,37 @@ const RegistryVisualization: React.FC = ({ // Filter relationships based on filterNode if provided if (filterNode) { + const connectedNodes = new Set(); + + const filterNodeId = `${getNodePrefix(filterNode.type)}-${filterNode.name}`; + connectedNodes.add(filterNodeId); + + // Function to recursively find all connected nodes + const findConnectedNodes = (nodeId: string, isDownstream: boolean) => { + relationshipsToShow.forEach((rel) => { + const sourceId = `${getNodePrefix(rel.source.type)}-${rel.source.name}`; + const targetId = `${getNodePrefix(rel.target.type)}-${rel.target.name}`; + + if (isDownstream && sourceId === nodeId && !connectedNodes.has(targetId)) { + connectedNodes.add(targetId); + findConnectedNodes(targetId, isDownstream); + } + + if (!isDownstream && targetId === nodeId && !connectedNodes.has(sourceId)) { + connectedNodes.add(sourceId); + findConnectedNodes(sourceId, isDownstream); + } + }); + }; + + findConnectedNodes(filterNodeId, true); + + findConnectedNodes(filterNodeId, false); + relationshipsToShow = relationshipsToShow.filter((rel) => { - return ( - (rel.source.type === filterNode.type && - rel.source.name === filterNode.name) || - (rel.target.type === filterNode.type && - rel.target.name === filterNode.name) - ); + const sourceId = `${getNodePrefix(rel.source.type)}-${rel.source.name}`; + const targetId = `${getNodePrefix(rel.target.type)}-${rel.target.name}`; + return connectedNodes.has(sourceId) && connectedNodes.has(targetId); }); } From 4cacc7f3c5db4b24173d45b92d1d958f421d77e5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 03:07:25 +0000 Subject: [PATCH 5/5] Format code with prettier Co-Authored-By: Francisco Javier Arceo --- ui/src/FeastUISansProviders.tsx | 9 ++--- ui/src/components/RegistryVisualization.tsx | 44 +++++++++++++-------- ui/src/components/ThemeToggle.tsx | 8 +--- ui/src/contexts/ThemeContext.tsx | 12 +++--- 4 files changed, 38 insertions(+), 35 deletions(-) diff --git a/ui/src/FeastUISansProviders.tsx b/ui/src/FeastUISansProviders.tsx index ba0afb4ff7f..64bf5fa46b1 100644 --- a/ui/src/FeastUISansProviders.tsx +++ b/ui/src/FeastUISansProviders.tsx @@ -72,10 +72,10 @@ const FeastUISansProviders = ({ return ( - ); @@ -91,7 +91,6 @@ const FeastUISansProvidersInner = ({ feastUIConfigs?: FeastUIConfigs; }) => { const { colorMode } = useTheme(); - return ( diff --git a/ui/src/components/RegistryVisualization.tsx b/ui/src/components/RegistryVisualization.tsx index 6be906ceeda..727c6cb967b 100644 --- a/ui/src/components/RegistryVisualization.tsx +++ b/ui/src/components/RegistryVisualization.tsx @@ -382,8 +382,8 @@ const Legend = () => { const backgroundColor = isDarkMode ? "#1D1E24" : "white"; const borderColor = isDarkMode ? "#343741" : "#ddd"; const textColor = isDarkMode ? "#DFE5EF" : "#333"; - const boxShadow = isDarkMode - ? "0 2px 5px rgba(0,0,0,0.3)" + const boxShadow = isDarkMode + ? "0 2px 5px rgba(0,0,0,0.3)" : "0 2px 5px rgba(0,0,0,0.1)"; return ( @@ -400,12 +400,14 @@ const Legend = () => { boxShadow: boxShadow, }} > -
+
Legend
{types.map((item) => ( @@ -616,32 +618,40 @@ const RegistryVisualization: React.FC = ({ // Filter relationships based on filterNode if provided if (filterNode) { const connectedNodes = new Set(); - + const filterNodeId = `${getNodePrefix(filterNode.type)}-${filterNode.name}`; connectedNodes.add(filterNodeId); - + // Function to recursively find all connected nodes const findConnectedNodes = (nodeId: string, isDownstream: boolean) => { relationshipsToShow.forEach((rel) => { const sourceId = `${getNodePrefix(rel.source.type)}-${rel.source.name}`; const targetId = `${getNodePrefix(rel.target.type)}-${rel.target.name}`; - - if (isDownstream && sourceId === nodeId && !connectedNodes.has(targetId)) { + + if ( + isDownstream && + sourceId === nodeId && + !connectedNodes.has(targetId) + ) { connectedNodes.add(targetId); findConnectedNodes(targetId, isDownstream); } - - if (!isDownstream && targetId === nodeId && !connectedNodes.has(sourceId)) { + + if ( + !isDownstream && + targetId === nodeId && + !connectedNodes.has(sourceId) + ) { connectedNodes.add(sourceId); findConnectedNodes(sourceId, isDownstream); } }); }; - + findConnectedNodes(filterNodeId, true); - + findConnectedNodes(filterNodeId, false); - + relationshipsToShow = relationshipsToShow.filter((rel) => { const sourceId = `${getNodePrefix(rel.source.type)}-${rel.source.name}`; const targetId = `${getNodePrefix(rel.target.type)}-${rel.target.name}`; diff --git a/ui/src/components/ThemeToggle.tsx b/ui/src/components/ThemeToggle.tsx index 52be1113afb..ed9ccfe919e 100644 --- a/ui/src/components/ThemeToggle.tsx +++ b/ui/src/components/ThemeToggle.tsx @@ -1,15 +1,11 @@ import React from "react"; -import { - EuiButtonIcon, - EuiToolTip, - useGeneratedHtmlId -} from "@elastic/eui"; +import { EuiButtonIcon, EuiToolTip, useGeneratedHtmlId } from "@elastic/eui"; import { useTheme } from "../contexts/ThemeContext"; const ThemeToggle: React.FC = () => { const { colorMode, toggleColorMode } = useTheme(); const buttonId = useGeneratedHtmlId({ prefix: "themeToggle" }); - + return ( ({ toggleColorMode: () => {}, }); -export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ - children +export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ + children, }) => { const [colorMode, setColorMode] = useState(() => { const savedTheme = localStorage.getItem("feast-theme"); @@ -24,7 +24,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ useEffect(() => { localStorage.setItem("feast-theme", colorMode); - + if (colorMode === "dark") { document.body.classList.add("euiTheme--dark"); } else { @@ -33,13 +33,11 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ }, [colorMode]); const toggleColorMode = () => { - setColorMode(prevMode => (prevMode === "light" ? "dark" : "light")); + setColorMode((prevMode) => (prevMode === "light" ? "dark" : "light")); }; return ( - + {children} );