This commit is contained in:
Henry Dollman
2024-07-12 16:45:44 -04:00
parent 2436e04705
commit e7ff1172d5
12 changed files with 157 additions and 129 deletions

View File

@@ -17,6 +17,7 @@ import { Copy, Plus } from 'lucide-react'
import { useState, useRef, MutableRefObject, useEffect } from 'react'
import { useStore } from '@nanostores/react'
import { copyToClipboard } from '@/lib/utils'
import { SystemStats } from '@/types'
export function AddServerButton() {
const [open, setOpen] = useState(false)
@@ -53,14 +54,14 @@ export function AddServerButton() {
const formData = new FormData(e.target as HTMLFormElement)
const data = Object.fromEntries(formData) as Record<string, any>
data.stats = {
cpu: 0,
mem: 0,
memUsed: 0,
memPct: 0,
disk: 0,
diskUsed: 0,
diskPct: 0,
}
c: 0,
d: 0,
dp: 0,
du: 0,
m: 0,
mp: 0,
mu: 0,
} as SystemStats
try {
setOpen(false)
await pb.collection('systems').create(data)
@@ -97,7 +98,7 @@ export function AddServerButton() {
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="ip" className="text-right">
IP Address
Host / IP
</Label>
<Input id="ip" name="ip" className="col-span-3" required />
</div>

View File

@@ -62,16 +62,20 @@ export default function ({
<AreaChart
accessibilityLayer
data={chartData}
margin={{
top: 10,
}}
// reverseStackOrder={true}
>
<CartesianGrid vertical={false} />
<YAxis
domain={[0, max]}
tickCount={5}
domain={[0, (max: number) => Math.ceil(max)]}
// tickCount={5}
tickLine={false}
axisLine={false}
tickFormatter={(v) => `${v}%`}
unit={'%'}
tickFormatter={(x) => (x % 1 === 0 ? x : x.toFixed(1))}
/>
<XAxis
dataKey="time"
@@ -88,7 +92,7 @@ export default function ({
// console.log('itemSorter', item)
// return -item.value
// }}
content={<ChartTooltipContent indicator="line" />}
content={<ChartTooltipContent unit="%" indicator="line" />}
/>
{Object.keys(chartConfig).map((key) => (
<Area
@@ -96,7 +100,7 @@ export default function ({
// isAnimationActive={false}
animateNewValues={false}
dataKey={key}
type="natural"
type="bump"
fill={chartConfig[key].color}
fillOpacity={0.4}
stroke={chartConfig[key].color}

View File

@@ -63,6 +63,9 @@ export default function ({
<AreaChart
accessibilityLayer
data={chartData}
margin={{
top: 10,
}}
// reverseStackOrder={true}
>
@@ -70,6 +73,7 @@ export default function ({
<YAxis
domain={[0, max]}
tickCount={9}
allowDecimals={false}
tickLine={false}
axisLine={false}
tickFormatter={(v) => `${Math.ceil(v / 1024)} GiB`}
@@ -89,7 +93,7 @@ export default function ({
// console.log('itemSorter', item)
// return -item.value
// }}
content={<ChartTooltipContent indicator="line" />}
content={<ChartTooltipContent unit=" MiB" indicator="line" />}
/>
{Object.keys(chartConfig).map((key) => (
<Area

View File

@@ -7,7 +7,6 @@ import {
ChartTooltipContent,
} from '@/components/ui/chart'
import { formatShortDate, formatShortTime } from '@/lib/utils'
import { useEffect } from 'react'
import Spinner from '../spinner'
// for (const data of chartData) {
// data.month = formatDateShort(data.month)
@@ -33,14 +32,16 @@ export default function ({
return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart accessibilityLayer data={chartData}>
<AreaChart accessibilityLayer data={chartData} margin={{ top: 10 }}>
<CartesianGrid vertical={false} />
<YAxis
domain={[0, max]}
tickCount={5}
width={47}
// tickCount={5}
tickLine={false}
axisLine={false}
tickFormatter={(v) => `${v}%`}
unit={'%'}
// tickFormatter={(v) => `${v}%`}
/>
{/* todo: short time if first date is same day, otherwise short date */}
<XAxis
@@ -54,16 +55,12 @@ export default function ({
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={formatShortDate}
defaultValue={'%'}
indicator="line"
/>
<ChartTooltipContent unit="%" labelFormatter={formatShortDate} indicator="line" />
}
/>
<Area
dataKey="cpu"
type="natural"
type="monotone"
fill="var(--color-cpu)"
fillOpacity={0.4}
stroke="var(--color-cpu)"

View File

@@ -50,17 +50,20 @@ export default function ({
margin={{
left: 0,
right: 0,
top: 0,
top: 10,
bottom: 0,
}}
>
<CartesianGrid vertical={false} />
<YAxis
width={75}
domain={[0, diskSize]}
// ticks={ticks}
tickCount={9}
minTickGap={8}
tickLine={false}
axisLine={false}
tickFormatter={(v) => `${v} GiB`}
unit={' GiB'}
/>
{/* todo: short time if first date is same day, otherwise short date */}
<XAxis
@@ -73,11 +76,13 @@ export default function ({
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent labelFormatter={formatShortDate} indicator="line" />}
content={
<ChartTooltipContent unit=" GiB" labelFormatter={formatShortDate} indicator="line" />
}
/>
<Area
dataKey="diskUsed"
type="natural"
type="bump"
fill="var(--color-diskUsed)"
fillOpacity={0.4}
stroke="var(--color-diskUsed)"

View File

@@ -36,10 +36,7 @@ export default function ({
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 0,
top: 0,
bottom: 0,
top: 10,
}}
>
<CartesianGrid vertical={false} />
@@ -48,6 +45,7 @@ export default function ({
domain={[0, totalMem]}
tickCount={9}
tickLine={false}
allowDecimals={false}
axisLine={false}
tickFormatter={(v) => `${v} GiB`}
/>
@@ -62,11 +60,13 @@ export default function ({
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent labelFormatter={formatShortDate} indicator="line" />}
content={
<ChartTooltipContent unit=" GiB" labelFormatter={formatShortDate} indicator="line" />
}
/>
<Area
dataKey="memUsed"
type="natural"
type="bump"
fill="var(--color-memUsed)"
fillOpacity={0.4}
stroke="var(--color-memUsed)"

View File

@@ -85,10 +85,10 @@ export default function ServerDetail({ name }: { name: string }) {
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 })
cpuData.push({ time: created, cpu: stats.c })
maxCpu = Math.max(maxCpu, stats.c)
memData.push({ time: created, mem: stats.m, memUsed: stats.mu })
diskData.push({ time: created, disk: stats.d, diskUsed: stats.du })
}
setCpuChartData({
max: Math.ceil(maxCpu),
@@ -137,8 +137,8 @@ export default function ServerDetail({ name }: { name: string }) {
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
cpuData[container.n] = container.c
memData[container.n] = container.m
}
containerCpuData.push(cpuData)
containerMemData.push(memData)
@@ -157,7 +157,7 @@ export default function ServerDetail({ name }: { name: string }) {
<CpuIcon className="opacity-70" />
</CardTitle>
<CardDescription>
Average usage of the one minute preceding the recorded time
System-wide CPU utilization of the preceding one minute as a percentage
</CardDescription>
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
@@ -166,20 +166,22 @@ export default function ServerDetail({ name }: { name: string }) {
</Suspense>
</CardContent>
</Card>
<Card className="pb-2">
<CardHeader>
<CardTitle className="flex gap-2 justify-between">
<span>Docker CPU Usage</span>
<CpuIcon className="opacity-70" />
</CardTitle>{' '}
<CardDescription>CPU usage of docker containers</CardDescription>
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
<Suspense fallback={<Spinner />}>
<ContainerCpuChart chartData={containerCpuChartData} max={cpuChartData.max} />
</Suspense>
</CardContent>
</Card>
{containerCpuChartData.length > 0 && (
<Card className="pb-2">
<CardHeader>
<CardTitle className="flex gap-2 justify-between">
<span>Docker CPU Usage</span>
<CpuIcon className="opacity-70" />
</CardTitle>{' '}
<CardDescription>CPU utilization of docker containers</CardDescription>
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
<Suspense fallback={<Spinner />}>
<ContainerCpuChart chartData={containerCpuChartData} max={cpuChartData.max} />
</Suspense>
</CardContent>
</Card>
)}
<Card className="pb-2">
<CardHeader>
<CardTitle>Memory Usage</CardTitle>
@@ -191,25 +193,27 @@ export default function ServerDetail({ name }: { name: string }) {
</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>
{containerMemChartData.length > 0 && (
<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?.m && (
<ContainerMemChart
chartData={containerMemChartData}
max={server.stats.m * 1024}
/>
)}
</Suspense>
</CardContent>
</Card>
)}
<Card className="pb-2">
<CardHeader>
<CardTitle>Disk Usage</CardTitle>

View File

@@ -52,6 +52,8 @@ import {
Cpu,
MemoryStick,
HardDrive,
PauseIcon,
CopyIcon,
} from 'lucide-react'
import { useMemo, useState } from 'react'
import { $servers, pb, navigate } from '@/lib/stores'
@@ -109,7 +111,7 @@ export default function () {
<span className="flex gap-0.5 items-center text-base">
<span
className={cn(
'w-2.5 h-2.5 left-0 rounded-full',
'w-2 h-2 left-0 rounded-full',
info.row.original.active ? 'bg-green-500' : 'bg-red-500'
)}
style={{ marginBottom: '-1px' }}
@@ -120,24 +122,24 @@ export default function () {
onClick={() => copyToClipboard(info.getValue() as string)}
>
{info.getValue() as string}
<Copy className="h-3.5 w-3.5 opacity-70" />
<CopyIcon className="h-3 w-3" />
</Button>
</span>
),
header: ({ column }) => sortableHeader(column, 'Server', Server),
},
{
accessorKey: 'stats.cpu',
accessorKey: 'stats.c',
cell: CellFormatter,
header: ({ column }) => sortableHeader(column, 'CPU', Cpu),
},
{
accessorKey: 'stats.memPct',
accessorKey: 'stats.mp',
cell: CellFormatter,
header: ({ column }) => sortableHeader(column, 'Memory', MemoryStick),
},
{
accessorKey: 'stats.diskPct',
accessorKey: 'stats.dp',
cell: CellFormatter,
header: ({ column }) => sortableHeader(column, 'Disk', HardDrive),
},
@@ -169,8 +171,8 @@ export default function () {
<DropdownMenuItem onClick={() => console.log('pause server')}>
Pause
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(system.ip)}>
Copy IP address
<DropdownMenuItem onClick={() => copyToClipboard(system.ip)}>
Copy host
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem

View File

@@ -58,21 +58,30 @@
}
}
.recharts-tooltip-wrapper {
z-index: 1;
}
/* charts */
@layer base {
:root {
--chart-1: 12 76% 61%;
/* --chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--chart-5: 27 87% 67%; */
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
/*
.dark {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
} */
}

View File

@@ -52,26 +52,18 @@ const Layout = () => {
<>
<div className="container">
<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-[1.1em] fill-foreground" />
</a>
</TooltipTrigger>
<TooltipContent>
<p>Home</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<a
href="/"
aria-label="Home"
className={'p-2 pl-0 -mb-1'}
onClick={(e) => {
e.preventDefault()
navigate('/')
}}
>
<Logo className="h-[1.1em] fill-foreground" />
</a>
<div className={'flex gap-1 ml-auto'}>
<TooltipProvider delayDuration={300}>
<Tooltip>

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

@@ -9,13 +9,20 @@ export interface SystemRecord extends RecordModel {
}
export interface SystemStats {
cpu: number
disk: number
diskPct: number
diskUsed: number
mem: number
memPct: number
memUsed: number
/** cpu percent */
c: number
/** disk size (gb) */
d: number
/** disk percent */
dp: number
/** disk used (gb) */
du: number
/** total memory (gb) */
m: number
/** memory percent */
mp: number
/** memory used (gb) */
mu: number
}
export interface ContainerStatsRecord extends RecordModel {
@@ -24,10 +31,12 @@ export interface ContainerStatsRecord extends RecordModel {
}
interface ContainerStats {
name: string
cpu: number
mem: number
memPct: number
/** name */
n: string
/** cpu percent */
c: number
/** memory used (gb) */
m: number
}
export interface SystemStatsRecord extends RecordModel {

View File

@@ -16,18 +16,19 @@ type SystemData struct {
}
type SystemStats struct {
Cpu float64 `json:"cpu"`
Mem float64 `json:"mem"`
MemUsed float64 `json:"memUsed"`
MemPct float64 `json:"memPct"`
Disk float64 `json:"disk"`
DiskUsed float64 `json:"diskUsed"`
DiskPct float64 `json:"diskPct"`
Cpu float64 `json:"c"`
Mem float64 `json:"m"`
MemUsed float64 `json:"mu"`
MemPct float64 `json:"mp"`
MemBuf float64 `json:"mb"`
Disk float64 `json:"d"`
DiskUsed float64 `json:"du"`
DiskPct float64 `json:"dp"`
}
type ContainerStats struct {
Name string `json:"name"`
Cpu float64 `json:"cpu"`
Mem float64 `json:"mem"`
MemPct float64 `json:"memPct"`
Name string `json:"n"`
Cpu float64 `json:"c"`
Mem float64 `json:"m"`
// MemPct float64 `json:"mp"`
}