mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 01:39:34 +08:00
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/preact": "^0.5.1",
|
"@nanostores/preact": "^0.5.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",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Database, Home, Server } from 'lucide-react'
|
import { Database, Github, Home, Server } from 'lucide-react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
@@ -14,9 +14,12 @@ import {
|
|||||||
} from '@/components/ui/command'
|
} from '@/components/ui/command'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import { navigate } from 'wouter-preact/use-browser-location'
|
import { navigate } from 'wouter-preact/use-browser-location'
|
||||||
|
import { useStore } from '@nanostores/preact'
|
||||||
|
import { $servers } from '@/lib/stores'
|
||||||
|
|
||||||
export function CommandPalette() {
|
export function CommandPalette() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const servers = useStore($servers)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
@@ -45,6 +48,7 @@ export function CommandPalette() {
|
|||||||
>
|
>
|
||||||
<Home className="mr-2 h-4 w-4" />
|
<Home className="mr-2 h-4 w-4" />
|
||||||
<span>Home</span>
|
<span>Home</span>
|
||||||
|
<CommandShortcut>⌘H</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
@@ -55,27 +59,31 @@ export function CommandPalette() {
|
|||||||
<span>PocketBase</span>
|
<span>PocketBase</span>
|
||||||
<CommandShortcut>⌘P</CommandShortcut>
|
<CommandShortcut>⌘P</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
|
||||||
<CommandSeparator />
|
|
||||||
<CommandGroup heading="Systems">
|
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
navigate('/server/kagemusha')
|
window.location.href = 'https://github.com/henrygd'
|
||||||
setOpen((open) => !open)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Server className="mr-2 h-4 w-4" />
|
<Github className="mr-2 h-4 w-4" />
|
||||||
<span>Kagemusha</span>
|
<span>Documentation</span>
|
||||||
</CommandItem>
|
<CommandShortcut>⌘D</CommandShortcut>
|
||||||
<CommandItem onSelect={() => navigate('/server/rashomon')}>
|
|
||||||
<Server className="mr-2 h-4 w-4" />
|
|
||||||
<span>Rashomon</span>
|
|
||||||
</CommandItem>
|
|
||||||
<CommandItem onSelect={() => navigate('/server/ikiru')}>
|
|
||||||
<Server className="mr-2 h-4 w-4" />
|
|
||||||
<span>Ikiru</span>
|
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup heading="Servers">
|
||||||
|
{servers.map((server) => (
|
||||||
|
<CommandItem
|
||||||
|
key={server.id}
|
||||||
|
onSelect={() => {
|
||||||
|
navigate(`/server/${server.name}`)
|
||||||
|
setOpen((open) => !open)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Server className="mr-2 h-4 w-4" />
|
||||||
|
<span>{server.name}</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
)
|
)
|
||||||
|
@@ -11,7 +11,11 @@ export default function LoginPage() {
|
|||||||
<div className="lg:p-8">
|
<div className="lg:p-8">
|
||||||
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||||
<div className="flex flex-col space-y-2 text-center">
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
<ChevronLeft className="mx-auto h-6 w-6" />
|
{/* <img
|
||||||
|
className="mx-auto h-10 w-10 mb-2"
|
||||||
|
src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Numelon_Logo.png/240px-Numelon_Logo.png"
|
||||||
|
alt=""
|
||||||
|
/> */}
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Welcome back</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">Welcome back</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Enter your email to sign in to your account
|
Enter your email to sign in to your account
|
||||||
@@ -33,19 +37,12 @@ export default function LoginPage() {
|
|||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<div class="relative z-20 flex gap-2 items-center text-lg font-medium ml-auto">
|
<div class="relative z-20 flex gap-2 items-center text-lg font-medium ml-auto">
|
||||||
Melon
|
placeholder
|
||||||
<svg
|
<img
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
className={'w-6 h-6'}
|
||||||
viewBox="0 0 24 24"
|
src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Numelon_Logo.png/240px-Numelon_Logo.png"
|
||||||
fill="none"
|
alt=""
|
||||||
stroke="currentColor"
|
/>
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="h-6 w-6"
|
|
||||||
>
|
|
||||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
{/* <div class="relative z-20 mt-auto">
|
{/* <div class="relative z-20 mt-auto">
|
||||||
<blockquote class="space-y-2">
|
<blockquote class="space-y-2">
|
||||||
|
@@ -1,47 +1,56 @@
|
|||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect } from 'preact/hooks'
|
||||||
import { pb } from '@/lib/stores'
|
import { $servers, pb } from '@/lib/stores'
|
||||||
import { SystemRecord } from '@/types'
|
|
||||||
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 { useStore } from '@nanostores/preact'
|
||||||
|
import { SystemRecord } from '@/types'
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const [systems, setSystems] = useState([] as SystemRecord[])
|
const servers = useStore($servers)
|
||||||
|
// const [systems, setSystems] = useState([] as SystemRecord[])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Home'
|
document.title = 'Home'
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pb.collection<SystemRecord>('systems')
|
console.log('servers', servers)
|
||||||
.getFullList({
|
}, [servers])
|
||||||
sort: 'name',
|
|
||||||
})
|
|
||||||
.then((items) => {
|
|
||||||
setSystems(items)
|
|
||||||
})
|
|
||||||
|
|
||||||
// pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
useEffect(() => {
|
||||||
// setSystems((curSystems) => {
|
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
||||||
// const i = curSystems.findIndex((s) => s.id === e.record.id)
|
const curServers = $servers.get()
|
||||||
// if (i > -1) {
|
const newServers = []
|
||||||
// const newSystems = [...curSystems]
|
console.log('e', e)
|
||||||
// newSystems[i] = e.record
|
if (e.action === 'delete') {
|
||||||
// return newSystems
|
for (const server of curServers) {
|
||||||
// } else {
|
if (server.id !== e.record.id) {
|
||||||
// return [...curSystems, e.record]
|
newServers.push(server)
|
||||||
// }
|
}
|
||||||
// })
|
}
|
||||||
// })
|
} else {
|
||||||
// return () => pb.collection('systems').unsubscribe('*')
|
let found = 0
|
||||||
|
for (const server of curServers) {
|
||||||
|
if (server.id === e.record.id) {
|
||||||
|
found = newServers.push(e.record)
|
||||||
|
} else {
|
||||||
|
newServers.push(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
newServers.push(e.record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$servers.set(newServers)
|
||||||
|
})
|
||||||
|
return () => pb.collection('systems').unsubscribe('*')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// if (!systems.length) return <>Loading...</>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className={'mb-3'}>All Systems</CardTitle>
|
<CardTitle className={'mb-3'}>All Servers</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Press{' '}
|
Press{' '}
|
||||||
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||||
@@ -51,10 +60,9 @@ export function Home() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<DataTable data={systems} />
|
<DataTable />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
{/* <pre>{JSON.stringify(systems, null, 2)}</pre> */}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui
|
|||||||
|
|
||||||
export function ServerDetail() {
|
export function ServerDetail() {
|
||||||
const [_, params] = useRoute('/server/:name')
|
const [_, params] = useRoute('/server/:name')
|
||||||
const [node, setNode] = useState({} as SystemRecord)
|
const [server, setServer] = useState({} as SystemRecord)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = params!.name
|
document.title = params!.name
|
||||||
@@ -16,7 +16,7 @@ export function ServerDetail() {
|
|||||||
pb.collection<SystemRecord>('systems')
|
pb.collection<SystemRecord>('systems')
|
||||||
.getFirstListItem(`name="${params!.name}"`)
|
.getFirstListItem(`name="${params!.name}"`)
|
||||||
.then((record) => {
|
.then((record) => {
|
||||||
setNode(record)
|
setServer(record)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ export function ServerDetail() {
|
|||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className={'mb-3'}>{node.name}</CardTitle>
|
<CardTitle className={'mb-3'}>{server.name}</CardTitle>
|
||||||
<CardDescription>5.342.34.234</CardDescription>
|
<CardDescription>5.342.34.234</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<pre>{JSON.stringify(node, null, 2)}</pre>
|
<pre>{JSON.stringify(server, null, 2)}</pre>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
|
@@ -32,10 +32,23 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog'
|
||||||
|
|
||||||
import { SystemRecord } from '@/types'
|
import { SystemRecord } from '@/types'
|
||||||
import { MoreHorizontal, ArrowUpDown, Copy, RefreshCcw } from 'lucide-react'
|
import { MoreHorizontal, ArrowUpDown, Copy, RefreshCcw, Eye } from 'lucide-react'
|
||||||
import { Link } from 'wouter-preact'
|
import { useMemo, useState } from 'preact/hooks'
|
||||||
import { useState } from 'preact/hooks'
|
import { navigate } from 'wouter-preact/use-browser-location'
|
||||||
|
import { $servers, pb } from '@/lib/stores'
|
||||||
|
import { useStore } from '@nanostores/preact'
|
||||||
|
|
||||||
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||||
const val = info.getValue() as number
|
const val = info.getValue() as number
|
||||||
@@ -73,76 +86,91 @@ function sortableHeader(column: Column<SystemRecord, unknown>, name: string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable({ data }: { data: SystemRecord[] }) {
|
export function DataTable() {
|
||||||
const columns: ColumnDef<SystemRecord>[] = [
|
const data = useStore($servers)
|
||||||
{
|
const [liveUpdates, setLiveUpdates] = useState(true)
|
||||||
// size: 70,
|
const [deleteServer, setDeleteServer] = useState({} as SystemRecord)
|
||||||
accessorKey: 'name',
|
|
||||||
cell: (info) => (
|
|
||||||
<span className="flex gap-2 items-center text-base">
|
|
||||||
{info.getValue() as string}{' '}
|
|
||||||
<button
|
|
||||||
title={`Copy "${info.getValue() as string}" to clipboard`}
|
|
||||||
class="opacity-50 hover:opacity-70 active:opacity-100 duration-75"
|
|
||||||
onClick={() => navigator.clipboard.writeText(info.getValue() as string)}
|
|
||||||
>
|
|
||||||
<Copy className="h-3.5 w-3.5 " />
|
|
||||||
</button>
|
|
||||||
{/* </Button> */}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
header: ({ column }) => sortableHeader(column, 'Node'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'stats.cpu',
|
|
||||||
cell: CellFormatter,
|
|
||||||
header: ({ column }) => sortableHeader(column, 'CPU'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'stats.memPct',
|
|
||||||
cell: CellFormatter,
|
|
||||||
header: ({ column }) => sortableHeader(column, 'Memory'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'stats.diskPct',
|
|
||||||
cell: CellFormatter,
|
|
||||||
header: ({ column }) => sortableHeader(column, 'Disk'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
size: 32,
|
|
||||||
maxSize: 32,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const system = row.original
|
|
||||||
|
|
||||||
return (
|
const columns: ColumnDef<SystemRecord>[] = useMemo(
|
||||||
<div class={'flex justify-end'}>
|
() => [
|
||||||
<DropdownMenu>
|
{
|
||||||
<DropdownMenuTrigger asChild>
|
// size: 70,
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
accessorKey: 'name',
|
||||||
<span className="sr-only">Open menu</span>
|
cell: (info) => (
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<span className="flex gap-2 items-center text-base">
|
||||||
</Button>
|
{info.getValue() as string}{' '}
|
||||||
</DropdownMenuTrigger>
|
<button
|
||||||
<DropdownMenuContent align="end">
|
title={`Copy "${info.getValue() as string}" to clipboard`}
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
class="opacity-50 hover:opacity-70 active:opacity-100 duration-75"
|
||||||
<DropdownMenuItem>
|
onClick={() => navigator.clipboard.writeText(info.getValue() as string)}
|
||||||
<Link class="w-full" href={`/server/${system.name}`}>
|
>
|
||||||
View details
|
<Copy className="h-3.5 w-3.5 " />
|
||||||
</Link>
|
</button>
|
||||||
</DropdownMenuItem>
|
{/* </Button> */}
|
||||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(system.id)}>
|
</span>
|
||||||
Copy IP address
|
),
|
||||||
</DropdownMenuItem>
|
header: ({ column }) => sortableHeader(column, 'Server'),
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>Delete node</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
]
|
accessorKey: 'stats.cpu',
|
||||||
|
cell: CellFormatter,
|
||||||
|
header: ({ column }) => sortableHeader(column, 'CPU'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'stats.memPct',
|
||||||
|
cell: CellFormatter,
|
||||||
|
header: ({ column }) => sortableHeader(column, 'Memory'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'stats.diskPct',
|
||||||
|
cell: CellFormatter,
|
||||||
|
header: ({ column }) => sortableHeader(column, 'Disk'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
size: 32,
|
||||||
|
maxSize: 32,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const system = row.original
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={'flex justify-end'}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
navigate(`/server/${system.name}`)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View details
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(system.id)}>
|
||||||
|
Copy IP address
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
setDeleteServer(system)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete server
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([])
|
const [sorting, setSorting] = useState<SortingState>([])
|
||||||
|
|
||||||
@@ -172,16 +200,32 @@ export function DataTable({ data }: { data: SystemRecord[] }) {
|
|||||||
onChange={(event: Event) => table.getColumn('name')?.setFilterValue(event.target.value)}
|
onChange={(event: Event) => table.getColumn('name')?.setFilterValue(event.target.value)}
|
||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
<Button
|
<div className="ml-auto flex gap-3">
|
||||||
variant="outline"
|
{liveUpdates || (
|
||||||
onClick={() => {
|
<Button
|
||||||
alert('todo: refresh')
|
variant="outline"
|
||||||
}}
|
onClick={() => {
|
||||||
className="ml-auto flex gap-2"
|
alert('todo: refresh')
|
||||||
>
|
}}
|
||||||
<RefreshCcw className="h-4 w-4" />
|
className="flex gap-2"
|
||||||
Refresh
|
>
|
||||||
</Button>
|
Refresh
|
||||||
|
<RefreshCcw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setLiveUpdates(!liveUpdates)
|
||||||
|
}}
|
||||||
|
className="flex gap-2"
|
||||||
|
>
|
||||||
|
Live Updates
|
||||||
|
<div
|
||||||
|
className={`h-2.5 w-2.5 rounded-full ${liveUpdates ? 'bg-green-500' : 'bg-red-500'}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -203,7 +247,7 @@ export function DataTable({ data }: { data: SystemRecord[] }) {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{table.getRowModel().rows?.length ? (
|
{table.getRowModel().rows?.length ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
<TableRow key={row.original.id} data-state={row.getIsSelected() && 'selected'}>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell key={cell.id} style={{ width: `${cell.column.getSize()}px` }}>
|
<TableCell key={cell.id} style={{ width: `${cell.column.getSize()}px` }}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
@@ -221,6 +265,32 @@ export function DataTable({ data }: { data: SystemRecord[] }) {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
<AlertDialog open={deleteServer?.name}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you sure you want to delete {deleteServer.name}?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete all current records for{' '}
|
||||||
|
<code class={'bg-muted rounded-sm px-1'}>{deleteServer.name}</code> from the database.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => setDeleteServer({} as SystemRecord)}>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteServer({} as SystemRecord)
|
||||||
|
pb.collection('systems').delete(deleteServer.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
139
site/src/components/ui/alert-dialog.tsx
Normal file
139
site/src/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName =
|
||||||
|
AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"mt-2 sm:mt-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
}
|
@@ -1,3 +1,14 @@
|
|||||||
import PocketBase from 'pocketbase'
|
import PocketBase from 'pocketbase'
|
||||||
|
import { atom } from 'nanostores'
|
||||||
|
import { SystemRecord } from '@/types'
|
||||||
|
|
||||||
export const pb = new PocketBase('/')
|
export const pb = new PocketBase('/')
|
||||||
|
// @ts-ignore
|
||||||
|
pb.authStore.storageKey = 'pb_admin_auth'
|
||||||
|
|
||||||
|
export const $authenticated = atom(pb.authStore.isValid)
|
||||||
|
export const $servers = atom([] as SystemRecord[])
|
||||||
|
|
||||||
|
pb.authStore.onChange(() => {
|
||||||
|
$authenticated.set(pb.authStore.isValid)
|
||||||
|
})
|
||||||
|
@@ -4,42 +4,71 @@ import { Link, Route, Switch } from 'wouter-preact'
|
|||||||
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 { pb } from './lib/stores.ts'
|
import { $authenticated, $servers, pb } from './lib/stores.ts'
|
||||||
import { ServerDetail } from './components/routes/server.tsx'
|
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-dialog.tsx'
|
import { CommandPalette } from './components/command-dialog.tsx'
|
||||||
|
import { cn } from './lib/utils.ts'
|
||||||
|
import { buttonVariants } from './components/ui/button.tsx'
|
||||||
|
import { Github } from 'lucide-react'
|
||||||
|
import { useStore } from '@nanostores/preact'
|
||||||
|
import { useEffect } from 'preact/hooks'
|
||||||
|
import { SystemRecord } from './types'
|
||||||
|
|
||||||
// import { ModeToggle } from './components/mode-toggle.tsx'
|
const App = () => {
|
||||||
|
const authenticated = useStore($authenticated)
|
||||||
|
|
||||||
// const ls = localStorage.getItem('auth')
|
return <ThemeProvider>{authenticated ? <Main /> : <LoginPage />}</ThemeProvider>
|
||||||
// console.log('ls', ls)
|
}
|
||||||
// @ts-ignore
|
|
||||||
pb.authStore.storageKey = 'pb_admin_auth'
|
|
||||||
|
|
||||||
console.log('pb.authStore', pb.authStore)
|
const Main = () => {
|
||||||
|
// const servers = useStore($servers)
|
||||||
|
|
||||||
const App = () => <ThemeProvider>{pb.authStore.isValid ? <Main /> : <LoginPage />}</ThemeProvider>
|
useEffect(() => {
|
||||||
|
console.log('fetching servers')
|
||||||
|
pb.collection<SystemRecord>('systems')
|
||||||
|
.getFullList({ sort: '+name' })
|
||||||
|
.then((records) => {
|
||||||
|
$servers.set(records)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const Main = () => (
|
return (
|
||||||
<div className="container mt-7 mb-14">
|
<div className="container mt-7 mb-14">
|
||||||
<div class="flex mb-4 justify-end">
|
<div class="flex mb-4">
|
||||||
<ModeToggle />
|
{/* <Link
|
||||||
</div>
|
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
||||||
|
href="/"
|
||||||
|
title={'All servers'}
|
||||||
|
>
|
||||||
|
<HomeIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
|
</Link> */}
|
||||||
|
<div className={'flex gap-1 ml-auto'}>
|
||||||
|
<a
|
||||||
|
title={'Github'}
|
||||||
|
href={'https://github.com/henrygd'}
|
||||||
|
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
||||||
|
>
|
||||||
|
<Github className="h-[1.2rem] w-[1.2rem]" />
|
||||||
|
</a>
|
||||||
|
<ModeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Routes below are matched exclusively -
|
Routes below are matched exclusively -
|
||||||
the first matched route gets rendered
|
the first matched route gets rendered
|
||||||
*/}
|
*/}
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={Home} />
|
<Route path="/" component={Home} />
|
||||||
|
|
||||||
<Route path="/server/:name" component={ServerDetail}></Route>
|
<Route path="/server/:name" component={ServerDetail}></Route>
|
||||||
|
|
||||||
{/* Default route in a switch */}
|
|
||||||
<Route>404: No such page!</Route>
|
|
||||||
</Switch>
|
|
||||||
<CommandPalette />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
|
{/* Default route in a switch */}
|
||||||
|
<Route>404: No such page!</Route>
|
||||||
|
</Switch>
|
||||||
|
<CommandPalette />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
render(<App />, document.getElementById('app')!)
|
render(<App />, document.getElementById('app')!)
|
||||||
|
Reference in New Issue
Block a user