diff --git a/site/bun.lockb b/site/bun.lockb index 3763ac4..c35f27c 100755 Binary files a/site/bun.lockb and b/site/bun.lockb differ diff --git a/site/package.json b/site/package.json index 2290537..36c6572 100644 --- a/site/package.json +++ b/site/package.json @@ -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", diff --git a/site/src/components/command-dialog.tsx b/site/src/components/command-dialog.tsx index 2881ec3..90d0cbf 100644 --- a/site/src/components/command-dialog.tsx +++ b/site/src/components/command-dialog.tsx @@ -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 + ⌘H { @@ -55,27 +59,31 @@ export function CommandPalette() { PocketBase ⌘P - - - { - navigate('/server/kagemusha') - setOpen((open) => !open) + window.location.href = 'https://github.com/henrygd' }} > - - Kagemusha - - navigate('/server/rashomon')}> - - Rashomon - - navigate('/server/ikiru')}> - - Ikiru + + Documentation + ⌘D + + + {servers.map((server) => ( + { + navigate(`/server/${server.name}`) + setOpen((open) => !open) + }} + > + + {server.name} + + ))} + ) diff --git a/site/src/components/login.tsx b/site/src/components/login.tsx index afea7ea..7041337 100644 --- a/site/src/components/login.tsx +++ b/site/src/components/login.tsx @@ -11,7 +11,11 @@ export default function LoginPage() {
- + {/* */}

Welcome back

Enter your email to sign in to your account @@ -33,19 +37,12 @@ export default function LoginPage() { }} >

- Melon - - - + placeholder +
{/*
diff --git a/site/src/components/routes/home.tsx b/site/src/components/routes/home.tsx index 9c18991..fb78072 100644 --- a/site/src/components/routes/home.tsx +++ b/site/src/components/routes/home.tsx @@ -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('systems') - .getFullList({ - sort: 'name', - }) - .then((items) => { - setSystems(items) - }) + console.log('servers', servers) + }, [servers]) - // pb.collection('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('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 ( <> - All Systems + All Servers Press{' '} @@ -51,10 +60,9 @@ export function Home() { - + - {/*
{JSON.stringify(systems, null, 2)}
*/} ) } diff --git a/site/src/components/routes/server.tsx b/site/src/components/routes/server.tsx index 72e62f0..f4e4ed5 100644 --- a/site/src/components/routes/server.tsx +++ b/site/src/components/routes/server.tsx @@ -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('systems') .getFirstListItem(`name="${params!.name}"`) .then((record) => { - setNode(record) + setServer(record) }) }) @@ -24,11 +24,11 @@ export function ServerDetail() { <> - {node.name} + {server.name} 5.342.34.234 -
{JSON.stringify(node, null, 2)}
+
{JSON.stringify(server, null, 2)}
diff --git a/site/src/components/server-table/data-table.tsx b/site/src/components/server-table/data-table.tsx index 101160b..b5ac83f 100644 --- a/site/src/components/server-table/data-table.tsx +++ b/site/src/components/server-table/data-table.tsx @@ -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) { const val = info.getValue() as number @@ -73,76 +86,91 @@ function sortableHeader(column: Column, name: string) { ) } -export function DataTable({ data }: { data: SystemRecord[] }) { - const columns: ColumnDef[] = [ - { - // size: 70, - accessorKey: 'name', - cell: (info) => ( - - {info.getValue() as string}{' '} - - {/* */} - - ), - 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 ( -
- - - - - - Actions - - - View details - - - navigator.clipboard.writeText(system.id)}> - Copy IP address - - - Delete node - - -
- ) + const columns: ColumnDef[] = useMemo( + () => [ + { + // size: 70, + accessorKey: 'name', + cell: (info) => ( + + {info.getValue() as string}{' '} + + {/* */} + + ), + 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 ( +
+ + + + + + Actions + { + navigate(`/server/${system.name}`) + }} + > + View details + + navigator.clipboard.writeText(system.id)}> + Copy IP address + + + { + setDeleteServer(system) + }} + > + Delete server + + + +
+ ) + }, + }, + ], + [] + ) const [sorting, setSorting] = useState([]) @@ -172,16 +200,32 @@ export function DataTable({ data }: { data: SystemRecord[] }) { onChange={(event: Event) => table.getColumn('name')?.setFilterValue(event.target.value)} className="max-w-sm" /> - +
+ {liveUpdates || ( + + )} + +
@@ -203,7 +247,7 @@ export function DataTable({ data }: { data: SystemRecord[] }) { {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -221,6 +265,32 @@ export function DataTable({ data }: { data: SystemRecord[] }) {
+ + + + + Are you sure you want to delete {deleteServer.name}? + + + This action cannot be undone. This will permanently delete all current records for{' '} + {deleteServer.name} from the database. + + + + setDeleteServer({} as SystemRecord)}> + Cancel + + { + setDeleteServer({} as SystemRecord) + pb.collection('systems').delete(deleteServer.id) + }} + > + Continue + + + +
) } diff --git a/site/src/components/ui/alert-dialog.tsx b/site/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..8722561 --- /dev/null +++ b/site/src/components/ui/alert-dialog.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/site/src/lib/stores.ts b/site/src/lib/stores.ts index 165c7dc..ab469ee 100644 --- a/site/src/lib/stores.ts +++ b/site/src/lib/stores.ts @@ -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) +}) diff --git a/site/src/main.tsx b/site/src/main.tsx index 0497a9e..cbafbde 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -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 {authenticated ?
: } +} -console.log('pb.authStore', pb.authStore) +const Main = () => { + // const servers = useStore($servers) -const App = () => {pb.authStore.isValid ?
: } + useEffect(() => { + console.log('fetching servers') + pb.collection('systems') + .getFullList({ sort: '+name' }) + .then((records) => { + $servers.set(records) + }) + }, []) -const Main = () => ( -
-
- -
+ return ( +
+
+ {/* + + */} +
+ + + + +
+
- {/* + {/* Routes below are matched exclusively - the first matched route gets rendered */} - - + + - - - {/* Default route in a switch */} - 404: No such page! - - -
-) + + {/* Default route in a switch */} + 404: No such page! + + +
+ ) +} render(, document.getElementById('app')!)