mirror of
https://github.com/fankes/beszel.git
synced 2025-10-20 02:09:28 +08:00
site updates
This commit is contained in:
BIN
site/bun.lockb
BIN
site/bun.lockb
Binary file not shown.
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nanostores/react": "^0.7.2",
|
"@nanostores/react": "^0.7.2",
|
||||||
|
"@nanostores/router": "^0.15.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
@@ -22,15 +23,15 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"lucide-react": "^0.401.0",
|
"lucide-react": "^0.407.0",
|
||||||
"nanostores": "^0.10.3",
|
"nanostores": "^0.10.3",
|
||||||
"pocketbase": "^0.21.3",
|
"pocketbase": "^0.21.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"recharts": "^2.12.7",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"valibot": "^0.36.0",
|
"valibot": "^0.36.0"
|
||||||
"wouter": "^3.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.1.6",
|
"@types/bun": "^1.1.6",
|
||||||
|
@@ -13,11 +13,10 @@ import {
|
|||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
} from '@/components/ui/command'
|
} from '@/components/ui/command'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { navigate } from 'wouter/use-browser-location'
|
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $servers } from '@/lib/stores'
|
import { $servers, navigate } from '@/lib/stores'
|
||||||
|
|
||||||
export function CommandPalette() {
|
export default function () {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const servers = useStore($servers)
|
const servers = useStore($servers)
|
||||||
|
|
||||||
|
117
site/src/components/cpu-chart.tsx
Normal file
117
site/src/components/cpu-chart.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
} from '@/components/ui/chart'
|
||||||
|
import { formatDateShort } from '@/lib/utils'
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
|
||||||
|
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
|
||||||
|
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
|
||||||
|
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
|
||||||
|
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
|
||||||
|
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
|
||||||
|
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
|
||||||
|
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
|
||||||
|
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
|
||||||
|
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
|
||||||
|
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
|
||||||
|
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
|
||||||
|
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
|
||||||
|
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
|
||||||
|
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
|
||||||
|
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
|
||||||
|
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
|
||||||
|
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
|
||||||
|
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
|
||||||
|
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
|
||||||
|
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
|
||||||
|
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
|
||||||
|
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
|
||||||
|
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
|
||||||
|
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
|
||||||
|
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
|
||||||
|
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// for (const data of chartData) {
|
||||||
|
// data.month = formatDateShort(data.month)
|
||||||
|
// }
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
cpu: {
|
||||||
|
label: 'cpu',
|
||||||
|
color: 'hsl(var(--chart-1))',
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={chartData}
|
||||||
|
margin={{
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 7,
|
||||||
|
bottom: 7,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis domain={[0, 100]} tickCount={5} tickLine={false} axisLine={false} tickMargin={8} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="month"
|
||||||
|
tickLine={true}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
minTickGap={30}
|
||||||
|
tickFormatter={(value) => formatDateShort(value)}
|
||||||
|
/>
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
|
||||||
|
<Area
|
||||||
|
dataKey="cpu"
|
||||||
|
type="natural"
|
||||||
|
fill="var(--color-cpu)"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke="var(--color-cpu)"
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
)
|
||||||
|
}
|
53
site/src/components/example-chart.tsx
Normal file
53
site/src/components/example-chart.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
'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>
|
||||||
|
)
|
||||||
|
}
|
0
site/src/components/loader.tsx
Normal file
0
site/src/components/loader.tsx
Normal file
@@ -1,11 +1,9 @@
|
|||||||
import { Link } from 'wouter'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import { UserAuthForm } from '@/components/user-auth-form'
|
import { UserAuthForm } from '@/components/user-auth-form'
|
||||||
import { ChevronLeft } from 'lucide-react'
|
import { ChevronLeft } from 'lucide-react'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function () {
|
||||||
return (
|
return (
|
||||||
<div className="container relative hidden h-screen flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0">
|
<div className="container relative hidden h-screen flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||||
<div className="lg:p-8">
|
<div className="lg:p-8">
|
||||||
@@ -23,9 +21,9 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
<UserAuthForm />
|
<UserAuthForm />
|
||||||
<p className="px-8 text-center text-sm text-muted-foreground">
|
<p className="px-8 text-center text-sm text-muted-foreground">
|
||||||
<Link href="/register" className="hover:text-brand underline underline-offset-4">
|
<a href="/register" className="hover:text-brand underline underline-offset-4">
|
||||||
Don't have an account? Sign Up
|
Don't have an account? Sign Up
|
||||||
</Link>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
import { useEffect } from 'react'
|
import { Suspense, lazy, useEffect } from 'react'
|
||||||
import { $servers, pb } from '@/lib/stores'
|
import { $servers, pb } from '@/lib/stores'
|
||||||
import { DataTable } from '../server-table/data-table'
|
// import { DataTable } from '../server-table/data-table'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
||||||
import { SystemRecord } from '@/types'
|
import { SystemRecord } from '@/types'
|
||||||
import { updateServerList } from '@/lib/utils'
|
import { updateServerList } from '@/lib/utils'
|
||||||
|
|
||||||
export function Home() {
|
const DataTable = lazy(() => import('../server-table/data-table'))
|
||||||
|
|
||||||
|
export default function () {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Home'
|
document.title = 'Home'
|
||||||
}, [])
|
}, [])
|
||||||
@@ -57,7 +59,9 @@ export function Home() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<DataTable />
|
<Suspense>
|
||||||
|
<DataTable />
|
||||||
|
</Suspense>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
|
@@ -1,24 +1,32 @@
|
|||||||
import { $servers, pb } from '@/lib/stores'
|
import { $servers, pb } from '@/lib/stores'
|
||||||
import { ContainerStatsRecord, SystemRecord } from '@/types'
|
import { ContainerStatsRecord, SystemRecord } from '@/types'
|
||||||
import { useEffect, useState } from 'react'
|
import { Suspense, lazy, useEffect, useState } from 'react'
|
||||||
import { useRoute } from 'wouter'
|
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
|
import Spinner from '../spinner'
|
||||||
|
// import { CpuChart } from '../cpu-chart'
|
||||||
|
|
||||||
|
const CpuChart = lazy(() => import('../cpu-chart'))
|
||||||
|
|
||||||
function timestampToBrowserTime(timestamp: string) {
|
function timestampToBrowserTime(timestamp: string) {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
return date.toLocaleString()
|
return date.toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServerDetail() {
|
// function addColors(objects: Record<string, any>[]) {
|
||||||
|
// objects.forEach((obj, index) => {
|
||||||
|
// const hue = ((index * 360) / objects.length) % 360 // Distribute hues evenly
|
||||||
|
// obj.fill = `hsl(${hue}, 100%, 50%)` // Set fill to HSL color with full saturation and 50% lightness
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default function ServerDetail({ name }: { name: string }) {
|
||||||
const servers = useStore($servers)
|
const servers = useStore($servers)
|
||||||
const [_, params] = useRoute('/server/:name')
|
|
||||||
const [server, setServer] = useState({} as SystemRecord)
|
const [server, setServer] = useState({} as SystemRecord)
|
||||||
const [containers, setContainers] = useState([] as ContainerStatsRecord[])
|
const [containers, setContainers] = useState([] as ContainerStatsRecord[])
|
||||||
// const [serverId, setServerId] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = params!.name
|
document.title = name
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -27,7 +35,7 @@ export function ServerDetail() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('running')
|
console.log('running')
|
||||||
const matchingServer = servers.find((s) => s.name === params!.name) as SystemRecord
|
const matchingServer = servers.find((s) => s.name === name) as SystemRecord
|
||||||
|
|
||||||
setServer(matchingServer)
|
setServer(matchingServer)
|
||||||
|
|
||||||
@@ -52,6 +60,20 @@ export function ServerDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className="grid grid-cols-1 gap-10">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>CPU Usage</CardTitle>
|
||||||
|
<CardDescription>Showing total visitors for the last 30 minutes</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pl-0 w-[calc(100%-2em)] h-72 relative">
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<CpuChart />
|
||||||
|
</Suspense>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className={'mb-3'}>{server.name}</CardTitle>
|
<CardTitle className={'mb-3'}>{server.name}</CardTitle>
|
||||||
|
@@ -48,18 +48,15 @@ import {
|
|||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
Copy,
|
Copy,
|
||||||
RefreshCcw,
|
|
||||||
Server,
|
Server,
|
||||||
Cpu,
|
Cpu,
|
||||||
MemoryStick,
|
MemoryStick,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { navigate } from 'wouter/use-browser-location'
|
import { $servers, pb, navigate } from '@/lib/stores'
|
||||||
import { $servers, pb } from '@/lib/stores'
|
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { AddServerButton } from '../add-server'
|
import { AddServerButton } from '../add-server'
|
||||||
import clsx from 'clsx'
|
|
||||||
import { cn, copyToClipboard } from '@/lib/utils'
|
import { cn, copyToClipboard } from '@/lib/utils'
|
||||||
|
|
||||||
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||||
@@ -74,7 +71,7 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
|||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<span className="grow block bg-muted h-4 relative rounded-sm overflow-hidden">
|
<span className="grow block bg-muted h-4 relative rounded-sm overflow-hidden">
|
||||||
<span
|
<span
|
||||||
className={clsx('absolute inset-0 w-full h-full origin-left', `bg-${color}-500`)}
|
className={cn('absolute inset-0 w-full h-full origin-left', `bg-${color}-500`)}
|
||||||
style={{ transform: `scalex(${val}%)` }}
|
style={{ transform: `scalex(${val}%)` }}
|
||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
@@ -97,7 +94,7 @@ function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Ico
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable() {
|
export default function () {
|
||||||
const data = useStore($servers)
|
const data = useStore($servers)
|
||||||
const [deleteServer, setDeleteServer] = useState({} as SystemRecord)
|
const [deleteServer, setDeleteServer] = useState({} as SystemRecord)
|
||||||
const [sorting, setSorting] = useState<SortingState>([])
|
const [sorting, setSorting] = useState<SortingState>([])
|
||||||
@@ -111,7 +108,7 @@ export function DataTable() {
|
|||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<span className="flex gap-1 items-center text-base">
|
<span className="flex gap-1 items-center text-base">
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={cn(
|
||||||
'w-2.5 h-2.5 block left-0 rounded-full',
|
'w-2.5 h-2.5 block left-0 rounded-full',
|
||||||
info.row.original.active ? 'bg-green-500' : 'bg-red-500'
|
info.row.original.active ? 'bg-green-500' : 'bg-red-500'
|
||||||
)}
|
)}
|
||||||
|
9
site/src/components/spinner.tsx
Normal file
9
site/src/components/spinner.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { LoaderCircleIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<div className="grid place-content-center h-full absolute inset-0">
|
||||||
|
<LoaderCircleIcon className="animate-spin h-10 w-10 opacity-60" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
361
site/src/components/ui/chart.tsx
Normal file
361
site/src/components/ui/chart.tsx
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as RechartsPrimitive from "recharts"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
const THEMES = { light: "", dark: ".dark" } as const
|
||||||
|
|
||||||
|
export type ChartConfig = {
|
||||||
|
[k in string]: {
|
||||||
|
label?: React.ReactNode
|
||||||
|
icon?: React.ComponentType
|
||||||
|
} & (
|
||||||
|
| { color?: string; theme?: never }
|
||||||
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartContextProps = {
|
||||||
|
config: ChartConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||||
|
|
||||||
|
function useChart() {
|
||||||
|
const context = React.useContext(ChartContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useChart must be used within a <ChartContainer />")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartContainer = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
config: ChartConfig
|
||||||
|
children: React.ComponentProps<
|
||||||
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
|
>["children"]
|
||||||
|
}
|
||||||
|
>(({ id, className, children, config, ...props }, ref) => {
|
||||||
|
const uniqueId = React.useId()
|
||||||
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartContext.Provider value={{ config }}>
|
||||||
|
<div
|
||||||
|
data-chart={chartId}
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChartStyle id={chartId} config={config} />
|
||||||
|
<RechartsPrimitive.ResponsiveContainer>
|
||||||
|
{children}
|
||||||
|
</RechartsPrimitive.ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</ChartContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
ChartContainer.displayName = "Chart"
|
||||||
|
|
||||||
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
|
const colorConfig = Object.entries(config).filter(
|
||||||
|
([_, config]) => config.theme || config.color
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!colorConfig.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: Object.entries(THEMES).map(
|
||||||
|
([theme, prefix]) => `
|
||||||
|
${prefix} [data-chart=${id}] {
|
||||||
|
${colorConfig
|
||||||
|
.map(([key, itemConfig]) => {
|
||||||
|
const color =
|
||||||
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||||
|
itemConfig.color
|
||||||
|
return color ? ` --color-${key}: ${color};` : null
|
||||||
|
})
|
||||||
|
.join("\n")}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||||
|
|
||||||
|
const ChartTooltipContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
hideLabel?: boolean
|
||||||
|
hideIndicator?: boolean
|
||||||
|
indicator?: "line" | "dot" | "dashed"
|
||||||
|
nameKey?: string
|
||||||
|
labelKey?: string
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
className,
|
||||||
|
indicator = "dot",
|
||||||
|
hideLabel = false,
|
||||||
|
hideIndicator = false,
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
labelClassName,
|
||||||
|
formatter,
|
||||||
|
color,
|
||||||
|
nameKey,
|
||||||
|
labelKey,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const { config } = useChart()
|
||||||
|
|
||||||
|
const tooltipLabel = React.useMemo(() => {
|
||||||
|
if (hideLabel || !payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [item] = payload
|
||||||
|
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
const value =
|
||||||
|
!labelKey && typeof label === "string"
|
||||||
|
? config[label as keyof typeof config]?.label || label
|
||||||
|
: itemConfig?.label
|
||||||
|
|
||||||
|
if (labelFormatter) {
|
||||||
|
return (
|
||||||
|
<div className={cn("font-medium", labelClassName)}>
|
||||||
|
{labelFormatter(value, payload)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||||
|
}, [
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
payload,
|
||||||
|
hideLabel,
|
||||||
|
labelClassName,
|
||||||
|
config,
|
||||||
|
labelKey,
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!active || !payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!nestLabel ? tooltipLabel : null}
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{payload.map((item, index) => {
|
||||||
|
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
const indicatorColor = color || item.payload.fill || item.color
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.dataKey}
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||||
|
indicator === "dot" && "items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatter && (item?.value !== undefined) && item.name ? (
|
||||||
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{itemConfig?.icon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
!hideIndicator && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||||
|
{
|
||||||
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
|
"w-1": indicator === "line",
|
||||||
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||||
|
indicator === "dashed",
|
||||||
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--color-bg": indicatorColor,
|
||||||
|
"--color-border": indicatorColor,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 justify-between leading-none",
|
||||||
|
nestLabel ? "items-end" : "items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{nestLabel ? tooltipLabel : null}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{itemConfig?.label || item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.value && (
|
||||||
|
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||||
|
{item.value.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ChartTooltipContent.displayName = "ChartTooltip"
|
||||||
|
|
||||||
|
const ChartLegend = RechartsPrimitive.Legend
|
||||||
|
|
||||||
|
const ChartLegendContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<"div"> &
|
||||||
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||||
|
hideIcon?: boolean
|
||||||
|
nameKey?: string
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const { config } = useChart()
|
||||||
|
|
||||||
|
if (!payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center gap-4",
|
||||||
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{payload.map((item) => {
|
||||||
|
const key = `${nameKey || item.dataKey || "value"}`
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
|
style={{
|
||||||
|
backgroundColor: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{itemConfig?.label}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ChartLegendContent.displayName = "ChartLegend"
|
||||||
|
|
||||||
|
// Helper to extract item config from a payload.
|
||||||
|
function getPayloadConfigFromPayload(
|
||||||
|
config: ChartConfig,
|
||||||
|
payload: unknown,
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
|
if (typeof payload !== "object" || payload === null) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadPayload =
|
||||||
|
"payload" in payload &&
|
||||||
|
typeof payload.payload === "object" &&
|
||||||
|
payload.payload !== null
|
||||||
|
? payload.payload
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
let configLabelKey: string = key
|
||||||
|
|
||||||
|
if (
|
||||||
|
key in payload &&
|
||||||
|
typeof payload[key as keyof typeof payload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payload[key as keyof typeof payload] as string
|
||||||
|
} else if (
|
||||||
|
payloadPayload &&
|
||||||
|
key in payloadPayload &&
|
||||||
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payloadPayload[
|
||||||
|
key as keyof typeof payloadPayload
|
||||||
|
] as string
|
||||||
|
}
|
||||||
|
|
||||||
|
return configLabelKey in config
|
||||||
|
? config[configLabelKey]
|
||||||
|
: config[key as keyof typeof config]
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartStyle,
|
||||||
|
}
|
@@ -57,3 +57,22 @@
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* charts */
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--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-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,11 +1,24 @@
|
|||||||
import PocketBase from 'pocketbase'
|
import PocketBase from 'pocketbase'
|
||||||
import { atom } from 'nanostores'
|
import { atom } from 'nanostores'
|
||||||
import { SystemRecord } from '@/types'
|
import { SystemRecord } from '@/types'
|
||||||
|
import { createRouter } from '@nanostores/router'
|
||||||
|
|
||||||
export const pb = new PocketBase('/')
|
export const pb = new PocketBase('/')
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
pb.authStore.storageKey = 'pb_admin_auth'
|
pb.authStore.storageKey = 'pb_admin_auth'
|
||||||
|
|
||||||
|
export const $router = createRouter(
|
||||||
|
{
|
||||||
|
home: '/',
|
||||||
|
server: '/server/:name',
|
||||||
|
},
|
||||||
|
{ links: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
export const navigate = (urlString: string) => {
|
||||||
|
$router.open(urlString)
|
||||||
|
}
|
||||||
|
|
||||||
export const $servers = atom([] as SystemRecord[])
|
export const $servers = atom([] as SystemRecord[])
|
||||||
|
|
||||||
export const $authenticated = atom(pb.authStore.isValid)
|
export const $authenticated = atom(pb.authStore.isValid)
|
||||||
|
@@ -31,3 +31,14 @@ export const updateServerList = () => {
|
|||||||
$servers.set(records)
|
$servers.set(records)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const shortDateFormatter = new Intl.DateTimeFormat('en-US', {
|
||||||
|
// day: 'numeric',
|
||||||
|
// month: 'numeric',
|
||||||
|
// year: '2-digit',
|
||||||
|
// hour12: false,
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const formatDateShort = (timestamp: string) => shortDateFormatter.format(new Date(timestamp))
|
||||||
|
@@ -1,40 +1,58 @@
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import React, { useEffect } from 'react'
|
import React, { Suspense, lazy, useEffect } from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { Route, Switch } from 'wouter'
|
import Home from './components/routes/home.tsx'
|
||||||
import { Home } from './components/routes/home.tsx'
|
|
||||||
import { ThemeProvider } from './components/theme-provider.tsx'
|
import { ThemeProvider } from './components/theme-provider.tsx'
|
||||||
import LoginPage from './components/login.tsx'
|
// import LoginPage from './components/login.tsx'
|
||||||
import { $authenticated } from './lib/stores.ts'
|
import { $authenticated, $router } from './lib/stores.ts'
|
||||||
import { ServerDetail } from './components/routes/server.tsx'
|
|
||||||
import { ModeToggle } from './components/mode-toggle.tsx'
|
import { ModeToggle } from './components/mode-toggle.tsx'
|
||||||
import { CommandPalette } from './components/command-palette.tsx'
|
// import { CommandPalette } from './components/command-palette.tsx'
|
||||||
import { cn, updateServerList } from './lib/utils.ts'
|
import { cn, updateServerList } from './lib/utils.ts'
|
||||||
import { buttonVariants } from './components/ui/button.tsx'
|
import { buttonVariants } from './components/ui/button.tsx'
|
||||||
import { Github } from 'lucide-react'
|
import { Github } from 'lucide-react'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { Toaster } from './components/ui/toaster.tsx'
|
import { Toaster } from './components/ui/toaster.tsx'
|
||||||
|
|
||||||
|
const ServerDetail = lazy(() => import('./components/routes/server.tsx'))
|
||||||
|
const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
|
||||||
|
const LoginPage = lazy(() => import('./components/login.tsx'))
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const authenticated = useStore($authenticated)
|
const page = useStore($router)
|
||||||
|
|
||||||
return <ThemeProvider>{authenticated ? <Main /> : <LoginPage />}</ThemeProvider>
|
|
||||||
}
|
|
||||||
|
|
||||||
const Main = () => {
|
|
||||||
// get servers
|
// get servers
|
||||||
useEffect(updateServerList, [])
|
useEffect(updateServerList, [])
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
return <h1>404</h1>
|
||||||
|
} else if (page.path === '/') {
|
||||||
|
return <Home />
|
||||||
|
} else if (page.route === 'server') {
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<ServerDetail name={page.params.name} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout = () => {
|
||||||
|
const authenticated = useStore($authenticated)
|
||||||
|
|
||||||
|
if (!authenticated) {
|
||||||
|
return <LoginPage />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-7 mb-14">
|
<div className="container mt-7 mb-14 relative">
|
||||||
<div className="flex mb-4">
|
<div className="flex mb-4">
|
||||||
{/* <Link
|
{/* <a
|
||||||
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
||||||
href="/"
|
href="/"
|
||||||
title={'All servers'}
|
title={'All servers'}
|
||||||
>
|
>
|
||||||
<HomeIcon className="h-[1.2rem] w-[1.2rem]" />
|
<HomeIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
</Link> */}
|
</a> */}
|
||||||
<div className={'flex gap-1 ml-auto'}>
|
<div className={'flex gap-1 ml-auto'}>
|
||||||
<a
|
<a
|
||||||
title={'Github'}
|
title={'Github'}
|
||||||
@@ -47,18 +65,7 @@ const Main = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*
|
<App />
|
||||||
Routes below are matched exclusively -
|
|
||||||
the first matched route gets rendered
|
|
||||||
*/}
|
|
||||||
<Switch>
|
|
||||||
<Route path="/" component={Home} />
|
|
||||||
|
|
||||||
<Route path="/server/:name" component={ServerDetail}></Route>
|
|
||||||
|
|
||||||
{/* Default route in a switch */}
|
|
||||||
<Route>404: No such page!</Route>
|
|
||||||
</Switch>
|
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
@@ -67,6 +74,8 @@ const Main = () => {
|
|||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('app')!).render(
|
ReactDOM.createRoot(document.getElementById('app')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
|
<Layout />
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user