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
This commit is contained in:
Faisal Amir 2024-02-22 13:51:23 +07:00 committed by GitHub
parent a859534ab6
commit 56be7742e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 297 additions and 354 deletions

View File

@ -2,17 +2,11 @@ import { expect } from '@playwright/test'
import { page, test, TIMEOUT } from '../config/fixtures' import { page, test, TIMEOUT } from '../config/fixtures'
test('renders left navigation panel', async () => { test('renders left navigation panel', async () => {
const systemMonitorBtn = await page
.getByTestId('System Monitor')
.first()
.isEnabled({
timeout: TIMEOUT,
})
const settingsBtn = await page const settingsBtn = await page
.getByTestId('Thread') .getByTestId('Thread')
.first() .first()
.isEnabled({ timeout: TIMEOUT }) .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 // Chat section should be there
await page.getByTestId('Local API Server').first().click({ await page.getByTestId('Local API Server').first().click({
timeout: TIMEOUT, timeout: TIMEOUT,

View File

@ -11,7 +11,6 @@ import ExploreModelsScreen from '@/screens/ExploreModels'
import LocalServerScreen from '@/screens/LocalServer' import LocalServerScreen from '@/screens/LocalServer'
import SettingsScreen from '@/screens/Settings' import SettingsScreen from '@/screens/Settings'
import SystemMonitorScreen from '@/screens/SystemMonitor'
export default function Page() { export default function Page() {
const { mainViewState } = useMainViewState() const { mainViewState } = useMainViewState()
@ -26,10 +25,6 @@ export default function Page() {
children = <SettingsScreen /> children = <SettingsScreen />
break break
case MainViewState.SystemMonitor:
children = <SystemMonitorScreen />
break
case MainViewState.LocalServer: case MainViewState.LocalServer:
children = <LocalServerScreen /> children = <LocalServerScreen />
break break

View File

@ -3,6 +3,5 @@ export enum MainViewState {
MyModels, MyModels,
Settings, Settings,
Thread, Thread,
SystemMonitor,
LocalServer, LocalServer,
} }

View File

@ -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 (
<div className="flex items-center gap-x-1 text-xs">
<p
className={twMerge(
titleBold ? 'font-semibold' : 'text-muted-foreground'
)}
>
{name}
</p>
<span
className={twMerge(
titleBold ? 'text-muted-foreground' : 'font-semibold'
)}
>
{value}
</span>
</div>
)
}

View File

@ -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 (
<div className="flex-shrink-0 m-4 mr-0 w-2/3">
<div className="rounded-lg border border-border shadow-sm overflow-hidden">
<table className="w-full px-8">
<thead className="w-full border-b border-border bg-secondary">
<tr>
{Column.map((col, i) => {
return (
<th
key={i}
className="px-6 py-2 text-left font-normal last:text-center"
>
{col}
</th>
)
})}
</tr>
</thead>
{activeModel && (
<Fragment>
<tbody>
<tr>
<td className="px-6 py-2 font-bold">{activeModel.name}</td>
<td className="px-6 py-2 font-bold">{activeModel.id}</td>
<td className="px-6 py-2">
<Badge themes="secondary">
{toGibibytes(activeModel.metadata.size)}
</Badge>
</td>
<td className="px-6 py-2">
<Badge themes="secondary">v{activeModel.version}</Badge>
</td>
<td className="px-6 py-2 text-center">
<Tooltip>
<TooltipTrigger className="w-full">
<Button
block
themes={
stateModel.state === 'stop' ? 'danger' : 'primary'
}
className="w-16"
loading={stateModel.loading}
onClick={() => {
stopModel()
window.core?.api?.stopServer()
setServerEnabled(false)
}}
>
Stop
</Button>
</TooltipTrigger>
{serverEnabled && (
<TooltipPortal>
<TooltipContent side="top">
<span>
The API server is running, stop the model will
also stop the server
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
)}
</Tooltip>
</td>
</tr>
</tbody>
</Fragment>
)}
</table>
</div>
</div>
)
}
export default TableActiveModel

View File

@ -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<HTMLDivElement | null>(null)
const [elementExpand, setElementExpand] = useState<HTMLDivElement | null>(
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 (
<Fragment>
<div
ref={setControl}
className={twMerge(
'flex items-center gap-x-2 cursor-pointer p-2 rounded-md hover:bg-secondary',
systemMonitorCollapse && 'bg-secondary'
)}
onClick={() => {
setSystemMonitorCollapse(!systemMonitorCollapse)
setShowFullScreen(false)
}}
>
<MonitorIcon size={16} />
<span className="text-xs font-medium">System Monitor</span>
</div>
{systemMonitorCollapse && (
<div
ref={setElementExpand}
className={twMerge(
'fixed left-16 bottom-12 bg-white w-[calc(100%-64px)] z-50 border-t border-border flex flex-col flex-shrink-0',
showFullScreen && 'h-[calc(100%-48px)]'
)}
>
<div className="h-12 flex items-center border-b border-border px-4 justify-between flex-shrink-0">
<h6 className="font-medium">Running Models</h6>
<div className="flex items-center gap-x-2 unset-drag">
{showFullScreen ? (
<ChevronDown
size={20}
className="text-muted-foreground cursor-pointer"
onClick={() => setShowFullScreen(!showFullScreen)}
/>
) : (
<ChevronUp
size={20}
className="text-muted-foreground cursor-pointer"
onClick={() => setShowFullScreen(!showFullScreen)}
/>
)}
<XIcon
size={16}
className="text-muted-foreground cursor-pointer"
onClick={() => {
setSystemMonitorCollapse(false)
setShowFullScreen(false)
}}
/>
</div>
</div>
<div className="flex gap-4 h-full">
<TableActiveModel />
<div className="border-l border-border p-4 w-full">
<div className="mb-4 pb-4 border-b border-border">
<h6 className="font-bold">CPU</h6>
<div className="flex items-center gap-x-4">
<Progress value={cpuUsage} className="h-2" />
<span className="flex-shrink-0 text-muted-foreground">
{cpuUsage}%
</span>
</div>
</div>
<div className="mb-4 pb-4 border-b border-border">
<div className="flex items-center gap-2">
<h6 className="font-bold">Memory</h6>
<span className="text-xs text-muted-foreground">
{toGibibytes(usedRam)} of {toGibibytes(totalRam)} used
</span>
</div>
<div className="flex items-center gap-x-4">
<Progress
value={Math.round((usedRam / totalRam) * 100)}
className="h-2"
/>
<span className="flex-shrink-0 text-muted-foreground">
{ramUtilitized}%
</span>
</div>
</div>
{gpus.length > 0 && (
<div className="mb-4 pb-4 border-b border-border">
<h6 className="font-bold">GPU</h6>
<div className="flex items-center gap-x-4">
<Progress value={calculateUtilization()} className="h-2" />
<span className="flex-shrink-0 text-muted-foreground">
{calculateUtilization()}%
</span>
</div>
{gpus.map((gpu, index) => (
<div
key={index}
className="flex items-start justify-between mt-4 gap-4"
>
<span className="text-muted-foreground font-medium line-clamp-1 w-1/2">
{gpu.name}
</span>
<div className="flex gap-x-2">
<span className="font-semibold">
{gpu.utilization}%
</span>
<div>
<span className="font-semibold">{gpu.vram}</span>
<span>MB VRAM</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
</Fragment>
)
}
export default SystemMonitor

View File

@ -1,43 +1,21 @@
import { useEffect } from 'react'
import { import {
Badge,
Button,
Tooltip, Tooltip,
TooltipArrow, TooltipArrow,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from '@janhq/uikit' } from '@janhq/uikit'
import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { useAtomValue } from 'jotai'
import { FaGithub, FaDiscord } from 'react-icons/fa' import { FaGithub, FaDiscord } from 'react-icons/fa'
import DownloadingState from '@/containers/Layout/BottomBar/DownloadingState' import DownloadingState from '@/containers/Layout/BottomBar/DownloadingState'
import SystemItem from '@/containers/Layout/BottomBar/SystemItem'
import CommandListDownloadedModel from '@/containers/Layout/TopBar/CommandListDownloadedModel' import CommandListDownloadedModel from '@/containers/Layout/TopBar/CommandListDownloadedModel'
import ProgressBar from '@/containers/ProgressBar' import ProgressBar from '@/containers/ProgressBar'
import { appDownloadProgress } from '@/containers/Providers/Jotai' import { appDownloadProgress } from '@/containers/Providers/Jotai'
import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener' import SystemMonitor from './SystemMonitor'
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'
const menuLinks = [ const menuLinks = [
{ {
@ -53,39 +31,7 @@ const menuLinks = [
] ]
const BottomBar = () => { const BottomBar = () => {
const { activeModel, stateModel } = useActiveModel()
const { watch, stopWatching } = useGetSystemResources()
const progress = useAtomValue(appDownloadProgress) 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 ( return (
<div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3"> <div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3">
@ -95,95 +41,11 @@ const BottomBar = () => {
<ProgressBar total={100} used={progress} /> <ProgressBar total={100} used={progress} />
) : null} ) : null}
</div> </div>
{!serverEnabled && (
<Badge
themes="secondary"
className="cursor-pointer rounded-md border-none font-medium"
onClick={() => setShowSelectModelModal((show) => !show)}
>
My Models
<ShortCut menu="E" />
</Badge>
)}
{stateModel.state === 'start' && stateModel.loading && (
<SystemItem
titleBold
name="Starting"
value={stateModel.model || '-'}
/>
)}
{stateModel.state === 'stop' && stateModel.loading && (
<SystemItem
titleBold
name="Stopping"
value={stateModel.model || '-'}
/>
)}
{!stateModel.loading &&
downloadedModels.length !== 0 &&
activeModel?.id && (
<SystemItem
titleBold
name={'Active model'}
value={activeModel?.id}
/>
)}
{downloadedModels.length === 0 &&
!stateModel.loading &&
Object.values(downloadStates).length === 0 && (
<Button
size="sm"
themes="outline"
onClick={() => setMainViewState(MainViewState.Hub)}
>
Download your first model
</Button>
)}
<DownloadingState /> <DownloadingState />
</div> </div>
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
<div className="flex items-center gap-x-2"> <SystemMonitor />
<SystemItem name="CPU:" value={`${cpu}%`} />
<SystemItem name="Mem:" value={`${ramUtilitized}%`} />
</div>
{gpus.length > 0 && (
<Tooltip>
<TooltipTrigger>
<div className="flex items-center">
<SystemItem
name={`${gpus.length} GPU `}
value={`${calculateUtilization()}% `}
/>
</div>
</TooltipTrigger>
{gpus.length > 1 && (
<TooltipContent
side="top"
sideOffset={10}
className="min-w-[240px]"
>
<span>
{gpus.map((gpu, index) => (
<div
key={index}
className="flex items-center justify-between"
>
<div>
<span>{gpu.name}</span>
<span>{gpu.vram}MB VRAM</span>
</div>
<span>{gpu.utilization}%</span>
</div>
))}
</span>
<TooltipArrow />
</TooltipContent>
)}
</Tooltip>
)}
{/* VERSION is defined by webpack, please see next.config.js */} {/* VERSION is defined by webpack, please see next.config.js */}
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Jan v{VERSION ?? ''} Jan v{VERSION ?? ''}

View File

@ -10,7 +10,6 @@ import { useAtom, useSetAtom } from 'jotai'
import { import {
MessageCircleIcon, MessageCircleIcon,
SettingsIcon, SettingsIcon,
MonitorIcon,
LayoutGridIcon, LayoutGridIcon,
SquareCodeIcon, SquareCodeIcon,
} from 'lucide-react' } from 'lucide-react'
@ -75,16 +74,6 @@ export default function RibbonNav() {
), ),
state: MainViewState.LocalServer, state: MainViewState.LocalServer,
}, },
{
name: 'System Monitor',
icon: (
<MonitorIcon
size={20}
className="flex-shrink-0 text-muted-foreground"
/>
),
state: MainViewState.SystemMonitor,
},
{ {
name: 'Settings', name: 'Settings',
icon: ( icon: (

View File

@ -38,11 +38,7 @@ const menus = [
icon: <LayoutGridIcon size={16} className="mr-3 text-muted-foreground" />, icon: <LayoutGridIcon size={16} className="mr-3 text-muted-foreground" />,
state: MainViewState.Hub, state: MainViewState.Hub,
}, },
{
name: 'System Monitor',
icon: <MonitorIcon size={16} className="mr-3 text-muted-foreground" />,
state: MainViewState.SystemMonitor,
},
{ {
name: 'Settings', name: 'Settings',
icon: <SettingsIcon size={16} className="mr-3 text-muted-foreground" />, icon: <SettingsIcon size={16} className="mr-3 text-muted-foreground" />,

View File

@ -68,7 +68,7 @@ const TopBar = () => {
} }
return ( return (
<div className="fixed left-0 top-0 z-50 flex h-12 w-full border-b border-border bg-background/80 backdrop-blur-md"> <div className="fixed left-0 top-0 z-20 flex h-12 w-full border-b border-border bg-background/80 backdrop-blur-md">
{mainViewState !== MainViewState.Thread && {mainViewState !== MainViewState.Thread &&
mainViewState !== MainViewState.LocalServer ? ( mainViewState !== MainViewState.LocalServer ? (
<div className="relative left-16 flex w-[calc(100%-64px)] items-center justify-between space-x-4 pl-6 pr-2"> <div className="relative left-16 flex w-[calc(100%-64px)] items-center justify-between space-x-4 pl-6 pr-2">

View File

@ -9,3 +9,4 @@ export const ramUtilitizedAtom = atom<number>(0)
export const gpusAtom = atom<Record<string, never>[]>([]) export const gpusAtom = atom<Record<string, never>[]>([])
export const nvidiaTotalVramAtom = atom<number>(0) export const nvidiaTotalVramAtom = atom<number>(0)
export const systemMonitorCollapseAtom = atom<boolean>(false)

View File

@ -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 (
<div className="flex h-full w-full bg-background dark:bg-background">
<ScrollArea className="h-full w-full">
<div className="h-full p-8" data-testid="testid-system-monitor">
<div className="grid grid-cols-2 gap-8 lg:grid-cols-3">
<div className="rounded-xl border border-border p-4">
<div className="flex items-center justify-between">
<h4 className="text-base font-bold uppercase">
cpu ({cpuUsage}%)
</h4>
<span className="text-xs text-muted-foreground">
{cpuUsage}% of 100%
</span>
</div>
<div className="mt-2">
<Progress className="mb-2 h-10 rounded-md" value={cpuUsage} />
</div>
</div>
<div className="rounded-xl border border-border p-4">
<div className="flex items-center justify-between">
<h4 className="text-base font-bold uppercase">
ram ({Math.round((usedRam / totalRam) * 100)}%)
</h4>
<span className="text-xs text-muted-foreground">
{toGibibytes(usedRam)} of {toGibibytes(totalRam)} used
</span>
</div>
<div className="mt-2">
<Progress
className="mb-2 h-10 rounded-md"
value={Math.round((usedRam / totalRam) * 100)}
/>
</div>
</div>
</div>
{activeModel && (
<div className="mt-8 overflow-hidden rounded-xl border border-border shadow-sm">
<div className="px-6 py-5">
<h4 className="text-base font-medium">
Actively Running Models
</h4>
</div>
<div className="relative overflow-x-auto shadow-md">
<table className="w-full px-8">
<thead className="w-full border-b border-border bg-secondary">
<tr>
{Column.map((col, i) => {
return (
<th
key={i}
className="px-6 py-2 text-left font-normal last:text-center"
>
{col}
</th>
)
})}
</tr>
</thead>
<tbody>
<tr>
<td className="px-6 py-2 font-bold">
{activeModel.name}
</td>
<td className="px-6 py-2 font-bold">{activeModel.id}</td>
<td className="px-6 py-2">
<Badge themes="secondary">
{toGibibytes(activeModel.metadata.size)}
</Badge>
</td>
<td className="px-6 py-2">
<Badge themes="secondary">v{activeModel.version}</Badge>
</td>
<td className="px-6 py-2 text-center">
<Tooltip>
<TooltipTrigger className="w-full">
<Button
block
themes={
stateModel.state === 'stop'
? 'danger'
: 'primary'
}
className="w-16"
loading={stateModel.loading}
onClick={() => {
stopModel()
window.core?.api?.stopServer()
setServerEnabled(false)
}}
>
Stop
</Button>
</TooltipTrigger>
{serverEnabled && (
<TooltipPortal>
<TooltipContent side="top">
<span>
The API server is running, stop the model will
also stop the server
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
)}
</Tooltip>
</td>
</tr>
</tbody>
</table>
</div>
</div>
)}
</div>
</ScrollArea>
</div>
)
}