From 56be7742e72828d39b2fb7a1767faefc168dd33d Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 22 Feb 2024 13:51:23 +0700 Subject: [PATCH] feat: revamp system monitor (#2097) * feat: revamp system monitor * feat: revamp system monitor ui * remove system monitor page * fix e2e test navigation * added click outside system monitor * update height content system monitor --- electron/tests/e2e/navigation.e2e.spec.ts | 8 +- web/app/page.tsx | 5 - web/constants/screens.ts | 1 - .../Layout/BottomBar/SystemItem/index.tsx | 30 --- .../SystemMonitor/TableActiveModel/index.tsx | 101 ++++++++++ .../Layout/BottomBar/SystemMonitor/index.tsx | 188 ++++++++++++++++++ web/containers/Layout/BottomBar/index.tsx | 146 +------------- web/containers/Layout/Ribbon/index.tsx | 11 - .../Layout/TopBar/CommandSearch/index.tsx | 6 +- web/containers/Layout/TopBar/index.tsx | 2 +- web/helpers/atoms/SystemBar.atom.ts | 1 + web/screens/SystemMonitor/index.tsx | 152 -------------- 12 files changed, 297 insertions(+), 354 deletions(-) delete mode 100644 web/containers/Layout/BottomBar/SystemItem/index.tsx create mode 100644 web/containers/Layout/BottomBar/SystemMonitor/TableActiveModel/index.tsx create mode 100644 web/containers/Layout/BottomBar/SystemMonitor/index.tsx delete mode 100644 web/screens/SystemMonitor/index.tsx diff --git a/electron/tests/e2e/navigation.e2e.spec.ts b/electron/tests/e2e/navigation.e2e.spec.ts index 66924ce78..b599a951c 100644 --- a/electron/tests/e2e/navigation.e2e.spec.ts +++ b/electron/tests/e2e/navigation.e2e.spec.ts @@ -2,17 +2,11 @@ import { expect } from '@playwright/test' import { page, test, TIMEOUT } from '../config/fixtures' test('renders left navigation panel', async () => { - const systemMonitorBtn = await page - .getByTestId('System Monitor') - .first() - .isEnabled({ - timeout: TIMEOUT, - }) const settingsBtn = await page .getByTestId('Thread') .first() .isEnabled({ timeout: TIMEOUT }) - expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0) + expect([settingsBtn].filter((e) => !e).length).toBe(0) // Chat section should be there await page.getByTestId('Local API Server').first().click({ timeout: TIMEOUT, diff --git a/web/app/page.tsx b/web/app/page.tsx index 20b15a235..92d654528 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -11,7 +11,6 @@ import ExploreModelsScreen from '@/screens/ExploreModels' import LocalServerScreen from '@/screens/LocalServer' import SettingsScreen from '@/screens/Settings' -import SystemMonitorScreen from '@/screens/SystemMonitor' export default function Page() { const { mainViewState } = useMainViewState() @@ -26,10 +25,6 @@ export default function Page() { children = break - case MainViewState.SystemMonitor: - children = - break - case MainViewState.LocalServer: children = break diff --git a/web/constants/screens.ts b/web/constants/screens.ts index 6a8adc185..74b441b17 100644 --- a/web/constants/screens.ts +++ b/web/constants/screens.ts @@ -3,6 +3,5 @@ export enum MainViewState { MyModels, Settings, Thread, - SystemMonitor, LocalServer, } diff --git a/web/containers/Layout/BottomBar/SystemItem/index.tsx b/web/containers/Layout/BottomBar/SystemItem/index.tsx deleted file mode 100644 index 6bb8b645d..000000000 --- a/web/containers/Layout/BottomBar/SystemItem/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { ReactNode } from 'react' - -import { twMerge } from 'tailwind-merge' - -type Props = { - name?: string - value: string | ReactNode - titleBold?: boolean -} - -export default function SystemItem({ name, value, titleBold }: Props) { - return ( -
-

- {name} -

- - {value} - -
- ) -} diff --git a/web/containers/Layout/BottomBar/SystemMonitor/TableActiveModel/index.tsx b/web/containers/Layout/BottomBar/SystemMonitor/TableActiveModel/index.tsx new file mode 100644 index 000000000..a73ec687f --- /dev/null +++ b/web/containers/Layout/BottomBar/SystemMonitor/TableActiveModel/index.tsx @@ -0,0 +1,101 @@ +import { Fragment } from 'react' + +import { + Tooltip, + TooltipTrigger, + Button, + TooltipPortal, + Badge, + TooltipContent, + TooltipArrow, +} from '@janhq/uikit' + +import { useAtom } from 'jotai' + +import { useActiveModel } from '@/hooks/useActiveModel' + +import { toGibibytes } from '@/utils/converter' + +import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' + +const Column = ['Name', 'Model ID', 'Size', 'Version', 'Action'] + +const TableActiveModel = () => { + const { activeModel, stateModel, stopModel } = useActiveModel() + const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom) + + return ( +
+
+ + + + {Column.map((col, i) => { + return ( + + ) + })} + + + {activeModel && ( + + + + + + + + + + + + )} +
+ {col} +
{activeModel.name}{activeModel.id} + + {toGibibytes(activeModel.metadata.size)} + + + v{activeModel.version} + + + + + + {serverEnabled && ( + + + + The API server is running, stop the model will + also stop the server + + + + + )} + +
+
+
+ ) +} + +export default TableActiveModel diff --git a/web/containers/Layout/BottomBar/SystemMonitor/index.tsx b/web/containers/Layout/BottomBar/SystemMonitor/index.tsx new file mode 100644 index 000000000..5b7853698 --- /dev/null +++ b/web/containers/Layout/BottomBar/SystemMonitor/index.tsx @@ -0,0 +1,188 @@ +import { Fragment, useEffect, useState } from 'react' + +import { Progress } from '@janhq/uikit' +import { useAtom, useAtomValue } from 'jotai' +import { MonitorIcon, XIcon, ChevronDown, ChevronUp } from 'lucide-react' + +import { twMerge } from 'tailwind-merge' + +import { useClickOutside } from '@/hooks/useClickOutside' +import useGetSystemResources from '@/hooks/useGetSystemResources' + +import { toGibibytes } from '@/utils/converter' + +import TableActiveModel from './TableActiveModel' + +import { + cpuUsageAtom, + gpusAtom, + ramUtilitizedAtom, + systemMonitorCollapseAtom, + totalRamAtom, + usedRamAtom, +} from '@/helpers/atoms/SystemBar.atom' + +const SystemMonitor = () => { + const totalRam = useAtomValue(totalRamAtom) + const usedRam = useAtomValue(usedRamAtom) + const cpuUsage = useAtomValue(cpuUsageAtom) + const gpus = useAtomValue(gpusAtom) + const [showFullScreen, setShowFullScreen] = useState(false) + const ramUtilitized = useAtomValue(ramUtilitizedAtom) + const [systemMonitorCollapse, setSystemMonitorCollapse] = useAtom( + systemMonitorCollapseAtom + ) + const [control, setControl] = useState(null) + const [elementExpand, setElementExpand] = useState( + null + ) + + const { watch, stopWatching } = useGetSystemResources() + useClickOutside( + () => { + setSystemMonitorCollapse(false) + setShowFullScreen(false) + }, + null, + [control, elementExpand] + ) + + useEffect(() => { + // Watch for resource update + watch() + + return () => { + stopWatching() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const calculateUtilization = () => { + let sum = 0 + const util = gpus.map((x) => { + return Number(x['utilization']) + }) + util.forEach((num) => { + sum += num + }) + return sum + } + + return ( + +
{ + setSystemMonitorCollapse(!systemMonitorCollapse) + setShowFullScreen(false) + }} + > + + System Monitor +
+ {systemMonitorCollapse && ( +
+
+
Running Models
+
+ {showFullScreen ? ( + setShowFullScreen(!showFullScreen)} + /> + ) : ( + setShowFullScreen(!showFullScreen)} + /> + )} + { + setSystemMonitorCollapse(false) + setShowFullScreen(false) + }} + /> +
+
+
+ +
+
+
CPU
+
+ + + {cpuUsage}% + +
+
+
+
+
Memory
+ + {toGibibytes(usedRam)} of {toGibibytes(totalRam)} used + +
+
+ + + {ramUtilitized}% + +
+
+ {gpus.length > 0 && ( +
+
GPU
+
+ + + {calculateUtilization()}% + +
+ {gpus.map((gpu, index) => ( +
+ + {gpu.name} + +
+ + {gpu.utilization}% + +
+ {gpu.vram} + MB VRAM +
+
+
+ ))} +
+ )} +
+
+
+ )} +
+ ) +} + +export default SystemMonitor diff --git a/web/containers/Layout/BottomBar/index.tsx b/web/containers/Layout/BottomBar/index.tsx index 5d6b7e79c..61e984c99 100644 --- a/web/containers/Layout/BottomBar/index.tsx +++ b/web/containers/Layout/BottomBar/index.tsx @@ -1,43 +1,21 @@ -import { useEffect } from 'react' - import { - Badge, - Button, Tooltip, TooltipArrow, TooltipContent, TooltipTrigger, } from '@janhq/uikit' -import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import { useAtomValue } from 'jotai' import { FaGithub, FaDiscord } from 'react-icons/fa' import DownloadingState from '@/containers/Layout/BottomBar/DownloadingState' -import SystemItem from '@/containers/Layout/BottomBar/SystemItem' import CommandListDownloadedModel from '@/containers/Layout/TopBar/CommandListDownloadedModel' import ProgressBar from '@/containers/ProgressBar' import { appDownloadProgress } from '@/containers/Providers/Jotai' -import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener' -import ShortCut from '@/containers/Shortcut' - -import { MainViewState } from '@/constants/screens' - -import { useActiveModel } from '@/hooks/useActiveModel' - -import { modelDownloadStateAtom } from '@/hooks/useDownloadState' -import useGetSystemResources from '@/hooks/useGetSystemResources' -import { useMainViewState } from '@/hooks/useMainViewState' - -import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' -import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' -import { - cpuUsageAtom, - gpusAtom, - ramUtilitizedAtom, -} from '@/helpers/atoms/SystemBar.atom' +import SystemMonitor from './SystemMonitor' const menuLinks = [ { @@ -53,39 +31,7 @@ const menuLinks = [ ] const BottomBar = () => { - const { activeModel, stateModel } = useActiveModel() - const { watch, stopWatching } = useGetSystemResources() const progress = useAtomValue(appDownloadProgress) - const downloadedModels = useAtomValue(downloadedModelsAtom) - const gpus = useAtomValue(gpusAtom) - const cpu = useAtomValue(cpuUsageAtom) - const ramUtilitized = useAtomValue(ramUtilitizedAtom) - - const { setMainViewState } = useMainViewState() - const downloadStates = useAtomValue(modelDownloadStateAtom) - const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom) - const [serverEnabled] = useAtom(serverEnabledAtom) - - const calculateUtilization = () => { - let sum = 0 - const util = gpus.map((x) => { - return Number(x['utilization']) - }) - util.forEach((num) => { - sum += num - }) - return sum - } - - useEffect(() => { - // Watch for resource update - watch() - - return () => { - stopWatching() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) return (
@@ -95,95 +41,11 @@ const BottomBar = () => { ) : null}
- - {!serverEnabled && ( - setShowSelectModelModal((show) => !show)} - > - My Models - - - )} - - {stateModel.state === 'start' && stateModel.loading && ( - - )} - {stateModel.state === 'stop' && stateModel.loading && ( - - )} - {!stateModel.loading && - downloadedModels.length !== 0 && - activeModel?.id && ( - - )} - {downloadedModels.length === 0 && - !stateModel.loading && - Object.values(downloadStates).length === 0 && ( - - )} -
-
- - -
- {gpus.length > 0 && ( - - -
- -
-
- {gpus.length > 1 && ( - - - {gpus.map((gpu, index) => ( -
-
- {gpu.name} - {gpu.vram}MB VRAM -
- {gpu.utilization}% -
- ))} -
- -
- )} -
- )} + + {/* VERSION is defined by webpack, please see next.config.js */} Jan v{VERSION ?? ''} diff --git a/web/containers/Layout/Ribbon/index.tsx b/web/containers/Layout/Ribbon/index.tsx index 8e7db604e..8a3c4a3a3 100644 --- a/web/containers/Layout/Ribbon/index.tsx +++ b/web/containers/Layout/Ribbon/index.tsx @@ -10,7 +10,6 @@ import { useAtom, useSetAtom } from 'jotai' import { MessageCircleIcon, SettingsIcon, - MonitorIcon, LayoutGridIcon, SquareCodeIcon, } from 'lucide-react' @@ -75,16 +74,6 @@ export default function RibbonNav() { ), state: MainViewState.LocalServer, }, - { - name: 'System Monitor', - icon: ( - - ), - state: MainViewState.SystemMonitor, - }, { name: 'Settings', icon: ( diff --git a/web/containers/Layout/TopBar/CommandSearch/index.tsx b/web/containers/Layout/TopBar/CommandSearch/index.tsx index ec73a7660..17887763e 100644 --- a/web/containers/Layout/TopBar/CommandSearch/index.tsx +++ b/web/containers/Layout/TopBar/CommandSearch/index.tsx @@ -38,11 +38,7 @@ const menus = [ icon: , state: MainViewState.Hub, }, - { - name: 'System Monitor', - icon: , - state: MainViewState.SystemMonitor, - }, + { name: 'Settings', icon: , diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index fdc4b4cdc..525cd97de 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -68,7 +68,7 @@ const TopBar = () => { } return ( -
+
{mainViewState !== MainViewState.Thread && mainViewState !== MainViewState.LocalServer ? (
diff --git a/web/helpers/atoms/SystemBar.atom.ts b/web/helpers/atoms/SystemBar.atom.ts index 3c9a48f79..5779ef822 100644 --- a/web/helpers/atoms/SystemBar.atom.ts +++ b/web/helpers/atoms/SystemBar.atom.ts @@ -9,3 +9,4 @@ export const ramUtilitizedAtom = atom(0) export const gpusAtom = atom[]>([]) export const nvidiaTotalVramAtom = atom(0) +export const systemMonitorCollapseAtom = atom(false) diff --git a/web/screens/SystemMonitor/index.tsx b/web/screens/SystemMonitor/index.tsx deleted file mode 100644 index 3bf8bb35e..000000000 --- a/web/screens/SystemMonitor/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { - ScrollArea, - Progress, - Badge, - Button, - Tooltip, - TooltipArrow, - TooltipContent, - TooltipPortal, - TooltipTrigger, -} from '@janhq/uikit' - -import { useAtom, useAtomValue } from 'jotai' - -import { useActiveModel } from '@/hooks/useActiveModel' - -import { toGibibytes } from '@/utils/converter' - -import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' -import { - cpuUsageAtom, - totalRamAtom, - usedRamAtom, -} from '@/helpers/atoms/SystemBar.atom' - -const Column = ['Name', 'Model ID', 'Size', 'Version', 'Action'] - -export default function SystemMonitorScreen() { - const totalRam = useAtomValue(totalRamAtom) - const usedRam = useAtomValue(usedRamAtom) - const cpuUsage = useAtomValue(cpuUsageAtom) - const { activeModel, stateModel, stopModel } = useActiveModel() - const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom) - - return ( -
- -
-
-
-
-

- cpu ({cpuUsage}%) -

- - {cpuUsage}% of 100% - -
-
- -
-
-
-
-

- ram ({Math.round((usedRam / totalRam) * 100)}%) -

- - {toGibibytes(usedRam)} of {toGibibytes(totalRam)} used - -
-
- -
-
-
- - {activeModel && ( -
-
-

- Actively Running Models -

-
-
- - - - {Column.map((col, i) => { - return ( - - ) - })} - - - - - - - - - - - -
- {col} -
- {activeModel.name} - {activeModel.id} - - {toGibibytes(activeModel.metadata.size)} - - - v{activeModel.version} - - - - - - {serverEnabled && ( - - - - The API server is running, stop the model will - also stop the server - - - - - )} - -
-
-
- )} -
-
-
- ) -}