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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,10 +36,7 @@ export default function ({
accessibilityLayer accessibilityLayer
data={chartData} data={chartData}
margin={{ margin={{
left: 0, top: 10,
right: 0,
top: 0,
bottom: 0,
}} }}
> >
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
@@ -48,6 +45,7 @@ export default function ({
domain={[0, totalMem]} domain={[0, totalMem]}
tickCount={9} tickCount={9}
tickLine={false} tickLine={false}
allowDecimals={false}
axisLine={false} axisLine={false}
tickFormatter={(v) => `${v} GiB`} tickFormatter={(v) => `${v} GiB`}
/> />
@@ -62,11 +60,13 @@ export default function ({
/> />
<ChartTooltip <ChartTooltip
cursor={false} cursor={false}
content={<ChartTooltipContent labelFormatter={formatShortDate} indicator="line" />} content={
<ChartTooltipContent unit=" GiB" labelFormatter={formatShortDate} indicator="line" />
}
/> />
<Area <Area
dataKey="memUsed" dataKey="memUsed"
type="natural" type="bump"
fill="var(--color-memUsed)" fill="var(--color-memUsed)"
fillOpacity={0.4} fillOpacity={0.4}
stroke="var(--color-memUsed)" 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 memData = [] as { time: string; mem: number; memUsed: number }[]
const diskData = [] as { time: string; disk: number; diskUsed: number }[] const diskData = [] as { time: string; disk: number; diskUsed: number }[]
for (let { created, stats } of serverStats) { for (let { created, stats } of serverStats) {
cpuData.push({ time: created, cpu: stats.cpu }) cpuData.push({ time: created, cpu: stats.c })
maxCpu = Math.max(maxCpu, stats.cpu) maxCpu = Math.max(maxCpu, stats.c)
memData.push({ time: created, mem: stats.mem, memUsed: stats.memUsed }) memData.push({ time: created, mem: stats.m, memUsed: stats.mu })
diskData.push({ time: created, disk: stats.disk, diskUsed: stats.diskUsed }) diskData.push({ time: created, disk: stats.d, diskUsed: stats.du })
} }
setCpuChartData({ setCpuChartData({
max: Math.ceil(maxCpu), 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 cpuData = { time: created } as Record<string, number | string>
let memData = { time: created } as Record<string, number | string> let memData = { time: created } as Record<string, number | string>
for (let container of stats) { for (let container of stats) {
cpuData[container.name] = container.cpu cpuData[container.n] = container.c
memData[container.name] = container.mem memData[container.n] = container.m
} }
containerCpuData.push(cpuData) containerCpuData.push(cpuData)
containerMemData.push(memData) containerMemData.push(memData)
@@ -157,7 +157,7 @@ export default function ServerDetail({ name }: { name: string }) {
<CpuIcon className="opacity-70" /> <CpuIcon className="opacity-70" />
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Average usage of the one minute preceding the recorded time System-wide CPU utilization of the preceding one minute as a percentage
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}> <CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
@@ -166,20 +166,22 @@ export default function ServerDetail({ name }: { name: string }) {
</Suspense> </Suspense>
</CardContent> </CardContent>
</Card> </Card>
<Card className="pb-2"> {containerCpuChartData.length > 0 && (
<CardHeader> <Card className="pb-2">
<CardTitle className="flex gap-2 justify-between"> <CardHeader>
<span>Docker CPU Usage</span> <CardTitle className="flex gap-2 justify-between">
<CpuIcon className="opacity-70" /> <span>Docker CPU Usage</span>
</CardTitle>{' '} <CpuIcon className="opacity-70" />
<CardDescription>CPU usage of docker containers</CardDescription> </CardTitle>{' '}
</CardHeader> <CardDescription>CPU utilization of docker containers</CardDescription>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}> </CardHeader>
<Suspense fallback={<Spinner />}> <CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
<ContainerCpuChart chartData={containerCpuChartData} max={cpuChartData.max} /> <Suspense fallback={<Spinner />}>
</Suspense> <ContainerCpuChart chartData={containerCpuChartData} max={cpuChartData.max} />
</CardContent> </Suspense>
</Card> </CardContent>
</Card>
)}
<Card className="pb-2"> <Card className="pb-2">
<CardHeader> <CardHeader>
<CardTitle>Memory Usage</CardTitle> <CardTitle>Memory Usage</CardTitle>
@@ -191,25 +193,27 @@ export default function ServerDetail({ name }: { name: string }) {
</Suspense> </Suspense>
</CardContent> </CardContent>
</Card> </Card>
<Card className="pb-2"> {containerMemChartData.length > 0 && (
<CardHeader> <Card className="pb-2">
<CardTitle className="flex gap-2 justify-between"> <CardHeader>
<span>Docker Memory Usage</span> <CardTitle className="flex gap-2 justify-between">
<MemoryStickIcon className="opacity-70" /> <span>Docker Memory Usage</span>
</CardTitle>{' '} <MemoryStickIcon className="opacity-70" />
<CardDescription>Memory usage of docker containers</CardDescription> </CardTitle>{' '}
</CardHeader> <CardDescription>Memory usage of docker containers</CardDescription>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}> </CardHeader>
<Suspense fallback={<Spinner />}> <CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
{server?.stats?.mem && ( <Suspense fallback={<Spinner />}>
<ContainerMemChart {server?.stats?.m && (
chartData={containerMemChartData} <ContainerMemChart
max={server.stats.mem * 1024} chartData={containerMemChartData}
/> max={server.stats.m * 1024}
)} />
</Suspense> )}
</CardContent> </Suspense>
</Card> </CardContent>
</Card>
)}
<Card className="pb-2"> <Card className="pb-2">
<CardHeader> <CardHeader>
<CardTitle>Disk Usage</CardTitle> <CardTitle>Disk Usage</CardTitle>

View File

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

View File

@@ -58,21 +58,30 @@
} }
} }
.recharts-tooltip-wrapper {
z-index: 1;
}
/* charts */ /* charts */
@layer base { @layer base {
:root { :root {
--chart-1: 12 76% 61%; /* --chart-1: 12 76% 61%;
--chart-2: 173 58% 39%; --chart-2: 173 58% 39%;
--chart-3: 197 37% 24%; --chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; --chart-4: 43 74% 66%;
--chart-5: 27 87% 67%; --chart-5: 27 87% 67%; */
}
.dark {
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55%; --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="container">
<div className="flex items-center h-16 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}> <a
<Tooltip> href="/"
<TooltipTrigger asChild> aria-label="Home"
<a className={'p-2 pl-0 -mb-1'}
href="/" onClick={(e) => {
aria-label="Home" e.preventDefault()
className={'p-2 pl-0 -mb-1'} navigate('/')
onClick={(e) => { }}
e.preventDefault() >
navigate('/') <Logo className="h-[1.1em] fill-foreground" />
}} </a>
>
<Logo className="h-[1.1em] fill-foreground" />
</a>
</TooltipTrigger>
<TooltipContent>
<p>Home</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className={'flex gap-1 ml-auto'}> <div className={'flex gap-1 ml-auto'}>
<TooltipProvider delayDuration={300}> <TooltipProvider delayDuration={300}>
<Tooltip> <Tooltip>

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

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

View File

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