react + updates

This commit is contained in:
Henry Dollman
2024-07-10 12:54:02 -04:00
parent 41f5b2a49f
commit 7e69e1665d
19 changed files with 579 additions and 241 deletions

26
main.go
View File

@@ -112,7 +112,7 @@ func main() {
// start ticker for server updates // start ticker for server updates
app.OnBeforeServe().Add(func(e *core.ServeEvent) error { app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// go serverUpdateTicker() go serverUpdateTicker()
return nil return nil
}) })
@@ -128,7 +128,7 @@ func main() {
} }
func serverUpdateTicker() { func serverUpdateTicker() {
ticker := time.NewTicker(15 * time.Second) ticker := time.NewTicker(60 * time.Second)
for range ticker.C { for range ticker.C {
updateServers() updateServers()
} }
@@ -144,7 +144,7 @@ func updateServers() {
records := []*models.Record{} records := []*models.Record{}
if err := query.All(&records); err != nil { if err := query.All(&records); err != nil {
app.Logger().Error("Failed to get servers: ", "err", err) app.Logger().Error("Failed to get servers: ", "err", err.Error())
// return nil, err // return nil, err
} }
@@ -166,7 +166,7 @@ func updateServer(record *models.Record) {
} }
client, err := getServerConnection(&server) client, err := getServerConnection(&server)
if err != nil { if err != nil {
app.Logger().Error("Failed to connect:", "err", err, "server", server.Ip, "port", server.Port) app.Logger().Error("Failed to connect:", "err", err.Error(), "server", server.Ip, "port", server.Port)
// todo update record to not connected // todo update record to not connected
record.Set("active", false) record.Set("active", false)
delete(serverConnections, record.Id) delete(serverConnections, record.Id)
@@ -178,7 +178,7 @@ func updateServer(record *models.Record) {
// get server stats from agent // get server stats from agent
systemData, err := requestJson(&server) systemData, err := requestJson(&server)
if err != nil { if err != nil {
app.Logger().Error("Failed to get server stats: ", "err", err) app.Logger().Error("Failed to get server stats: ", "err", err.Error())
record.Set("active", false) record.Set("active", false)
if server.Client != nil { if server.Client != nil {
server.Client.Close() server.Client.Close()
@@ -190,7 +190,7 @@ func updateServer(record *models.Record) {
record.Set("active", true) record.Set("active", true)
record.Set("stats", systemData.System) record.Set("stats", systemData.System)
if err := app.Dao().SaveRecord(record); err != nil { if err := app.Dao().SaveRecord(record); err != nil {
app.Logger().Error("Failed to update record: ", "err", err) app.Logger().Error("Failed to update record: ", "err", err.Error())
} }
// add new system_stats record // add new system_stats record
system_stats, _ := app.Dao().FindCollectionByNameOrId("system_stats") system_stats, _ := app.Dao().FindCollectionByNameOrId("system_stats")
@@ -198,7 +198,7 @@ func updateServer(record *models.Record) {
system_stats_record.Set("system", record.Id) system_stats_record.Set("system", record.Id)
system_stats_record.Set("stats", systemData.System) system_stats_record.Set("stats", systemData.System)
if err := app.Dao().SaveRecord(system_stats_record); err != nil { if err := app.Dao().SaveRecord(system_stats_record); err != nil {
app.Logger().Error("Failed to save record: ", "err", err) app.Logger().Error("Failed to save record: ", "err", err.Error())
} }
// add new container_stats record // add new container_stats record
if len(systemData.Containers) > 0 { if len(systemData.Containers) > 0 {
@@ -207,7 +207,7 @@ func updateServer(record *models.Record) {
container_stats_record.Set("system", record.Id) container_stats_record.Set("system", record.Id)
container_stats_record.Set("stats", systemData.Containers) container_stats_record.Set("stats", systemData.Containers)
if err := app.Dao().SaveRecord(container_stats_record); err != nil { if err := app.Dao().SaveRecord(container_stats_record); err != nil {
app.Logger().Error("Failed to save record: ", "err", err) app.Logger().Error("Failed to save record: ", "err", err.Error())
} }
} }
} }
@@ -216,7 +216,7 @@ func getServerConnection(server *Server) (*ssh.Client, error) {
// app.Logger().Debug("new ssh connection", "server", server.Ip) // app.Logger().Debug("new ssh connection", "server", server.Ip)
key, err := getSSHKey() key, err := getSSHKey()
if err != nil { if err != nil {
app.Logger().Error("Failed to get SSH key: ", "err", err) app.Logger().Error("Failed to get SSH key: ", "err", err.Error())
return nil, err return nil, err
} }
time.Sleep(time.Second) time.Sleep(time.Second)
@@ -284,27 +284,27 @@ func getSSHKey() ([]byte, error) {
// Generate the Ed25519 key pair // Generate the Ed25519 key pair
pubKey, privKey, err := ed25519.GenerateKey(nil) pubKey, privKey, err := ed25519.GenerateKey(nil)
if err != nil { if err != nil {
// app.Logger().Error("Error generating key pair:", "err", err) // app.Logger().Error("Error generating key pair:", "err", err.Error())
return nil, err return nil, err
} }
// Get the private key in OpenSSH format // Get the private key in OpenSSH format
privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "") privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "")
if err != nil { if err != nil {
// app.Logger().Error("Error marshaling private key:", "err", err) // app.Logger().Error("Error marshaling private key:", "err", err.Error())
return nil, err return nil, err
} }
// Save the private key to a file // Save the private key to a file
privateFile, err := os.Create("./pb_data/id_ed25519") privateFile, err := os.Create("./pb_data/id_ed25519")
if err != nil { if err != nil {
// app.Logger().Error("Error creating private key file:", "err", err) // app.Logger().Error("Error creating private key file:", "err", err.Error())
return nil, err return nil, err
} }
defer privateFile.Close() defer privateFile.Close()
if err := pem.Encode(privateFile, privKeyBytes); err != nil { if err := pem.Encode(privateFile, privKeyBytes); err != nil {
// app.Logger().Error("Error writing private key to file:", "err", err) // app.Logger().Error("Error writing private key to file:", "err", err.Error())
return nil, err return nil, err
} }

Binary file not shown.

View File

@@ -9,29 +9,33 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@nanostores/preact": "^0.5.1", "@nanostores/react": "^0.7.2",
"@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",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-table": "^8.19.2", "@tanstack/react-table": "^8.19.2",
"@vitejs/plugin-react": "^4.3.1",
"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.401.0",
"nanostores": "^0.10.3", "nanostores": "^0.10.3",
"pocketbase": "^0.21.3", "pocketbase": "^0.21.3",
"preact": "^10.22.1", "react": "^18.3.1",
"react-dom": "^18.3.1",
"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-preact": "^3.3.1" "wouter": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.8.3",
"@types/bun": "^1.1.6", "@types/bun": "^1.1.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"postcss": "^8.4.39", "postcss": "^8.4.39",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",

View File

@@ -14,11 +14,17 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { $publicKey, pb } from '@/lib/stores' import { $publicKey, pb } from '@/lib/stores'
import { ClipboardIcon, Plus } from 'lucide-react' import { ClipboardIcon, Plus } from 'lucide-react'
import { MutableRef, useRef, useState } from 'preact/hooks' import { useState, useRef, MutableRefObject, useEffect } from 'react'
import { useStore } from '@nanostores/react'
import { copyToClipboard } from '@/lib/utils'
function copyDockerCompose(port: string) { export function AddServerButton() {
console.log('copying docker compose') const [open, setOpen] = useState(false)
navigator.clipboard.writeText(`services: const port = useRef() as MutableRefObject<HTMLInputElement>
const publicKey = useStore($publicKey)
function copyDockerCompose(port: string) {
copyToClipboard(`services:
agent: agent:
image: 'henrygd/monitor-agent' image: 'henrygd/monitor-agent'
container_name: 'monitor-agent' container_name: 'monitor-agent'
@@ -27,16 +33,24 @@ function copyDockerCompose(port: string) {
- '${port}:45876' - '${port}:45876'
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock`) - /var/run/docker.sock:/var/run/docker.sock`)
} }
export function AddServerButton() { useEffect(() => {
const [open, setOpen] = useState(false) if (publicKey || !open) {
const port = useRef() as MutableRef<HTMLInputElement> return
}
// get public key
pb.send('/getkey', {}).then(({ key }) => {
console.log('key', key)
$publicKey.set(key)
})
}, [open])
async function handleSubmit(e: SubmitEvent) { async function handleSubmit(e: SubmitEvent) {
e.preventDefault() e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement) const formData = new FormData(e.target as HTMLFormElement)
const stats = { const data = Object.fromEntries(formData) as Record<string, any>
data.stats = {
cpu: 0, cpu: 0,
mem: 0, mem: 0,
memUsed: 0, memUsed: 0,
@@ -45,16 +59,10 @@ export function AddServerButton() {
diskUsed: 0, diskUsed: 0,
diskPct: 0, diskPct: 0,
} }
const data = { stats } as Record<string, any>
for (const [key, value] of formData) {
data[key.slice(2)] = value
}
console.log(data)
try { try {
const record = await pb.collection('systems').create(data)
console.log(record)
setOpen(false) setOpen(false)
await pb.collection('systems').create(data)
// console.log(record)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
@@ -73,57 +81,56 @@ export function AddServerButton() {
<DialogTitle>Add New Server</DialogTitle> <DialogTitle>Add New Server</DialogTitle>
<DialogDescription> <DialogDescription>
The agent must be running on the server to connect. Copy the{' '} The agent must be running on the server to connect. Copy the{' '}
<code class="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the agent below. <code className="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the agent
below.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form name="testing" action="/" onSubmit={handleSubmit}> <form name="testing" action="/" onSubmit={handleSubmit as any}>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label for="s-name" className="text-right"> <Label htmlFor="name" className="text-right">
Name Name
</Label> </Label>
<Input id="s-name" name="s-name" className="col-span-3" required /> <Input id="name" name="name" className="col-span-3" required />
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label for="s-ip" className="text-right"> <Label htmlFor="ip" className="text-right">
IP Address IP Address
</Label> </Label>
<Input id="s-ip" name="s-ip" className="col-span-3" required /> <Input id="ip" name="ip" className="col-span-3" required />
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label for="s-port" className="text-right"> <Label htmlFor="port" className="text-right">
Port Port
</Label> </Label>
<Input <Input
ref={port} ref={port}
name="s-port" name="port"
id="s-port" id="port"
defaultValue="45876" defaultValue="45876"
className="col-span-3" className="col-span-3"
required required
/> />
</div> </div>
<div className="grid grid-cols-4 items-center gap-4 relative"> <div className="grid grid-cols-4 items-center gap-4 relative">
<Label for="s-port" className="text-right"> <Label htmlFor="pkey" className="text-right">
Public Key Public Key
</Label> </Label>
<Input <Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
readonly
name="s-port"
id="s-port"
value={$publicKey.get()}
className="col-span-3"
required
></Input>
<div <div
className={ className={
'h-6 w-28 bg-gradient-to-r from-transparent to-background to-70% absolute right-1 pointer-events-none' 'h-6 w-28 bg-gradient-to-r from-transparent to-background to-70% absolute right-1 pointer-events-none'
} }
></div> ></div>
<TooltipProvider className="z-10"> <TooltipProvider delayDuration={100}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button type="button" variant={'link'} className="absolute right-0 z-50"> <Button
type="button"
variant={'link'}
className="absolute right-0"
onClick={() => copyToClipboard(publicKey)}
>
<ClipboardIcon className="h-4 w-4 " /> <ClipboardIcon className="h-4 w-4 " />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>

View File

@@ -12,9 +12,9 @@ import {
CommandSeparator, CommandSeparator,
CommandShortcut, CommandShortcut,
} from '@/components/ui/command' } from '@/components/ui/command'
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'react'
import { navigate } from 'wouter-preact/use-browser-location' import { navigate } from 'wouter/use-browser-location'
import { useStore } from '@nanostores/preact' import { useStore } from '@nanostores/react'
import { $servers } from '@/lib/stores' import { $servers } from '@/lib/stores'
export function CommandPalette() { export function CommandPalette() {
@@ -24,7 +24,6 @@ export function CommandPalette() {
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
console.log('open')
e.preventDefault() e.preventDefault()
setOpen((open) => !open) setOpen((open) => !open)
} }
@@ -39,7 +38,7 @@ export function CommandPalette() {
<CommandInput placeholder="Type a command or search..." /> <CommandInput placeholder="Type a command or search..." />
<CommandList> <CommandList>
<CommandEmpty>No results found.</CommandEmpty> <CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions"> <CommandGroup heading="Suggestions0">
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
navigate('/') navigate('/')

View File

@@ -1,4 +1,4 @@
import { Link } from 'wouter-preact' 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'
@@ -29,14 +29,14 @@ export default function LoginPage() {
</p> </p>
</div> </div>
</div> </div>
<div class="relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex"> <div className="relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex">
<div <div
class="absolute inset-0 bg-slate-900 bg-cover opacity-80" className="absolute inset-0 bg-slate-900 bg-cover opacity-80"
style={{ style={{
backgroundImage: `url(https://directus.cloud/assets/waves.2b156907.svg)`, backgroundImage: `url(https://directus.cloud/assets/waves.2b156907.svg)`,
}} }}
></div> ></div>
<div class="relative z-20 flex gap-2 items-center text-lg font-medium ml-auto"> <div className="relative z-20 flex gap-2 items-center text-lg font-medium ml-auto">
placeholder placeholder
<img <img
className={'w-6 h-6'} className={'w-6 h-6'}
@@ -44,13 +44,13 @@ export default function LoginPage() {
alt="" alt=""
/> />
</div> </div>
{/* <div class="relative z-20 mt-auto"> {/* <div className="relative z-20 mt-auto">
<blockquote class="space-y-2"> <blockquote className="space-y-2">
<p class="text-lg"> <p className="text-lg">
“This library has saved me countless hours of work and helped me deliver stunning “This library has saved me countless hours of work and helped me deliver stunning
designs to my clients faster than ever before.” designs to my clients faster than ever before.”
</p> </p>
<footer class="text-sm">Sofia Davis</footer> <footer className="text-sm">Sofia Davis</footer>
</blockquote> </blockquote>
</div> */} </div> */}
</div> </div>

View File

@@ -1,8 +1,8 @@
import { useEffect } from 'preact/hooks' import { 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 { useStore } from '@nanostores/preact' import { useStore } from '@nanostores/react'
import { SystemRecord } from '@/types' import { SystemRecord } from '@/types'
export function Home() { export function Home() {
@@ -43,7 +43,9 @@ export function Home() {
} }
$servers.set(newServers) $servers.set(newServers)
}) })
return () => pb.collection('systems').unsubscribe('*') return () => {
pb.collection('systems').unsubscribe('*')
}
}, []) }, [])
return ( return (

View File

@@ -1,7 +1,7 @@
import { pb } from '@/lib/stores' import { pb } from '@/lib/stores'
import { SystemRecord } from '@/types' import { SystemRecord } from '@/types'
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'react'
import { useRoute } from 'wouter-preact' import { useRoute } from 'wouter'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
export function ServerDetail() { export function ServerDetail() {
@@ -18,7 +18,7 @@ export function ServerDetail() {
.then((record) => { .then((record) => {
setServer(record) setServer(record)
}) })
}) }, [])
return ( return (
<> <>

View File

@@ -45,13 +45,13 @@ import {
import { SystemRecord } from '@/types' import { SystemRecord } from '@/types'
import { MoreHorizontal, ArrowUpDown, Copy, RefreshCcw } from 'lucide-react' import { MoreHorizontal, ArrowUpDown, Copy, RefreshCcw } from 'lucide-react'
import { useMemo, useState } from 'preact/hooks' import { useMemo, useState } from 'react'
import { navigate } from 'wouter-preact/use-browser-location' import { navigate } from 'wouter/use-browser-location'
import { $servers, pb } from '@/lib/stores' import { $servers, pb } from '@/lib/stores'
import { useStore } from '@nanostores/preact' import { useStore } from '@nanostores/react'
import { AddServerButton } from '../add-server' import { AddServerButton } from '../add-server'
import clsx from 'clsx' import clsx from 'clsx'
import { cn } from '@/lib/utils' import { cn, copyToClipboard } from '@/lib/utils'
function CellFormatter(info: CellContext<SystemRecord, unknown>) { function CellFormatter(info: CellContext<SystemRecord, unknown>) {
const val = info.getValue() as number const val = info.getValue() as number
@@ -62,14 +62,14 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
color = 'yellow' color = 'yellow'
} }
return ( return (
<div class="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<span class="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={clsx('absolute inset-0 w-full h-full origin-left', `bg-${color}-500`)}
style={{ transform: `scalex(${val}%)` }} style={{ transform: `scalex(${val}%)` }}
></span> ></span>
</span> </span>
<span class="w-16">{val.toFixed(2)}%</span> <span className="w-16">{val.toFixed(2)}%</span>
</div> </div>
) )
} }
@@ -100,7 +100,7 @@ export function DataTable() {
// size: 70, // size: 70,
accessorKey: 'name', accessorKey: 'name',
cell: (info) => ( cell: (info) => (
<span className="flex gap-1.5 items-center text-base"> <span className="flex gap-1 items-center text-base">
<span <span
className={clsx( className={clsx(
'w-2.5 h-2.5 block left-0 rounded-full', 'w-2.5 h-2.5 block left-0 rounded-full',
@@ -108,14 +108,14 @@ export function DataTable() {
)} )}
style={{ marginBottom: '-1px' }} style={{ marginBottom: '-1px' }}
></span> ></span>
{info.getValue() as string} <Button
<button variant={'ghost'}
title={`Copy "${info.getValue() as string}" to clipboard`} className="text-foreground/80 h-7 px-2 gap-1.5"
class="opacity-50 hover:opacity-70 active:opacity-100 duration-75" onClick={() => copyToClipboard(info.getValue() as string)}
onClick={() => navigator.clipboard.writeText(info.getValue() as string)}
> >
<Copy className="h-3.5 w-3.5 " /> {info.getValue() as string}
</button> <Copy className="h-3.5 w-3.5 opacity-70" />
</Button>
</span> </span>
), ),
header: ({ column }) => sortableHeader(column, 'Server'), header: ({ column }) => sortableHeader(column, 'Server'),
@@ -143,7 +143,7 @@ export function DataTable() {
const system = row.original const system = row.original
return ( return (
<div class={'flex justify-end'}> <div className={'flex justify-end'}>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0"> <Button variant="ghost" className="h-8 w-8 p-0">
@@ -288,7 +288,8 @@ export function DataTable() {
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This action cannot be undone. This will permanently delete all current records for{' '} 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. <code className={'bg-muted rounded-sm px-1'}>{deleteServer.name}</code> from the
database.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>

View File

@@ -1,23 +0,0 @@
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
export function TooltipDemo() {
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger>
<Button
onMouseEnter={() => console.log('hovered')}
onMouseLeave={() => console.log('unhovered')}
variant="outline"
>
Hover
</Button>
</TooltipTrigger>
<TooltipContent hideWhenDetached={true} className="pointer-events-none">
<p>Add to library</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

View File

@@ -1,8 +1,8 @@
import * as React from "react" import * as React from 'react'
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from "lucide-react" import { X } from 'lucide-react'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( 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", 'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className className
)} )}
{...props} {...props}
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( 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", 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card 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 className
)} )}
{...props} {...props}
@@ -51,33 +51,18 @@ const DialogContent = React.forwardRef<
)) ))
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
) )
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = 'DialogHeader'
const DialogFooter = ({ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props} {...props}
/> />
) )
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = 'DialogFooter'
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@@ -85,10 +70,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn( className={cn('text-lg font-semibold leading-none tracking-tight', className)}
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props} {...props}
/> />
)) ))
@@ -100,7 +82,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn('text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ))

View File

@@ -0,0 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@@ -0,0 +1,33 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@@ -0,0 +1,192 @@
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

View File

@@ -3,22 +3,22 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 7.69% 97.45%; --background: 30 8% 97.45%;
--foreground: 0 0% 0%; --foreground: 30 0% 0%;
--card: 0 0% 100%; --card: 30 0% 100%;
--card-foreground: 240 6.67% 2.94%; --card-foreground: 240 6.67% 2.94%;
--popover: 0 0% 100%; --popover: 30 0% 100%;
--popover-foreground: 240 10% 3.92%; --popover-foreground: 240 10% 6.2%;
--primary: 240 5.88% 10%; --primary: 240 5.88% 10%;
--primary-foreground: 0 0% 100%; --primary-foreground: 30 0% 100%;
--secondary: 240 4.76% 95.88%; --secondary: 240 4.76% 95.88%;
--secondary-foreground: 240 5.88% 10%; --secondary-foreground: 240 5.88% 10%;
--muted: 26 6% 90%; --muted: 26 6% 94%;
--muted-foreground: 24 2.79% 35.1%; --muted-foreground: 24 2.79% 35.1%;
--accent: 20 23.08% 93%; --accent: 20 23.08% 94%;
--accent-foreground: 240 5.88% 10%; --accent-foreground: 240 5.88% 10%;
--destructive: 0 65.33% 44.12%; --destructive: 30 67% 46%;
--destructive-foreground: 0 0% 98.04%; --destructive-foreground: 30 0% 98.04%;
--border: 30 8.11% 85.49%; --border: 30 8.11% 85.49%;
--input: 30 4.29% 72.55%; --input: 30 4.29% 72.55%;
--ring: 30 3.97% 49.41%; --ring: 30 3.97% 49.41%;
@@ -26,11 +26,11 @@
} }
.dark { .dark {
--background: 240 10% 3.92%; --background: 240 10% 6.2%;
--foreground: 0 0% 98.04%; --foreground: 0 0% 98.04%;
--card: 240 8.57% 6.86%; --card: 240 8.57% 8%;
--card-foreground: 0 0% 98.04%; --card-foreground: 0 0% 98.04%;
--popover: 240 10% 3.92%; --popover: 240 10% 6.2%;
--popover-foreground: 0 0% 98.04%; --popover-foreground: 0 0% 98.04%;
--primary: 0 0% 98.04%; --primary: 0 0% 98.04%;
--primary-foreground: 240 5.88% 10%; --primary-foreground: 240 5.88% 10%;
@@ -42,9 +42,9 @@
--accent-foreground: 0 0% 98.04%; --accent-foreground: 0 0% 98.04%;
--destructive: 0 56.48% 42.35%; --destructive: 0 56.48% 42.35%;
--destructive-foreground: 0 0% 98.04%; --destructive-foreground: 0 0% 98.04%;
--border: 240 2.86% 13.73%; --border: 240 2.86% 14%;
--input: 240 3.7% 15.88%; --input: 240 3.7% 15.88%;
--ring: 240 4.88% 83.92%; --ring: 240 4.88% 86%;
--radius: 0.8rem; --radius: 0.8rem;
} }
} }

View File

@@ -1,6 +1,23 @@
import { type ClassValue, clsx } from "clsx" import { toast } from '@/components/ui/use-toast'
import { twMerge } from "tailwind-merge" import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export async function copyToClipboard(content: string) {
const duration = 1500
try {
await navigator.clipboard.writeText(content)
toast({
duration,
description: 'Copied to clipboard',
})
} catch (e: any) {
toast({
duration,
description: 'Failed to copy',
})
}
}

View File

@@ -1,6 +1,7 @@
import './index.css' import './index.css'
import { render } from 'preact' import React, { useEffect } from 'react'
import { Route, Switch } from 'wouter-preact' 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'
@@ -11,9 +12,9 @@ import { CommandPalette } from './components/command-dialog.tsx'
import { cn } from './lib/utils.ts' import { cn } 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/preact' import { useStore } from '@nanostores/react'
import { useEffect } from 'preact/hooks'
import { SystemRecord } from './types' import { SystemRecord } from './types'
import { Toaster } from './components/ui/toaster.tsx'
const App = () => { const App = () => {
const authenticated = useStore($authenticated) const authenticated = useStore($authenticated)
@@ -22,25 +23,18 @@ const App = () => {
} }
const Main = () => { const Main = () => {
// const servers = useStore($servers)
useEffect(() => {
// get servers // get servers
useEffect(() => {
pb.collection<SystemRecord>('systems') pb.collection<SystemRecord>('systems')
.getFullList({ sort: '+name' }) .getFullList({ sort: '+name' })
.then((records) => { .then((records) => {
$servers.set(records) $servers.set(records)
}) })
// get public key
pb.send('/getkey', {}).then(({ key }) => {
console.log('key', key)
})
}, []) }, [])
return ( return (
<div className="container mt-7 mb-14"> <div className="container mt-7 mb-14">
<div class="flex mb-4"> <div className="flex mb-4">
{/* <Link {/* <Link
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))} className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
href="/" href="/"
@@ -73,7 +67,13 @@ const Main = () => {
<Route>404: No such page!</Route> <Route>404: No such page!</Route>
</Switch> </Switch>
<CommandPalette /> <CommandPalette />
<Toaster />
</div> </div>
) )
} }
render(<App />, document.getElementById('app')!)
ReactDOM.createRoot(document.getElementById('app')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

View File

@@ -9,8 +9,6 @@
"skipLibCheck": true, "skipLibCheck": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"],
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
@@ -22,7 +20,6 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "preact",
/* Linting */ /* Linting */
"strict": true, "strict": true,

View File

@@ -1,9 +1,9 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import preact from '@preact/preset-vite' import react from '@vitejs/plugin-react'
import path from 'path' import path from 'path'
export default defineConfig({ export default defineConfig({
plugins: [preact()], plugins: [react()],
esbuild: { esbuild: {
legalComments: 'external', legalComments: 'external',
}, },