This commit is contained in:
Henry Dollman
2024-07-14 23:39:51 -04:00
parent 2e48aa5560
commit f1819e59b9
17 changed files with 488 additions and 151 deletions

View File

@@ -390,8 +390,8 @@ func handleStatusAlerts(newStatus string, oldRecord *models.Record) error {
// send alert
sendAlert(EmailData{
to: user.Get("email").(string),
subj: fmt.Sprintf("Server %s is %s %v", systemName, alertStatus, emoji),
body: fmt.Sprintf("Server %s is %s %v", systemName, alertStatus, emoji),
subj: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
body: fmt.Sprintf("Connection to %s is %s %v\n\n- Qoma", systemName, alertStatus, emoji),
})
}
return nil

Binary file not shown.

View File

@@ -1,46 +1,49 @@
{
"name": "site",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@nanostores/react": "^0.7.2",
"@nanostores/router": "^0.15.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-table": "^8.19.2",
"@vitejs/plugin-react": "^4.3.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"lucide-react": "^0.407.0",
"nanostores": "^0.10.3",
"pocketbase": "^0.21.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"recharts": "^2.13.0-alpha.1",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"valibot": "^0.36.0"
},
"devDependencies": {
"@types/bun": "^1.1.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.4",
"typescript": "^5.5.3",
"vite": "^5.3.3"
}
"name": "site",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@nanostores/react": "^0.7.2",
"@nanostores/router": "^0.15.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-table": "^8.19.2",
"@vitejs/plugin-react": "^4.3.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"lucide-react": "^0.407.0",
"nanostores": "^0.10.3",
"pocketbase": "^0.21.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"recharts": "^2.13.0-alpha.1",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"valibot": "^0.36.0"
},
"devDependencies": {
"@types/bun": "^1.1.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.4",
"typescript": "^5.5.3",
"vite": "^5.3.3"
}
}

View File

@@ -52,15 +52,18 @@ export function AddServerButton() {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
const data = Object.fromEntries(formData) as Record<string, any>
data.status = 'down'
data.stats = {
c: 0,
d: 0,
dp: 0,
du: 0,
data.status = 'pending'
data.info = {
cpu: 0,
m: 0,
mp: 0,
mu: 0,
mp: 0,
mb: 0,
d: 0,
du: 0,
dp: 0,
dr: 0,
dw: 0,
} as SystemStats
try {
setOpen(false)

View File

@@ -51,7 +51,7 @@ export default function CpuChart({ chartData }: { chartData: { time: string; cpu
/>
<Area
dataKey="cpu"
type="natural"
type="monotone"
fill="var(--color-cpu)"
fillOpacity={0.4}
stroke="var(--color-cpu)"

View File

@@ -0,0 +1,93 @@
import { TrendingUp } from 'lucide-react'
import { Label, PolarRadiusAxis, RadialBar, RadialBarChart } from 'recharts'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
const chartData = [{ month: 'january', desktop: 1260, mobile: 570 }]
const chartConfig = {
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig
export function RadialChart() {
const totalVisitors = chartData[0].desktop + chartData[0].mobile
return (
<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>Radial Chart - Stacked</CardTitle>
<CardDescription>January - June 2024</CardDescription>
</CardHeader>
<CardContent className="flex flex-1 items-center pb-0">
<ChartContainer config={chartConfig} className="mx-auto aspect-square w-full max-w-[250px]">
<RadialBarChart data={chartData} endAngle={180} innerRadius={80} outerRadius={130}>
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<Label
content={({ viewBox }) => {
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
return (
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle">
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) - 16}
className="fill-foreground text-2xl font-bold"
>
{totalVisitors.toLocaleString()}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 4}
className="fill-muted-foreground"
>
Visitors
</tspan>
</text>
)
}
}}
/>
</PolarRadiusAxis>
<RadialBar
dataKey="desktop"
stackId="a"
cornerRadius={5}
fill="var(--color-desktop)"
className="stroke-transparent stroke-2"
/>
<RadialBar
dataKey="mobile"
fill="var(--color-mobile)"
stackId="a"
cornerRadius={5}
className="stroke-transparent stroke-2"
/>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="leading-none text-muted-foreground">
Showing total visitors for the last 6 months
</div>
</CardFooter>
</Card>
)
}

View File

@@ -97,7 +97,6 @@ export default function CommandPalette() {
<CommandShortcut>Admin</CommandShortcut>
</CommandItem>
<CommandItem
keywords={['email']}
onSelect={() => {
window.location.href = '/_/#/settings/backups'
}}

View File

@@ -1,53 +0,0 @@
'use client'
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: '#2563eb',
},
mobile: {
label: 'Mobile',
color: '#60a5fa',
},
} satisfies ChartConfig
export function Component() {
return (
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>
)
}

View File

@@ -1,8 +1,8 @@
export function Logo({ className }: { className?: string }) {
return (
// audiowide
<svg viewBox="0 0 300 82" className={className}>
<path d="M202.1 36v35h-12.6V36a7.5 7.5 0 0 0-.2-1.5q-.1-.8-.5-1.4a3.9 3.9 0 0 0-.7-.9q-1-1-3-1.3a9.2 9.2 0 0 0-.9 0h-16.5V71H155V24.5a6.3 6.3 0 0 1 .4-2.2 6 6 0 0 1 .1-.3q.5-1.1 1.4-2 .9-.8 2-1.3a6.4 6.4 0 0 1 2.5-.5h23a17.2 17.2 0 0 1 3 .3 22.8 22.8 0 0 1 2.6.6q3 1 5.8 3v-3.9h17.1a17.7 17.7 0 0 1 2.6.2 21.3 21.3 0 0 1 1.2.2 19.5 19.5 0 0 1 3.9 1.3 21.5 21.5 0 0 1 .1 0q2 .9 3.8 2.3a15 15 0 0 1 3.2 3.3 16.3 16.3 0 0 1 0 0 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.9v35H218V36a8 8 0 0 0-.1-1.5q-.3-1.4-1.1-2.3-1.3-1.3-3.9-1.3h-11.4q.6 2.4.6 5Zm94.8-.3v17.8a17.3 17.3 0 0 1-.3 3 23.1 23.1 0 0 1-.7 2.7 17 17 0 0 1-3 5.7 15.9 15.9 0 0 1-3.6 3.3 19.6 19.6 0 0 1-1.8 1Q284.4 71 280 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.7-3 15.9 15.9 0 0 1-3.3-3.7 19.6 19.6 0 0 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 .6-2.7q1-3 3-5.7a15.9 15.9 0 0 1 3.7-3.3 19.6 19.6 0 0 1 1.8-1q3.1-1.7 7.5-1.8a25.4 25.4 0 0 1 .6 0h22.9V48h-22.9a6.8 6.8 0 0 0-1.6.2 4.6 4.6 0 0 0-2.4 1.4A5.5 5.5 0 0 0 251 53a7.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 3.9 1.4h22.8a6.9 6.9 0 0 0 1.6-.2 4.6 4.6 0 0 0 2.4-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.6-1.4-4-1.4h-27.8v-12h27.9a17.3 17.3 0 0 1 3 .2 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.4 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 .6ZM75.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.1Zm68.5 6.7v17.2a22.2 22.2 0 0 1-.2 3 17 17 0 0 1-.6 2.9 18.6 18.6 0 0 1-1.2 2.8 15.3 15.3 0 0 1-1 1.7 15 15 0 0 1-3.1 3.4 14.2 14.2 0 0 1 0 0 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.4.3 17 17 0 0 1-1.5.1h-22.9q-2.6 0-5.7-1-3.2-.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-.4-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 .3-.7 18.6 18.6 0 0 1 2.4-3.5 15.6 15.6 0 0 1 2-2q2.7-2.1 5.9-3a24.2 24.2 0 0 1 2.8-.7q1.5-.3 3-.3h22.8a22.7 22.7 0 0 1 3.7.3q2.2.4 4 1.2a13.5 13.5 0 0 1 .6.3 18 18 0 0 1 3.4 2.2 15 15 0 0 1 2.1 2.2q2.1 2.7 3 5.8a23.3 23.3 0 0 1 .8 3 17.3 17.3 0 0 1 .2 2.8ZM63 42.7V29.3a20.4 20.4 0 0 0-.4-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.4Zm68.5 10.5V36a8.4 8.4 0 0 0 0-1.5l-.5-1.4a3.7 3.7 0 0 0-.8-1 4.2 4.2 0 0 0-1.7-1l-1.4-.3a8.9 8.9 0 0 0-.7 0h-22.8q-1.8 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 0h22.8a8 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.7q.2-.7.2-1.5a8.9 8.9 0 0 0 0-.6Z" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 353.3 75.1" className={className}>
<path d="M118.9 57.3H96v-12h22.9a6.8 6.8 0 0 0 1.6-.3 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.3 6.3 0 0 0-.2-1.7 4.5 4.5 0 0 0-1.4-2.2 5.5 5.5 0 0 0-3.7-1.4 7 7 0 0 0-.1 0H96a6.8 6.8 0 0 0-1.6.2A4.6 4.6 0 0 0 92 36a5.5 5.5 0 0 0-1.4 3.3 7.3 7.3 0 0 0 0 .6v17.7a6.5 6.5 0 0 0 .2 1.7 4.6 4.6 0 0 0 1.4 2.3 5.5 5.5 0 0 0 3.3 1.4 7.3 7.3 0 0 0 .6 0h22.8V75H96a17.3 17.3 0 0 1-3-.3 23.1 23.1 0 0 1-2.7-.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.8q-1.7-3.1-1.8-7.5a25.4 25.4 0 0 1 0-.6V39.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.8-1q3.1-1.7 7.5-1.8a25.4 25.4 0 0 1 .6 0h22.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.4 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 .6 17.3 17.3 0 0 1-.3 3 23.1 23.1 0 0 1-.7 2.7 17 17 0 0 1-3 5.6 15.9 15.9 0 0 1-3.6 3.4 19.6 19.6 0 0 1-1.8 1q-3.1 1.7-7.5 1.8a25.4 25.4 0 0 1-.6 0Zm185.2 0h-23v-12h23a6.8 6.8 0 0 0 1.6-.3 4.6 4.6 0 0 0 2.4-1.4 5.5 5.5 0 0 0 1.3-3.3 7.3 7.3 0 0 0 0-.6 6.3 6.3 0 0 0-.1-1.7 4.5 4.5 0 0 0-1.4-2.2 5.5 5.5 0 0 0-3.7-1.4 7 7 0 0 0-.1 0h-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.3 3.3 7.3 7.3 0 0 0 0 .6v17.7a6.5 6.5 0 0 0 .1 1.7 4.6 4.6 0 0 0 1.4 2.3 5.5 5.5 0 0 0 3.3 1.4 7.3 7.3 0 0 0 .6 0h22.9V75h-23a17.3 17.3 0 0 1-3-.3 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.8q-1.7-3.1-1.8-7.5a25.4 25.4 0 0 1 0-.6V39.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.6-3.3 19.6 19.6 0 0 1 1.8-1q3.2-1.7 7.6-1.8a25.4 25.4 0 0 1 .6 0H304a17.3 17.3 0 0 1 3 .3 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 .6 17.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.6 15.9 15.9 0 0 1-3.6 3.4 19.6 19.6 0 0 1-1.8 1q-3.2 1.7-7.6 1.8a25.4 25.4 0 0 1-.5 0ZM178 75h-34.3V62.4H178a8 8 0 0 0 1.5-.1l1.5-.5a4 4 0 0 0 1-.7 4.2 4.2 0 0 0 1-1.8q.2-.7.2-1.5a8.7 8.7 0 0 0 0-.5 7.7 7.7 0 0 0-.3-2q-.8-2.4-3.1-3a7.2 7.2 0 0 0-1.7-.1h-19.7a20 20 0 0 1-3.2-.2q-1.7-.3-3.2-1a11.7 11.7 0 0 1-.7-.3 16 16 0 0 1-2.9-1.8 13.1 13.1 0 0 1-1.8-1.8 14.3 14.3 0 0 1-2.2-3.5 13.2 13.2 0 0 1-.5-1.3q-.8-2.5-.8-4.7a19.8 19.8 0 0 1 .3-3.2q.2-1.7.9-3.2a11.3 11.3 0 0 1 .3-.8 15.4 15.4 0 0 1 2-3 13 13 0 0 1 1.8-1.7 15 15 0 0 1 5-2.6 20.2 20.2 0 0 1 2.6-.6 15.4 15.4 0 0 1 2.4-.2h31.2V35h-31q-1 0-1.6.2a2 2 0 0 0 0 0 2.5 2.5 0 0 0-.4.2l-.3.3a1.3 1.3 0 0 0-.1.1 1.7 1.7 0 0 0-.3.5 1.5 1.5 0 0 0 0 .3v.8a4.6 4.6 0 0 0 0 .5v.4a1.9 1.9 0 0 0 .2.3 1.9 1.9 0 0 0 .2.4 1.4 1.4 0 0 0 .4.3 1.9 1.9 0 0 0 .6.2 2.3 2.3 0 0 0 .2 0 17.2 17.2 0 0 0 1 0 16 16 0 0 0 0 0H178a22.7 22.7 0 0 1 3.8.3q2.1.4 3.9 1.1a13.5 13.5 0 0 1 .6.4 18 18 0 0 1 3.4 2.2 15 15 0 0 1 2.1 2.2q2.1 2.6 3 5.8a23.3 23.3 0 0 1 .8 3 17.3 17.3 0 0 1 .2 2.8 22.2 22.2 0 0 1-.2 3 17 17 0 0 1-.6 2.9A18.6 18.6 0 0 1 194 66a15.3 15.3 0 0 1-1 1.7 15 15 0 0 1-3.1 3.4 14.2 14.2 0 0 1 0 0 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.4.3 17 17 0 0 1-1.5.1Zm75.5-42-29.4 29.3h30.5v12.7H209q-2 0-3.5-1.1a6.8 6.8 0 0 1-2.4-2.8 6.4 6.4 0 0 1-.5-2.5 6.5 6.5 0 0 1 .2-1.2 6.3 6.3 0 0 1 1.7-3.3L233.6 35h-30.5V22.3h46a6.3 6.3 0 0 1 3.4 1q1.6 1 2.3 3 .6 1.2.6 2.4a6 6 0 0 1-.2 1.2q-.3 1.8-1.6 3.2ZM70 57.3v2.4a15.6 15.6 0 0 1-.3 2.8 20.5 20.5 0 0 1-.5 2.2q-.8 2.7-2.7 5a14 14 0 0 1-3.2 3 17.2 17.2 0 0 1-1.5.9q-3 1.5-7.2 1.5H6.4a6.7 6.7 0 0 1-2-.3 6.1 6.1 0 0 1-.5-.2 6.2 6.2 0 0 1-2-1.3 6.2 6.2 0 0 1-1.4-2A6.4 6.4 0 0 1 0 69a7.5 7.5 0 0 1 0-.3V11.5A6.4 6.4 0 0 1 .5 9Q1 7.8 1.8 7a6 6 0 0 1 2-1.4A6.4 6.4 0 0 1 6.2 5a7.5 7.5 0 0 1 .3 0h42.5a15.2 15.2 0 0 1 2.7.2A20 20 0 0 1 54 6q2.7.8 5 2.7a14 14 0 0 1 3 3.3 17 17 0 0 1 .9 1.4q1.5 3 1.5 7.2V23a23.2 23.2 0 0 1-.3 4 30.7 30.7 0 0 1-.8 3.3 23.9 23.9 0 0 1-3.5 7 26.8 26.8 0 0 1-.1.3 22.4 22.4 0 0 1 4 3.2 20.1 20.1 0 0 1 3 3.8 22.6 22.6 0 0 1 .3.5 21.5 21.5 0 0 1 1.7 3.6 26 26 0 0 1 .5 1.9 23.8 23.8 0 0 1 .7 4 30 30 0 0 1 .2 2.8Zm-12.7 2.3v-2.3a13.6 13.6 0 0 0-.2-2.5 10.7 10.7 0 0 0-.6-2 10 10 0 0 0-1.8-3 9.4 9.4 0 0 0-.4-.4 9.4 9.4 0 0 0-3.1-2 11 11 0 0 0-.3-.2 11.6 11.6 0 0 0-2.7-.6 14.7 14.7 0 0 0-1.8-.1H17.8V33.7h22.9a14.1 14.1 0 0 0 2.5-.2 11.2 11.2 0 0 0 2-.6 9.7 9.7 0 0 0 2.7-1.5 9 9 0 0 0 .7-.6 9.5 9.5 0 0 0 2.1-3.3 11 11 0 0 0 0-.1 11.3 11.3 0 0 0 .7-2.7 14.6 14.6 0 0 0 .1-1.8v-2.4q0-2.7-2.6-2.7H12.7v44.6h41.9a6 6 0 0 0 .3 0h.5a2 2 0 0 0 .7-.2 2 2 0 0 0 .2-.1 1.5 1.5 0 0 0 .3-.3l.4-.6.3-1a6 6 0 0 0 0-.6Zm296 2.8v12.7h-5.7a18.4 18.4 0 0 1-3.3-.3 23.8 23.8 0 0 1-2.5-.6 17.2 17.2 0 0 1-5.6-3 19.5 19.5 0 0 1-.2 0 16 16 0 0 1-3.5-4 19.4 19.4 0 0 1-1-1.6q-1.6-3-1.7-7.4a26.5 26.5 0 0 1 0-1V0h12.7v57.3a6 6 0 0 0 .2 1.6 4.5 4.5 0 0 0 1.2 2.1 5 5 0 0 0 3.3 1.4 6.6 6.6 0 0 0 .4 0h5.7Z" />
</svg>
)
}

View File

@@ -9,7 +9,16 @@ import Spinner from '../spinner'
// import DiskChart from '../charts/disk-chart'
// import ContainerCpuChart from '../charts/container-cpu-chart'
// import ContainerMemChart from '../charts/container-mem-chart'
import { CpuIcon, MemoryStickIcon } from 'lucide-react'
import { CpuIcon, MemoryStickIcon, ServerIcon } from 'lucide-react'
import { RadialChart } from '../charts/radial'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
const CpuChart = lazy(() => import('../charts/cpu-chart'))
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
@@ -87,7 +96,7 @@ export default function ServerDetail({ name }: { name: string }) {
const memData = [] as { time: string; mem: number; memUsed: number; memCache: number }[]
const diskData = [] as { time: string; disk: number; diskUsed: number }[]
for (let { created, stats } of serverStats) {
cpuData.push({ time: created, cpu: stats.c })
cpuData.push({ time: created, cpu: stats.cpu })
// maxCpu = Math.max(maxCpu, stats.c)
memData.push({ time: created, mem: stats.m, memUsed: stats.mu, memCache: stats.mb })
diskData.push({ time: created, disk: stats.d, diskUsed: stats.du })
@@ -135,18 +144,32 @@ export default function ServerDetail({ name }: { name: string }) {
containerCpuData.push(cpuData)
containerMemData.push(memData)
}
// console.log('containerMemData', containerMemData)
setContainerCpuChartData(containerCpuData.reverse())
setContainerMemChartData(containerMemData.reverse())
}, [containers])
return (
<>
<div className="grid gap-6 mb-10">
<div className="p-6">
<h1 className="text-3xl font-semibold">{name}</h1>
</div>
<Card className="pb-2">
<div className="grid gap-6 mb-10 grid-cols-3">
<Card className="col-span-full">
<CardHeader>
<CardTitle className="flex gap-2 items-center text-3xl">{name}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between gap-6">
<p>{server.status}</p>
<p>Uptime {(server.info?.u / 86400).toLocaleString()} days</p>
<p>
{server.info?.m} ({server.info?.c} cores / {server.info?.t} threads)
</p>
</CardContent>
</Card>
<RadialChart />
<RadialChart />
<RadialChart />
<Card className="pb-3 col-span-full">
<CardHeader className="pb-5">
<CardTitle className="flex gap-2 justify-between">
<span>Total CPU Usage</span>
<CpuIcon className="opacity-70" />
@@ -162,8 +185,8 @@ export default function ServerDetail({ name }: { name: string }) {
</CardContent>
</Card>
{containerCpuChartData.length > 0 && (
<Card className="pb-2">
<CardHeader>
<Card className="pb-3 col-span-full">
<CardHeader className="pb-5">
<CardTitle className="flex gap-2 justify-between">
<span>Docker CPU Usage</span>
<CpuIcon className="opacity-70" />
@@ -177,8 +200,8 @@ export default function ServerDetail({ name }: { name: string }) {
</CardContent>
</Card>
)}
<Card className="pb-2">
<CardHeader>
<Card className="pb-3 col-span-full">
<CardHeader className="pb-5">
<CardTitle>Total Memory Usage</CardTitle>
<CardDescription>Precise utilization at the recorded time</CardDescription>
</CardHeader>
@@ -189,8 +212,8 @@ export default function ServerDetail({ name }: { name: string }) {
</CardContent>
</Card>
{containerMemChartData.length > 0 && (
<Card className="pb-2">
<CardHeader>
<Card className="pb-3 col-span-full">
<CardHeader className="pb-5">
<CardTitle className="flex gap-2 justify-between">
<span>Docker Memory Usage</span>
<MemoryStickIcon className="opacity-70" />
@@ -199,13 +222,13 @@ export default function ServerDetail({ name }: { name: string }) {
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
<Suspense fallback={<Spinner />}>
{server?.stats?.m && <ContainerMemChart chartData={containerMemChartData} />}
<ContainerMemChart chartData={containerMemChartData} />
</Suspense>
</CardContent>
</Card>
)}
<Card className="pb-2">
<CardHeader>
<Card className="pb-3 col-span-full">
<CardHeader className="pb-5">
<CardTitle>Disk Usage</CardTitle>
<CardDescription>Precise usage at the recorded time</CardDescription>
</CardHeader>

View File

@@ -57,7 +57,7 @@ import {
Trash2Icon,
BellIcon,
} from 'lucide-react'
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { $servers, pb, navigate } from '@/lib/stores'
import { useStore } from '@nanostores/react'
import { AddServerButton } from '../add-server'
@@ -70,6 +70,8 @@ import {
DialogTitle,
DialogHeader,
} from '@/components/ui/dialog'
import { Switch } from '@/components/ui/switch'
import { Separator } from '../ui/separator'
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
const val = info.getValue() as number
@@ -105,10 +107,13 @@ function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Ico
export default function SystemsTable() {
const data = useStore($servers)
// const [deleteServer, setDeleteServer] = useState({} as SystemRecord)
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
// useEffect(() => {
// console.log('servers', data)
// }, [data])
const columns: ColumnDef<SystemRecord>[] = useMemo(() => {
return [
{
@@ -124,7 +129,8 @@ export default function SystemsTable() {
className={cn('w-2 h-2 left-0 rounded-full', {
'bg-green-500': status === 'up',
'bg-red-500': status === 'down',
'bg-yellow-500': status === 'paused',
'bg-primary/40': status === 'paused',
'bg-yellow-500': status === 'pending',
})}
style={{ marginBottom: '-1px' }}
></span>
@@ -143,17 +149,17 @@ export default function SystemsTable() {
header: ({ column }) => sortableHeader(column, 'Server', Server),
},
{
accessorKey: 'stats.c',
accessorKey: 'info.cpu',
cell: CellFormatter,
header: ({ column }) => sortableHeader(column, 'CPU', Cpu),
},
{
accessorKey: 'stats.mp',
accessorKey: 'info.mp',
cell: CellFormatter,
header: ({ column }) => sortableHeader(column, 'Memory', MemoryStick),
},
{
accessorKey: 'stats.dp',
accessorKey: 'info.dp',
cell: CellFormatter,
header: ({ column }) => sortableHeader(column, 'Disk', HardDrive),
},
@@ -167,18 +173,44 @@ export default function SystemsTable() {
<div className={'flex justify-end items-center gap-1'}>
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size={'icon'} aria-label="Notifications" data-nolink>
<Button variant="ghost" size={'icon'} aria-label="Alerts" data-nolink>
<BellIcon className="h-[1.2em] w-[1.2em] pointer-events-none" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Notifications</DialogTitle>
<DialogTitle className="mb-1">Alerts for {name}</DialogTitle>
{isAdmin() && (
<DialogDescription>
Please{' '}
<a
href="/_/#/settings/mail"
className="font-medium text-primary opacity-80 hover:opacity-100 duration-100"
>
configure an SMTP server
</a>{' '}
to ensure alerts are delivered.
</DialogDescription>
)}
</DialogHeader>
<DialogDescription>
The agent must be running on the server to connect. Copy the{' '}
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the
agent below.
<div className="space-y-2 flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<label
className="font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-base"
htmlFor=":r3m:-form-item"
>
Status change
</label>
<p
id=":r3m:-form-item-description"
className="text-[0.8rem] text-muted-foreground"
>
Triggers when system status switches between up and down.
</p>
</div>
<Switch />
</div>
</DialogDescription>
</DialogContent>
</Dialog>
@@ -202,7 +234,7 @@ export default function SystemsTable() {
<DropdownMenuItem
onClick={() => {
pb.collection('systems').update(id, {
status: status === 'paused' ? 'up' : 'paused',
status: status === 'paused' ? 'pending' : 'paused',
})
}}
>

View File

@@ -0,0 +1,158 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@@ -124,7 +124,7 @@ const Layout = () => {
<a
href="/"
aria-label="Home"
className={'p-2 pl-0 -mb-1'}
className={'p-2 pl-0'}
onClick={(e) => {
e.preventDefault()
navigate('/')

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

@@ -3,28 +3,51 @@ import { RecordModel } from 'pocketbase'
export interface SystemRecord extends RecordModel {
name: string
host: string
status: 'up' | 'down' | 'paused'
status: 'up' | 'down' | 'paused' | 'pending'
port: string
stats: SystemStats
info: SystemInfo
}
export interface SystemInfo {
/** cpu percent */
cpu: number
/** cpu threads */
t: number
/** cpu cores */
c: number
/** cpu model */
m: string
/** operating system */
o?: string
/** uptime */
u: number
/** memory percent */
mp: number
/** disk percent */
dp: number
}
export interface SystemStats {
/** cpu percent */
c: number
/** disk size (gb) */
d: number
/** disk percent */
dp: number
/** disk used (gb) */
du: number
cpu: number
/** total memory (gb) */
m: number
/** memory used (gb) */
mu: number
/** memory percent */
mp: number
/** memory buffer + cache (gb) */
mb: number
/** memory used (gb) */
mu: number
/** disk size (gb) */
d: number
/** disk used (gb) */
du: number
/** disk percent */
dp: number
/** disk read (mb) */
dr: number
/** disk write (mb) */
dw: number
}
export interface ContainerStatsRecord extends RecordModel {
@@ -43,5 +66,5 @@ interface ContainerStats {
export interface SystemStatsRecord extends RecordModel {
system: string
stats: SystemStats
info: SystemStats
}