mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
updates
This commit is contained in:
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
@@ -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)"
|
||||
|
@@ -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)"
|
||||
|
@@ -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)"
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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%;
|
||||
} */
|
||||
}
|
||||
|
@@ -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
31
site/src/types.d.ts
vendored
@@ -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 {
|
||||
|
23
types.go
23
types.go
@@ -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"`
|
||||
}
|
||||
|
Reference in New Issue
Block a user