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": {
|
||||
"@nanostores/preact": "^0.5.1",
|
||||
"@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",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Database, Home, Server } from 'lucide-react'
|
||||
import { Database, Github, Home, Server } from 'lucide-react'
|
||||
|
||||
import {
|
||||
CommandDialog,
|
||||
@@ -14,9 +14,12 @@ import {
|
||||
} from '@/components/ui/command'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { navigate } from 'wouter-preact/use-browser-location'
|
||||
import { useStore } from '@nanostores/preact'
|
||||
import { $servers } from '@/lib/stores'
|
||||
|
||||
export function CommandPalette() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const servers = useStore($servers)
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
@@ -45,6 +48,7 @@ export function CommandPalette() {
|
||||
>
|
||||
<Home className="mr-2 h-4 w-4" />
|
||||
<span>Home</span>
|
||||
<CommandShortcut>⌘H</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
@@ -55,27 +59,31 @@ export function CommandPalette() {
|
||||
<span>PocketBase</span>
|
||||
<CommandShortcut>⌘P</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Systems">
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
navigate('/server/kagemusha')
|
||||
setOpen((open) => !open)
|
||||
window.location.href = 'https://github.com/henrygd'
|
||||
}}
|
||||
>
|
||||
<Server className="mr-2 h-4 w-4" />
|
||||
<span>Kagemusha</span>
|
||||
</CommandItem>
|
||||
<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>
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
<span>Documentation</span>
|
||||
<CommandShortcut>⌘D</CommandShortcut>
|
||||
</CommandItem>
|
||||
</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>
|
||||
</CommandDialog>
|
||||
)
|
||||
|
@@ -11,7 +11,11 @@ export default function LoginPage() {
|
||||
<div className="lg:p-8">
|
||||
<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">
|
||||
<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>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enter your email to sign in to your account
|
||||
@@ -33,19 +37,12 @@ export default function LoginPage() {
|
||||
}}
|
||||
></div>
|
||||
<div class="relative z-20 flex gap-2 items-center text-lg font-medium ml-auto">
|
||||
Melon
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
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>
|
||||
placeholder
|
||||
<img
|
||||
className={'w-6 h-6'}
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Numelon_Logo.png/240px-Numelon_Logo.png"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
{/* <div class="relative z-20 mt-auto">
|
||||
<blockquote class="space-y-2">
|
||||
|
@@ -1,47 +1,56 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { pb } from '@/lib/stores'
|
||||
import { SystemRecord } from '@/types'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
import { $servers, pb } from '@/lib/stores'
|
||||
import { DataTable } from '../server-table/data-table'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
||||
import { useStore } from '@nanostores/preact'
|
||||
import { SystemRecord } from '@/types'
|
||||
|
||||
export function Home() {
|
||||
const [systems, setSystems] = useState([] as SystemRecord[])
|
||||
const servers = useStore($servers)
|
||||
// const [systems, setSystems] = useState([] as SystemRecord[])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Home'
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
pb.collection<SystemRecord>('systems')
|
||||
.getFullList({
|
||||
sort: 'name',
|
||||
})
|
||||
.then((items) => {
|
||||
setSystems(items)
|
||||
})
|
||||
console.log('servers', servers)
|
||||
}, [servers])
|
||||
|
||||
// pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
||||
// setSystems((curSystems) => {
|
||||
// const i = curSystems.findIndex((s) => s.id === e.record.id)
|
||||
// if (i > -1) {
|
||||
// const newSystems = [...curSystems]
|
||||
// newSystems[i] = e.record
|
||||
// return newSystems
|
||||
// } else {
|
||||
// return [...curSystems, e.record]
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// return () => pb.collection('systems').unsubscribe('*')
|
||||
useEffect(() => {
|
||||
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
||||
const curServers = $servers.get()
|
||||
const newServers = []
|
||||
console.log('e', e)
|
||||
if (e.action === 'delete') {
|
||||
for (const server of curServers) {
|
||||
if (server.id !== e.record.id) {
|
||||
newServers.push(server)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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 (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className={'mb-3'}>All Systems</CardTitle>
|
||||
<CardTitle className={'mb-3'}>All Servers</CardTitle>
|
||||
<CardDescription>
|
||||
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">
|
||||
@@ -51,10 +60,9 @@ export function Home() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable data={systems} />
|
||||
<DataTable />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* <pre>{JSON.stringify(systems, null, 2)}</pre> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui
|
||||
|
||||
export function ServerDetail() {
|
||||
const [_, params] = useRoute('/server/:name')
|
||||
const [node, setNode] = useState({} as SystemRecord)
|
||||
const [server, setServer] = useState({} as SystemRecord)
|
||||
|
||||
useEffect(() => {
|
||||
document.title = params!.name
|
||||
@@ -16,7 +16,7 @@ export function ServerDetail() {
|
||||
pb.collection<SystemRecord>('systems')
|
||||
.getFirstListItem(`name="${params!.name}"`)
|
||||
.then((record) => {
|
||||
setNode(record)
|
||||
setServer(record)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,11 +24,11 @@ export function ServerDetail() {
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className={'mb-3'}>{node.name}</CardTitle>
|
||||
<CardTitle className={'mb-3'}>{server.name}</CardTitle>
|
||||
<CardDescription>5.342.34.234</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre>{JSON.stringify(node, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(server, null, 2)}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
|
@@ -32,10 +32,23 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
import { SystemRecord } from '@/types'
|
||||
import { MoreHorizontal, ArrowUpDown, Copy, RefreshCcw } from 'lucide-react'
|
||||
import { Link } from 'wouter-preact'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { MoreHorizontal, ArrowUpDown, Copy, RefreshCcw, Eye } from 'lucide-react'
|
||||
import { useMemo, 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>) {
|
||||
const val = info.getValue() as number
|
||||
@@ -73,76 +86,91 @@ function sortableHeader(column: Column<SystemRecord, unknown>, name: string) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DataTable({ data }: { data: SystemRecord[] }) {
|
||||
const columns: ColumnDef<SystemRecord>[] = [
|
||||
{
|
||||
// size: 70,
|
||||
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
|
||||
export function DataTable() {
|
||||
const data = useStore($servers)
|
||||
const [liveUpdates, setLiveUpdates] = useState(true)
|
||||
const [deleteServer, setDeleteServer] = useState({} as SystemRecord)
|
||||
|
||||
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>
|
||||
<Link class="w-full" href={`/server/${system.name}`}>
|
||||
View details
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(system.id)}>
|
||||
Copy IP address
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Delete node</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
const columns: ColumnDef<SystemRecord>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
// size: 70,
|
||||
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, 'Server'),
|
||||
},
|
||||
},
|
||||
]
|
||||
{
|
||||
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>([])
|
||||
|
||||
@@ -172,16 +200,32 @@ export function DataTable({ data }: { data: SystemRecord[] }) {
|
||||
onChange={(event: Event) => table.getColumn('name')?.setFilterValue(event.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
alert('todo: refresh')
|
||||
}}
|
||||
className="ml-auto flex gap-2"
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<div className="ml-auto flex gap-3">
|
||||
{liveUpdates || (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
alert('todo: refresh')
|
||||
}}
|
||||
className="flex gap-2"
|
||||
>
|
||||
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 className="rounded-md border">
|
||||
<Table>
|
||||
@@ -203,7 +247,7 @@ export function DataTable({ data }: { data: SystemRecord[] }) {
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
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) => (
|
||||
<TableCell key={cell.id} style={{ width: `${cell.column.getSize()}px` }}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
@@ -221,6 +265,32 @@ export function DataTable({ data }: { data: SystemRecord[] }) {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
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 { atom } from 'nanostores'
|
||||
import { SystemRecord } from '@/types'
|
||||
|
||||
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 { ThemeProvider } from './components/theme-provider.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 { ModeToggle } from './components/mode-toggle.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')
|
||||
// console.log('ls', ls)
|
||||
// @ts-ignore
|
||||
pb.authStore.storageKey = 'pb_admin_auth'
|
||||
return <ThemeProvider>{authenticated ? <Main /> : <LoginPage />}</ThemeProvider>
|
||||
}
|
||||
|
||||
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 = () => (
|
||||
<div className="container mt-7 mb-14">
|
||||
<div class="flex mb-4 justify-end">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
return (
|
||||
<div className="container mt-7 mb-14">
|
||||
<div class="flex mb-4">
|
||||
{/* <Link
|
||||
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 -
|
||||
the first matched route gets rendered
|
||||
*/}
|
||||
<Switch>
|
||||
<Route path="/" component={Home} />
|
||||
<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 />
|
||||
</div>
|
||||
)
|
||||
<Route path="/server/:name" component={ServerDetail}></Route>
|
||||
|
||||
{/* Default route in a switch */}
|
||||
<Route>404: No such page!</Route>
|
||||
</Switch>
|
||||
<CommandPalette />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
render(<App />, document.getElementById('app')!)
|
||||
|
Reference in New Issue
Block a user