Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions ui/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
25 changes: 24 additions & 1 deletion ui/src/FeastUISansProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -70,7 +71,29 @@ const FeastUISansProviders = ({
};

return (
<EuiProvider colorMode="light">
<ThemeProvider>
<FeastUISansProvidersInner
basename={basename}
projectListContext={projectListContext}
feastUIConfigs={feastUIConfigs}
/>
</ThemeProvider>
);
};

const FeastUISansProvidersInner = ({
basename,
projectListContext,
feastUIConfigs,
}: {
basename: string;
projectListContext: ProjectsListContextInterface;
feastUIConfigs?: FeastUIConfigs;
}) => {
const { colorMode } = useTheme();

return (
<EuiProvider colorMode={colorMode}>
<EuiErrorBoundary>
<TabsRegistryContext.Provider
value={feastUIConfigs?.tabsRegistry || {}}
Expand Down
71 changes: 60 additions & 11 deletions ui/src/components/RegistryVisualization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -369,28 +370,44 @@ 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" },
{ type: FEAST_FCO_TYPES.entity, label: "Entity" },
{ 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 (
<div
style={{
position: "absolute",
left: 10,
top: 10,
background: "white",
border: "1px solid #ddd",
background: backgroundColor,
border: `1px solid ${borderColor}`,
borderRadius: 5,
padding: 10,
zIndex: 10,
boxShadow: "0 2px 5px rgba(0,0,0,0.1)",
boxShadow: boxShadow,
}}
>
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 5 }}>
<div
style={{
fontSize: 14,
fontWeight: 600,
marginBottom: 5,
color: textColor,
}}
>
Legend
</div>
{types.map((item) => (
Expand All @@ -414,7 +431,7 @@ const Legend = () => {
>
{getNodeIcon(item.type)}
</div>
<div style={{ fontSize: 12 }}>{item.label}</div>
<div style={{ fontSize: 12, color: textColor }}>{item.label}</div>
</div>
))}
</div>
Expand Down Expand Up @@ -600,13 +617,45 @@ const RegistryVisualization: React.FC<RegistryVisualizationProps> = ({

// Filter relationships based on filterNode if provided
if (filterNode) {
const connectedNodes = new Set<string>();

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

Expand Down
25 changes: 25 additions & 0 deletions ui/src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<EuiToolTip
position="right"
content={`Switch to ${colorMode === "light" ? "dark" : "light"} theme`}
>
<EuiButtonIcon
id={buttonId}
onClick={toggleColorMode}
iconType={colorMode === "light" ? "moon" : "sun"}
aria-label={`Switch to ${colorMode === "light" ? "dark" : "light"} theme`}
color="text"
/>
</EuiToolTip>
);
};

export default ThemeToggle;
48 changes: 48 additions & 0 deletions ui/src/contexts/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ThemeContextType>({
colorMode: "light",
setColorMode: () => {},
toggleColorMode: () => {},
});

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [colorMode, setColorMode] = useState<ThemeMode>(() => {
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 (
<ThemeContext.Provider value={{ colorMode, setColorMode, toggleColorMode }}>
{children}
</ThemeContext.Provider>
);
};

export const useTheme = () => useContext(ThemeContext);

export default ThemeContext;
7 changes: 7 additions & 0 deletions ui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions ui/src/pages/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,6 +49,9 @@ const Layout = () => {
<React.Fragment>
<EuiHorizontalRule margin="s" />
<Sidebar />
<EuiSpacer size="l" />
<EuiHorizontalRule margin="s" />
<ThemeToggle />
</React.Fragment>
)}
</EuiPageSidebar>
Expand Down
Loading