site updates

This commit is contained in:
Henry Dollman
2024-07-12 14:03:16 -04:00
parent 477428149a
commit 2ef3846199
7 changed files with 167 additions and 22 deletions

View File

@@ -26,8 +26,8 @@ export function AddServerButton() {
function copyDockerCompose(port: string) {
copyToClipboard(`services:
agent:
image: 'henrygd/ubik-agent'
container_name: 'ubik-agent'
image: 'henrygd/quoma-agent'
container_name: 'quoma-agent'
restart: unless-stopped
ports:
- '${port}:45876'

View File

@@ -0,0 +1,110 @@
'use client'
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { useMemo } from 'react'
import { formatShortDate, formatShortTime } from '@/lib/utils'
import Spinner from '../spinner'
export default function ({
chartData,
max,
}: {
chartData: Record<string, number | string>[]
max: number
}) {
console.log('max', max)
const chartConfig = useMemo(() => {
let config = {} as Record<
string,
{
label: string
color: string
}
>
const totalUsage = {} as Record<string, number>
for (let stats of chartData) {
for (let key in stats) {
if (key === 'time') {
continue
}
if (!(key in totalUsage)) {
totalUsage[key] = 0
}
// @ts-ignore
totalUsage[key] += stats[key]
}
}
let keys = Object.keys(totalUsage)
keys.sort((a, b) => (totalUsage[a] > totalUsage[b] ? -1 : 1))
const length = keys.length
for (let i = 0; i < length; i++) {
const key = keys[i]
const hue = ((i * 360) / length) % 360
config[key] = {
label: key,
color: `hsl(${hue}, 60%, 60%)`,
}
}
return config satisfies ChartConfig
}, [chartData])
if (!chartData.length) {
return <Spinner />
}
return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart
accessibilityLayer
data={chartData}
// reverseStackOrder={true}
>
<CartesianGrid vertical={false} />
<YAxis
domain={[0, max]}
tickCount={9}
tickLine={false}
axisLine={false}
tickFormatter={(v) => `${Math.ceil(v / 1024)} GiB`}
/>
<XAxis
dataKey="time"
tickLine={true}
axisLine={false}
tickMargin={8}
minTickGap={30}
tickFormatter={formatShortTime}
/>
<ChartTooltip
cursor={false}
labelFormatter={formatShortDate}
// itemSorter={(item) => {
// console.log('itemSorter', item)
// return -item.value
// }}
content={<ChartTooltipContent indicator="line" />}
/>
{Object.keys(chartConfig).map((key) => (
<Area
key={key}
// isAnimationActive={false}
animateNewValues={false}
dataKey={key}
type="natural"
fill={chartConfig[key].color}
fillOpacity={0.4}
stroke={chartConfig[key].color}
stackId="a"
/>
))}
</AreaChart>
</ChartContainer>
)
}

View File

@@ -1,7 +1,8 @@
export function Logo({ className }: { className?: string }) {
return (
<svg viewBox="0 0 421.6 140.2" className={className}>
<path d="M0 109.4V0h20.4v109.4a9.6 9.6 0 0 0 2.4 6.4 12.8 12.8 0 0 0 .7.7q3.1 3.1 7.1 3.1h58.6V0h20.2v119.6H89.2V140H30.6a33.4 33.4 0 0 1-9.6-1.3 30.7 30.7 0 0 1-2.7-1 30 30 0 0 1-7-4 26.8 26.8 0 0 1-2.7-2.3 28.1 28.1 0 0 1-5.5-8 33 33 0 0 1-.8-1.7 32.2 32.2 0 0 1-2.3-11 37.1 37.1 0 0 1 0-1.3ZM307 0h20.4v73.8l58.4-30.6q2 0 3-.8t1.5-2q.5-1.2.6-2.5l.1-2.3V17.8l20.4-10.2v28a29.5 29.5 0 0 1-3.8 14.4 34.6 34.6 0 0 1-.1.2 26 26 0 0 1-11 10.5 31.6 31.6 0 0 1-.5.3l-17.8 10.2 43.4 68.8h-23l-38.2-58.6-28 12.8q-2 2-3.5 3.8a7 7 0 0 0-1 1.5 5.3 5.3 0 0 0-.5 2.3V140H307V0ZM127.6 109.4v-89h20.2V0H214q5.8 0 11.3 2.3 5.5 2.3 9.8 6.3a30 30 0 0 1 6.5 9 34.4 34.4 0 0 1 .4.7 29.3 29.3 0 0 1 2.6 12.2 33.4 33.4 0 0 1 0 .1v12.6a23.3 23.3 0 0 1-1.2 7.4 26.1 26.1 0 0 1-.1.2q-1.3 3.8-3.4 7.1-2.1 3.3-4.9 6.1a48.8 48.8 0 0 1-3.3 3 39.5 39.5 0 0 1-2.3 1.8l2.4 2.4q7.6 4 12.7 11a28 28 0 0 1 3.7 6.7 22.5 22.5 0 0 1 1.4 7.9v12.6a29.4 29.4 0 0 1-2.4 11.8 28.6 28.6 0 0 1-.2.5 30.7 30.7 0 0 1-5.3 8 28.7 28.7 0 0 1-1.6 1.7q-4.3 4-9.7 6.3-5.4 2.3-11.2 2.3h-63.6q-5.8 0-10.9-2.3-5.1-2.3-8.9-6.3a28 28 0 0 1-5-7.3 33.7 33.7 0 0 1-1-2.4 33.3 33.3 0 0 1-2.2-10.6 38.6 38.6 0 0 1 0-1.7Zm20.2-89V112q0 6 5 7.3a11.8 11.8 0 0 0 2.8.3h63.6a9.2 9.2 0 0 0 5.3-1.7 13 13 0 0 0 1.7-1.4 11.2 11.2 0 0 0 2.1-2.7 9 9 0 0 0 1.1-4.4V96.8q0-2-1.6-3.9a11 11 0 0 0 0 0 57.9 57.9 0 0 0-2.2-2.4 70.3 70.3 0 0 0-1.4-1.5l-17.8-7.6-43.2 23v-23l56-30.4q1.6 0 3-2.3a12.4 12.4 0 0 0 .4-.6 18.3 18.3 0 0 0 .8-1.6q.7-1.8.8-3.1a5.2 5.2 0 0 0 0-.2V30.6a9.6 9.6 0 0 0-2.4-6.4 12.8 12.8 0 0 0-.7-.7q-3.1-3.1-7.1-3.1h-66.2ZM266.4 0H287v140.2h-20.6V0Z" />
// audiowide
<svg viewBox="0 0 370 82" className={className} fill="currentColor">
<path d="M271.8 36v35H259V36a7.5 7.5 0 0 0-.1-1.5q-.2-.8-.6-1.4a3.9 3.9 0 0 0-.6-.9q-1.1-1-3.1-1.3a9.2 9.2 0 0 0-.8 0h-16.5V71h-12.7V24.5a6.3 6.3 0 0 1 .4-2.2 6 6 0 0 1 0-.3q.5-1.1 1.4-2 .9-.8 2-1.3a6.4 6.4 0 0 1 2.6-.5H254a17.2 17.2 0 0 1 3 .3 22.8 22.8 0 0 1 2.6.6q3 1 5.8 3v-3.9h17.2a17.7 17.7 0 0 1 2.5.2 21.3 21.3 0 0 1 1.3.2 19.5 19.5 0 0 1 3.8 1.3 21.5 21.5 0 0 1 .2 0q2 .9 3.7 2.3a15 15 0 0 1 3.2 3.3 16.3 16.3 0 0 1 1.6 3 19.7 19.7 0 0 1 .6 1.6q.8 2.6.8 5.9v35h-12.7V36a8 8 0 0 0 0-1.5q-.3-1.4-1.2-2.3-1.2-1.3-3.8-1.3H271q.7 2.4.7 5Zm94.7-.3v17.8a17.3 17.3 0 0 1-.3 3 23.1 23.1 0 0 1-.6 2.7 17 17 0 0 1-3 5.7 15.9 15.9 0 0 1-3.7 3.3 19.6 19.6 0 0 1-1.7 1Q354 71 349.6 71a25.4 25.4 0 0 1-.6 0h-22.9a17.3 17.3 0 0 1-3-.3 23.1 23.1 0 0 1-2.7-.6 17 17 0 0 1-5.6-3 15.9 15.9 0 0 1-3.3-3.7 19.6 19.6 0 0 1-1.1-1.8q-1.7-3.1-1.8-7.5a25.4 25.4 0 0 1 0-.6 17.3 17.3 0 0 1 .3-3 23.4 23.4 0 0 1 .7-2.7q.9-3 3-5.7a15.9 15.9 0 0 1 3.6-3.3 19.6 19.6 0 0 1 1.8-1q3.1-1.7 7.6-1.8a25.4 25.4 0 0 1 .5 0h23V48h-23a6.8 6.8 0 0 0-1.6.2 4.6 4.6 0 0 0-2.4 1.4 5.5 5.5 0 0 0-1.4 3.3 7.3 7.3 0 0 0 0 .6 6 6 0 0 0 .2 1.7 4.4 4.4 0 0 0 1.4 2.2 5.8 5.8 0 0 0 4 1.4H349a6.9 6.9 0 0 0 1.7-.2 4.6 4.6 0 0 0 2.3-1.4 5.4 5.4 0 0 0 1.4-3.2 7.2 7.2 0 0 0 0-.7V35.7a6.5 6.5 0 0 0-.2-1.7 4.8 4.8 0 0 0-1.3-2.3q-1.5-1.4-3.9-1.4h-27.9v-12h28a17.3 17.3 0 0 1 3 .2 23.1 23.1 0 0 1 2.6.6 17 17 0 0 1 5.7 3 15.9 15.9 0 0 1 3.3 3.7 19.6 19.6 0 0 1 1 1.8q1.7 3.1 1.8 7.5a25.4 25.4 0 0 1 0 .6Zm-222-17.5v46.4a6.7 6.7 0 0 1-.2 2 6.1 6.1 0 0 1-.3.5 6.2 6.2 0 0 1-1.3 2q-.9 1-2 1.4a6.2 6.2 0 0 1-2.4.5h-28.8a23.3 23.3 0 0 1-3-.2 29 29 0 0 1-2-.3 23.6 23.6 0 0 1-5.2-1.8 27 27 0 0 1-4.7-2.8 30 30 0 0 1-.3-.2q-2.5-1.8-4.3-4.4a21.5 21.5 0 0 1-2.1-4 26.1 26.1 0 0 1-.8-2 22.4 22.4 0 0 1-.9-4.2 29.5 29.5 0 0 1-.2-3.6V18.2h12.7v29.3A12.5 12.5 0 0 0 99 50a10 10 0 0 0 .6 2 10.5 10.5 0 0 0 2 3 10 10 0 0 0 .3.4q1.5 1.4 3.4 2.1 2 .8 4.3.8h22.2v-40h12.7ZM75.7 29.3v13.4a32.8 32.8 0 0 1-.8 7.1 29.5 29.5 0 0 1-.5 2 29 29 0 0 1-3.1 7 26.9 26.9 0 0 1-.6 1 27 27 0 0 1-5.7 6 27 27 0 0 1-7.4 4.2l14.8 11.8H54L41.8 72H29.3a32.1 32.1 0 0 1-8.2-1 28.7 28.7 0 0 1-3.5-1.2q-5.3-2.2-9.3-6a28.2 28.2 0 0 1-6-9.4A29.6 29.6 0 0 1 0 45.2a35.1 35.1 0 0 1-.1-2.5V29.3A31.8 31.8 0 0 1 1 21a28.5 28.5 0 0 1 1.2-3.4 28.1 28.1 0 0 1 5.2-8.3 26.7 26.7 0 0 1 1-1 28 28 0 0 1 9.1-6 31.8 31.8 0 0 1 .1-.1A29.9 29.9 0 0 1 27.4.1a35 35 0 0 1 1.9-.1h17.2a31.6 31.6 0 0 1 8.2 1 28.2 28.2 0 0 1 3.4 1.2 28.1 28.1 0 0 1 9.3 6 27.5 27.5 0 0 1 6 9 31.3 31.3 0 0 1 0 .4 30.1 30.1 0 0 1 2.2 9.6 35.4 35.4 0 0 1 0 2.1ZM213.9 36v17.2a22.2 22.2 0 0 1-.2 3 17 17 0 0 1-.7 2.9 18.6 18.6 0 0 1-1.1 2.8 15.3 15.3 0 0 1-1 1.7 15 15 0 0 1-3.2 3.4 18.5 18.5 0 0 1-3.8 2.3 19.5 19.5 0 0 1-4 1.3 20.8 20.8 0 0 1-2.3.3 17 17 0 0 1-1.6.1h-22.9q-2.6 0-5.7-1-3.1-.9-5.8-3a16.3 16.3 0 0 1-3.4-3.7 20 20 0 0 1-1-1.8 14.7 14.7 0 0 1-1.4-3.8q-.3-1.7-.4-3.6a26.1 26.1 0 0 1 0-1V36q0-4.4 1.4-7.6a13.2 13.2 0 0 1 .4-.7 18.6 18.6 0 0 1 2.3-3.5 15.6 15.6 0 0 1 2.1-2q2.7-2.1 5.8-3a24.2 24.2 0 0 1 2.8-.7q1.5-.3 3-.3H196a22.7 22.7 0 0 1 3.8.3q2.1.4 3.9 1.2a13.5 13.5 0 0 1 .6.3 18 18 0 0 1 3.5 2.2 15 15 0 0 1 2 2.2q2.2 2.7 3.1 5.8a23.3 23.3 0 0 1 .7 3 17.3 17.3 0 0 1 .3 2.8Zm-151 6.7V29.3a20.4 20.4 0 0 0-.3-4 16.6 16.6 0 0 0-.8-2.8 15.4 15.4 0 0 0-2.5-4.2 14.3 14.3 0 0 0-.9-1 15 15 0 0 0-4.8-3.2 17.2 17.2 0 0 0-.4-.2 17.5 17.5 0 0 0-4.9-1.1 21 21 0 0 0-1.8-.1H29.3a20 20 0 0 0-4 .4 16.6 16.6 0 0 0-2.8.8q-3 1.2-5.2 3.4a14.8 14.8 0 0 0-3.3 5 17 17 0 0 0-.1.2 17.3 17.3 0 0 0-1 4.4 21.6 21.6 0 0 0-.2 2.4v13.4a20.4 20.4 0 0 0 .4 4 16.6 16.6 0 0 0 .8 2.8 15 15 0 0 0 2.7 4.5 14.3 14.3 0 0 0 .7.7 15.2 15.2 0 0 0 5 3.3 17.5 17.5 0 0 0 .2 0 17.4 17.4 0 0 0 4.7 1.2 21.3 21.3 0 0 0 2.1 0h17a20 20 0 0 0 4.2-.3A16.6 16.6 0 0 0 53 58q3.1-1.2 5.3-3.4a14.8 14.8 0 0 0 3.3-5 17 17 0 0 0 0-.2A17.3 17.3 0 0 0 63 45a21.6 21.6 0 0 0 0-2.4Zm138.3 10.5V36a8.4 8.4 0 0 0-.1-1.5l-.5-1.4a3.7 3.7 0 0 0-.7-1 4.2 4.2 0 0 0-1.8-1l-1.4-.3a8.9 8.9 0 0 0-.7 0h-22.8q-1.7 0-3 .6a4 4 0 0 0-.8.7q-1.2 1.1-1.3 3.2a8.7 8.7 0 0 0 0 .6v17.2q0 1.8.7 3a4 4 0 0 0 .6.8 4.2 4.2 0 0 0 1.7 1l1.5.3a8.9 8.9 0 0 0 .6 0H196a8 8 0 0 0 1.5-.1l1.4-.5a3.7 3.7 0 0 0 1-.7 4.2 4.2 0 0 0 1-1.7l.3-1.5a8.9 8.9 0 0 0 0-.6Z" />
</svg>
)
}

View File

@@ -8,7 +8,8 @@ import CpuChart from '../charts/cpu-chart'
import MemChart from '../charts/mem-chart'
import DiskChart from '../charts/disk-chart'
import ContainerCpuChart from '../charts/container-cpu-chart'
import { CpuIcon } from 'lucide-react'
import { CpuIcon, MemoryStickIcon } from 'lucide-react'
import ContainerMemChart from '../charts/container-mem-chart'
// const CpuChart = lazy(() => import('../cpu-chart'))
@@ -42,14 +43,17 @@ export default function ServerDetail({ name }: { name: string }) {
const [containerCpuChartData, setContainerCpuChartData] = useState(
[] as Record<string, number | string>[]
)
const [containerMemChartData, setContainerMemChartData] = useState(
[] as Record<string, number | string>[]
)
useEffect(() => {
document.title = name
return () => {
setContainerCpuChartData([])
setCpuChartData({} as { max: number; data: { time: string; cpu: number }[] })
setMemChartData([] as { time: string; mem: number; memUsed: number }[])
setDiskChartData([] as { time: string; disk: number; diskUsed: number }[])
setMemChartData([])
setDiskChartData([])
}
}, [name])
@@ -76,16 +80,18 @@ export default function ServerDetail({ name }: { name: string }) {
if (!serverStats.length) {
return
}
let maxCpu = 0
const cpuData = [] as { time: string; cpu: number }[]
const memData = [] as { time: string; mem: number; memUsed: number }[]
const diskData = [] as { time: string; disk: number; diskUsed: number }[]
for (let { created, stats } of serverStats) {
cpuData.push({ time: created, cpu: stats.cpu })
maxCpu = Math.max(maxCpu, stats.cpu)
memData.push({ time: created, mem: stats.mem, memUsed: stats.memUsed })
diskData.push({ time: created, disk: stats.disk, diskUsed: stats.diskUsed })
}
setCpuChartData({
max: Math.ceil(Math.max(...cpuData.map((d) => d.cpu))),
max: Math.ceil(maxCpu),
data: cpuData.reverse(),
})
setMemChartData(memData.reverse())
@@ -125,15 +131,20 @@ export default function ServerDetail({ name }: { name: string }) {
useEffect(() => {
console.log('containers', containers)
const containerCpuData = [] as Record<string, number | string>[]
const containerMemData = [] as Record<string, number | string>[]
for (let { created, stats } of containers) {
let obj = { time: created } as Record<string, number | string>
for (let { name, cpu } of stats) {
obj[name] = cpu
let cpuData = { time: created } as Record<string, number | string>
let memData = { time: created } as Record<string, number | string>
for (let container of stats) {
cpuData[container.name] = container.cpu
memData[container.name] = container.mem
}
containerCpuData.push(obj)
containerCpuData.push(cpuData)
containerMemData.push(memData)
}
setContainerCpuChartData(containerCpuData.reverse())
setContainerMemChartData(containerMemData.reverse())
}, [containers])
return (
@@ -175,9 +186,28 @@ export default function ServerDetail({ name }: { name: string }) {
<CardDescription>Precise usage at the recorded time</CardDescription>
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
{/* <Suspense fallback={<Spinner />}> */}
<MemChart chartData={memChartData} />
{/* </Suspense> */}
<Suspense fallback={<Spinner />}>
<MemChart chartData={memChartData} />
</Suspense>
</CardContent>
</Card>
<Card className="pb-2">
<CardHeader>
<CardTitle className="flex gap-2 justify-between">
<span>Docker Memory Usage</span>
<MemoryStickIcon className="opacity-70" />
</CardTitle>{' '}
<CardDescription>Memory usage of docker containers</CardDescription>
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
<Suspense fallback={<Spinner />}>
{server?.stats?.mem && (
<ContainerMemChart
chartData={containerMemChartData}
max={server.stats.mem * 1024}
/>
)}
</Suspense>
</CardContent>
</Card>
<Card className="pb-2">

View File

@@ -69,7 +69,7 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
}
return (
<div className="flex gap-2 items-center">
<span className="grow block bg-muted h-4 relative rounded-sm overflow-hidden">
<span className="grow min-w-10 block bg-muted h-4 relative rounded-sm overflow-hidden">
<span
className={cn('absolute inset-0 w-full h-full origin-left', `bg-${color}-500`)}
style={{ transform: `scalex(${val}%)` }}
@@ -109,7 +109,7 @@ export default function () {
<span className="flex gap-0.5 items-center text-base">
<span
className={cn(
'w-2.5 h-2.5 block left-0 rounded-full',
'w-2.5 h-2.5 left-0 rounded-full',
info.row.original.active ? 'bg-green-500' : 'bg-red-500'
)}
style={{ marginBottom: '-1px' }}
@@ -159,12 +159,15 @@ export default function () {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
{/* <DropdownMenuItem
onSelect={() => {
navigate(`/server/${system.name}`)
}}
>
View details
</DropdownMenuItem> */}
<DropdownMenuItem onClick={() => console.log('pause server')}>
Pause
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(system.ip)}>
Copy IP address

View File

@@ -10,7 +10,7 @@ import { buttonVariants } from './components/ui/button.tsx'
import { Github } from 'lucide-react'
import { useStore } from '@nanostores/react'
import { Toaster } from './components/ui/toaster.tsx'
import { Can, Logo } from './components/logo.tsx'
import { Logo } from './components/logo.tsx'
import {
TooltipProvider,
Tooltip,
@@ -51,19 +51,20 @@ const Layout = () => {
return (
<>
<div className="container">
<div className="flex items-center py-3.5 bg-card px-6 border bt-0 rounded-md my-5">
<div className="flex items-center h-16 bg-card px-6 border bt-0 rounded-md my-5">
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<a
href="/"
aria-label="Home"
className={'p-2 pl-0 -mb-1'}
onClick={(e) => {
e.preventDefault()
navigate('/')
}}
>
<Logo className="h-5 fill-foreground" />
<Logo className="h-[1.1em] fill-foreground" />
</a>
</TooltipTrigger>
<TooltipContent>

2
site/src/types.d.ts vendored
View File

@@ -27,7 +27,7 @@ interface ContainerStats {
name: string
cpu: number
mem: number
mempct: number
memPct: number
}
export interface SystemStatsRecord extends RecordModel {