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..64bf5fa46b1 100644 --- a/ui/src/FeastUISansProviders.tsx +++ b/ui/src/FeastUISansProviders.tsx @@ -5,6 +5,7 @@ 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 +71,29 @@ const FeastUISansProviders = ({ }; return ( - + + + + ); +}; + +const FeastUISansProvidersInner = ({ + basename, + projectListContext, + feastUIConfigs, +}: { + basename: string; + projectListContext: ProjectsListContextInterface; + feastUIConfigs?: FeastUIConfigs; +}) => { + const { colorMode } = useTheme(); + + return ( + { + const { colorMode } = useTheme(); const types = [ { type: FEAST_FCO_TYPES.featureService, label: "Feature Service" }, { type: FEAST_FCO_TYPES.featureView, label: "Feature View" }, @@ -376,21 +378,36 @@ 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 +431,7 @@ const Legend = () => { > {getNodeIcon(item.type)}
-
{item.label}
+
{item.label}
))} @@ -600,13 +617,45 @@ 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); }); } diff --git a/ui/src/components/ThemeToggle.tsx b/ui/src/components/ThemeToggle.tsx new file mode 100644 index 00000000000..ed9ccfe919e --- /dev/null +++ b/ui/src/components/ThemeToggle.tsx @@ -0,0 +1,25 @@ +import React from "react"; +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 ( + + + + ); +}; + +export default ThemeToggle; diff --git a/ui/src/contexts/ThemeContext.tsx b/ui/src/contexts/ThemeContext.tsx new file mode 100644 index 00000000000..a47364b3fa0 --- /dev/null +++ b/ui/src/contexts/ThemeContext.tsx @@ -0,0 +1,48 @@ +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 = () => { + + + )}