diff --git a/components/log-viewer-webui/client/src/AntdApp.tsx b/components/log-viewer-webui/client/src/AntdApp.tsx index 5435bae9d9..28de5c877f 100644 --- a/components/log-viewer-webui/client/src/AntdApp.tsx +++ b/components/log-viewer-webui/client/src/AntdApp.tsx @@ -1,6 +1,9 @@ import {RouterProvider} from "react-router"; +import {ConfigProvider} from "antd"; + import router from "./router"; +import THEME_CONFIG, {useThemeStore} from "./theme"; import "@ant-design/v5-patch-for-react-19"; @@ -11,7 +14,18 @@ import "@ant-design/v5-patch-for-react-19"; * @return */ const AntApp = () => { - return ; + const {algorithm} = useThemeStore(); + + return ( + + + + ); }; export default AntApp; diff --git a/components/log-viewer-webui/client/src/components/Layout/MainLayout.module.css b/components/log-viewer-webui/client/src/components/Layout/MainLayout.module.css index 784310750a..05a29739fa 100644 --- a/components/log-viewer-webui/client/src/components/Layout/MainLayout.module.css +++ b/components/log-viewer-webui/client/src/components/Layout/MainLayout.module.css @@ -1,5 +1,6 @@ .mainLayout { - min-height: 100vh; + height: 100vh; + overflow: hidden; } .siderLogoContainer { @@ -9,8 +10,20 @@ height: 64px; } +.siderContainer { + flex-direction: column; + display: flex; + justify-content: space-between; + height: 100%; +} + .siderLogo { max-width: 100%; max-height: 100%; - height: 32px; + padding-inline: 10%; + padding-block: 16px; +} + +.siderTopMenu { + flex-grow: 1; } diff --git a/components/log-viewer-webui/client/src/components/Layout/MainLayout.tsx b/components/log-viewer-webui/client/src/components/Layout/MainLayout.tsx index b3ef4f89c5..26778e0b5b 100644 --- a/components/log-viewer-webui/client/src/components/Layout/MainLayout.tsx +++ b/components/log-viewer-webui/client/src/components/Layout/MainLayout.tsx @@ -1,7 +1,7 @@ -import {useState} from "react"; import { Link, Outlet, + useLocation, } from "react-router"; import { @@ -15,45 +15,57 @@ import { } from "antd"; import styles from "./MainLayout.module.css"; +import ThemeToggleMenuItem from "./ThemeToggleMenuItem"; const {Sider} = Layout; type MenuItem = Required["items"][number]; -const SIDEBAR_MENU_ITEMS: MenuItem[] = [ +const sidebarTopMenuItems: MenuItem[] = [ + {type: "divider"}, {label: Ingest, key: "/ingest", icon: }, {label: Search, key: "/search", icon: }, ]; +const sidebarBottomMenuItems = [ + , +]; + /** * The main layout of web ui. * * @return */ const MainLayout = () => { - const [collapsed, setCollapsed] = useState(false); + const {pathname} = useLocation(); return ( { - setCollapsed(value); - }} + width={"200"} > -
- {"CLP +
+
+ {"CLP +
+ + + {sidebarBottomMenuItems} +
- diff --git a/components/log-viewer-webui/client/src/components/Layout/ThemeToggleMenuItem.tsx b/components/log-viewer-webui/client/src/components/Layout/ThemeToggleMenuItem.tsx new file mode 100644 index 0000000000..07b64f7760 --- /dev/null +++ b/components/log-viewer-webui/client/src/components/Layout/ThemeToggleMenuItem.tsx @@ -0,0 +1,66 @@ +import {useCallback} from "react"; + +import { + FormatPainterOutlined, + MoonOutlined, + SunOutlined, +} from "@ant-design/icons"; +import {Menu} from "antd"; + +import { + THEME_MODE, + useThemeStore, +} from "../../theme"; + + +/** + * Maps theme mode to icon. + */ +const THEME_MODE_TO_ICON: Record = Object.freeze({ + [THEME_MODE.SYSTEM]: , + [THEME_MODE.DARK]: , + [THEME_MODE.LIGHT]: , +}); + +/** + * Maps theme mode to label. + */ +const THEME_MODE_TO_LABEL: Record = Object.freeze({ + [THEME_MODE.SYSTEM]: "System", + [THEME_MODE.DARK]: "Dark", + [THEME_MODE.LIGHT]: "Light", +}); + +const {Item} = Menu; + +/** + * Displays a theme toggle menu item. + * + * @return + */ +const ThemeToggleMenuItem = () => { + const mode = useThemeStore((state) => state.mode); + + const handleClick = useCallback(() => { + const {setMode} = useThemeStore.getState(); + if (mode === THEME_MODE.SYSTEM) { + setMode(THEME_MODE.DARK); + } else if (mode === THEME_MODE.DARK) { + setMode(THEME_MODE.LIGHT); + } else { + setMode(THEME_MODE.SYSTEM); + } + }, [mode]); + + return ( + + {THEME_MODE_TO_LABEL[mode]} + + ); +}; + + +export default ThemeToggleMenuItem; diff --git a/components/log-viewer-webui/client/src/theme/index.tsx b/components/log-viewer-webui/client/src/theme/index.tsx new file mode 100644 index 0000000000..18fa69f625 --- /dev/null +++ b/components/log-viewer-webui/client/src/theme/index.tsx @@ -0,0 +1,25 @@ +import type {ThemeConfig} from "antd/es/config-provider/context"; + +import useThemeStore from "./themeStore"; +import {THEME_MODE} from "./typings"; + + +/** + * Ant Design theme configuration. + */ +const THEME_CONFIG: ThemeConfig = Object.freeze({ + token: { + fontFamily: "'Inter', sans-serif", + colorPrimary: "#2a8efa", + borderRadius: 3, + }, + cssVar: true, + hashed: false, +}); + + +export { + THEME_MODE, + useThemeStore, +}; +export default THEME_CONFIG; diff --git a/components/log-viewer-webui/client/src/theme/themeStore.ts b/components/log-viewer-webui/client/src/theme/themeStore.ts new file mode 100644 index 0000000000..ad28f82b05 --- /dev/null +++ b/components/log-viewer-webui/client/src/theme/themeStore.ts @@ -0,0 +1,56 @@ +import type {MappingAlgorithm} from "antd/es/theme/interface"; +import {create} from "zustand"; +import {persist} from "zustand/middleware"; + +import {THEME_MODE} from "./typings"; +import { + getAlgorithm, + getSystemAlgorithm, +} from "./utils"; + + +interface ThemeValues { + algorithm: MappingAlgorithm; + mode: THEME_MODE; +} + +interface ThemeActions { + setMode: (mode: THEME_MODE) => void; +} + +type ThemeState = ThemeValues & ThemeActions; + +const THEME_DEFAULT: ThemeValues = { + algorithm: getSystemAlgorithm(), + mode: THEME_MODE.SYSTEM, +}; + +const useThemeStore = create( + persist( + (set) => ({ + ...THEME_DEFAULT, + setMode: (newMode: THEME_MODE) => { + set({mode: newMode}); + set({algorithm: getAlgorithm(newMode)}); + }, + }), + { + name: "theme-storage", + merge: (persistedState, currentState) => { + const safePersistedState = persistedState as Partial; + + return { + ...currentState, + ...safePersistedState, + ...( + safePersistedState.mode && + {algorithm: getAlgorithm(safePersistedState.mode)} + ), + }; + }, + } + ) +); + + +export default useThemeStore; diff --git a/components/log-viewer-webui/client/src/theme/typings.ts b/components/log-viewer-webui/client/src/theme/typings.ts new file mode 100644 index 0000000000..548dc6e959 --- /dev/null +++ b/components/log-viewer-webui/client/src/theme/typings.ts @@ -0,0 +1,8 @@ +enum THEME_MODE { + SYSTEM = "system", + DARK = "dark", + LIGHT = "light", +} + + +export {THEME_MODE}; diff --git a/components/log-viewer-webui/client/src/theme/utils.ts b/components/log-viewer-webui/client/src/theme/utils.ts new file mode 100644 index 0000000000..0f6215b6ed --- /dev/null +++ b/components/log-viewer-webui/client/src/theme/utils.ts @@ -0,0 +1,37 @@ +import {theme} from "antd"; + +import {THEME_MODE} from "./typings"; + + +/** + * Detects Antd theme algorithm from system theme. + * + * @return The system theme algorithm. + */ +const getSystemAlgorithm = () => { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? + theme.darkAlgorithm : + theme.defaultAlgorithm; +}; + +/** + * Detects Antd theme algorithm for the given theme mode. + * + * @param mode + * @return The theme algorithm. + */ +const getAlgorithm = (mode: THEME_MODE) => { + if (mode === THEME_MODE.SYSTEM) { + return getSystemAlgorithm(); + } else if (mode === THEME_MODE.DARK) { + return theme.darkAlgorithm; + } + + return theme.defaultAlgorithm; +}; + + +export { + getAlgorithm, + getSystemAlgorithm, +};