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
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// go serverUpdateTicker()
go serverUpdateTicker()
return nil
})
@@ -128,7 +128,7 @@ func main() {
}
func serverUpdateTicker() {
ticker := time.NewTicker(15 * time.Second)
ticker := time.NewTicker(60 * time.Second)
for range ticker.C {
updateServers()
}
@@ -144,7 +144,7 @@ func updateServers() {
records := []*models.Record{}
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
}
@@ -166,7 +166,7 @@ func updateServer(record *models.Record) {
}
client, err := getServerConnection(&server)
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
record.Set("active", false)
delete(serverConnections, record.Id)
@@ -178,7 +178,7 @@ func updateServer(record *models.Record) {
// get server stats from agent
systemData, err := requestJson(&server)
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)
if server.Client != nil {
server.Client.Close()
@@ -190,7 +190,7 @@ func updateServer(record *models.Record) {
record.Set("active", true)
record.Set("stats", systemData.System)
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
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("stats", systemData.System)
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
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("stats", systemData.Containers)
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)
key, err := getSSHKey()
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
}
time.Sleep(time.Second)
@@ -284,27 +284,27 @@ func getSSHKey() ([]byte, error) {
// Generate the Ed25519 key pair
pubKey, privKey, err := ed25519.GenerateKey(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
}
// Get the private key in OpenSSH format
privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "")
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
}
// Save the private key to a file
privateFile, err := os.Create("./pb_data/id_ed25519")
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
}
defer privateFile.Close()
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
}

Binary file not shown.

View File

@@ -9,29 +9,33 @@
"preview": "vite preview"
},
"dependencies": {
"@nanostores/preact": "^0.5.1",
"@nanostores/react": "^0.7.2",
"@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",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-table": "^8.19.2",
"@vitejs/plugin-react": "^4.3.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"lucide-react": "^0.401.0",
"nanostores": "^0.10.3",
"pocketbase": "^0.21.3",
"preact": "^10.22.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"valibot": "^0.36.0",
"wouter-preact": "^3.3.1"
"wouter": "^3.3.1"
},
"devDependencies": {
"@preact/preset-vite": "^2.8.3",
"@types/bun": "^1.1.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.4",

View File

@@ -14,29 +14,43 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { $publicKey, pb } from '@/lib/stores'
import { ClipboardIcon, Plus } from 'lucide-react'
import { MutableRef, useRef, useState } from 'preact/hooks'
function copyDockerCompose(port: string) {
console.log('copying docker compose')
navigator.clipboard.writeText(`services:
agent:
image: 'henrygd/monitor-agent'
container_name: 'monitor-agent'
restart: unless-stopped
ports:
- '${port}:45876'
volumes:
- /var/run/docker.sock:/var/run/docker.sock`)
}
import { useState, useRef, MutableRefObject, useEffect } from 'react'
import { useStore } from '@nanostores/react'
import { copyToClipboard } from '@/lib/utils'
export function AddServerButton() {
const [open, setOpen] = useState(false)
const port = useRef() as MutableRef<HTMLInputElement>
const port = useRef() as MutableRefObject<HTMLInputElement>
const publicKey = useStore($publicKey)
function copyDockerCompose(port: string) {
copyToClipboard(`services:
agent:
image: 'henrygd/monitor-agent'
container_name: 'monitor-agent'
restart: unless-stopped
ports:
- '${port}:45876'
volumes:
- /var/run/docker.sock:/var/run/docker.sock`)
}
useEffect(() => {
if (publicKey || !open) {
return
}
// get public key
pb.send('/getkey', {}).then(({ key }) => {
console.log('key', key)
$publicKey.set(key)
})
}, [open])
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
const stats = {
const data = Object.fromEntries(formData) as Record<string, any>
data.stats = {
cpu: 0,
mem: 0,
memUsed: 0,
@@ -45,16 +59,10 @@ export function AddServerButton() {
diskUsed: 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 {
const record = await pb.collection('systems').create(data)
console.log(record)
setOpen(false)
await pb.collection('systems').create(data)
// console.log(record)
} catch (e) {
console.log(e)
}
@@ -73,57 +81,56 @@ export function AddServerButton() {
<DialogTitle>Add New Server</DialogTitle>
<DialogDescription>
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>
</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 grid-cols-4 items-center gap-4">
<Label for="s-name" className="text-right">
<Label htmlFor="name" className="text-right">
Name
</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 className="grid grid-cols-4 items-center gap-4">
<Label for="s-ip" className="text-right">
<Label htmlFor="ip" className="text-right">
IP Address
</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 className="grid grid-cols-4 items-center gap-4">
<Label for="s-port" className="text-right">
<Label htmlFor="port" className="text-right">
Port
</Label>
<Input
ref={port}
name="s-port"
id="s-port"
name="port"
id="port"
defaultValue="45876"
className="col-span-3"
required
/>
</div>
<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
</Label>
<Input
readonly
name="s-port"
id="s-port"
value={$publicKey.get()}
className="col-span-3"
required
></Input>
<Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
<div
className={
'h-6 w-28 bg-gradient-to-r from-transparent to-background to-70% absolute right-1 pointer-events-none'
}
></div>
<TooltipProvider className="z-10">
<TooltipProvider delayDuration={100}>
<Tooltip>
<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 " />
</Button>
</TooltipTrigger>

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,13 +45,13 @@ import {
import { SystemRecord } from '@/types'
import { MoreHorizontal, ArrowUpDown, Copy, RefreshCcw } from 'lucide-react'
import { useMemo, useState } from 'preact/hooks'
import { navigate } from 'wouter-preact/use-browser-location'
import { useMemo, useState } from 'react'
import { navigate } from 'wouter/use-browser-location'
import { $servers, pb } from '@/lib/stores'
import { useStore } from '@nanostores/preact'
import { useStore } from '@nanostores/react'
import { AddServerButton } from '../add-server'
import clsx from 'clsx'
import { cn } from '@/lib/utils'
import { cn, copyToClipboard } from '@/lib/utils'
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
const val = info.getValue() as number
@@ -62,14 +62,14 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
color = 'yellow'
}
return (
<div class="flex gap-2 items-center">
<span class="grow block bg-muted h-4 relative rounded-sm overflow-hidden">
<div className="flex gap-2 items-center">
<span className="grow block bg-muted h-4 relative rounded-sm overflow-hidden">
<span
className={clsx('absolute inset-0 w-full h-full origin-left', `bg-${color}-500`)}
style={{ transform: `scalex(${val}%)` }}
></span>
</span>
<span class="w-16">{val.toFixed(2)}%</span>
<span className="w-16">{val.toFixed(2)}%</span>
</div>
)
}
@@ -100,7 +100,7 @@ export function DataTable() {
// size: 70,
accessorKey: 'name',
cell: (info) => (
<span className="flex gap-1.5 items-center text-base">
<span className="flex gap-1 items-center text-base">
<span
className={clsx(
'w-2.5 h-2.5 block left-0 rounded-full',
@@ -108,14 +108,14 @@ export function DataTable() {
)}
style={{ marginBottom: '-1px' }}
></span>
{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)}
<Button
variant={'ghost'}
className="text-foreground/80 h-7 px-2 gap-1.5"
onClick={() => copyToClipboard(info.getValue() as string)}
>
<Copy className="h-3.5 w-3.5 " />
</button>
{info.getValue() as string}
<Copy className="h-3.5 w-3.5 opacity-70" />
</Button>
</span>
),
header: ({ column }) => sortableHeader(column, 'Server'),
@@ -143,7 +143,7 @@ export function DataTable() {
const system = row.original
return (
<div class={'flex justify-end'}>
<div className={'flex justify-end'}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
@@ -288,7 +288,8 @@ export function DataTable() {
</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.
<code className={'bg-muted rounded-sm px-1'}>{deleteServer.name}</code> from the
database.
</AlertDialogDescription>
</AlertDialogHeader>
<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 DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const Dialog = DialogPrimitive.Root
@@ -13,108 +13,90 @@ const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
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}
/>
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'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
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.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}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.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-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
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
const DialogHeader = ({ className, ...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 = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
DialogFooter.displayName = 'DialogFooter'
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

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

View File

@@ -1,6 +1,23 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { toast } from '@/components/ui/use-toast'
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
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 { render } from 'preact'
import { Route, Switch } from 'wouter-preact'
import React, { useEffect } from 'react'
import ReactDOM from 'react-dom/client'
import { Route, Switch } from 'wouter'
import { Home } from './components/routes/home.tsx'
import { ThemeProvider } from './components/theme-provider.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 { buttonVariants } from './components/ui/button.tsx'
import { Github } from 'lucide-react'
import { useStore } from '@nanostores/preact'
import { useEffect } from 'preact/hooks'
import { useStore } from '@nanostores/react'
import { SystemRecord } from './types'
import { Toaster } from './components/ui/toaster.tsx'
const App = () => {
const authenticated = useStore($authenticated)
@@ -22,25 +23,18 @@ const App = () => {
}
const Main = () => {
// const servers = useStore($servers)
// get servers
useEffect(() => {
// get servers
pb.collection<SystemRecord>('systems')
.getFullList({ sort: '+name' })
.then((records) => {
$servers.set(records)
})
// get public key
pb.send('/getkey', {}).then(({ key }) => {
console.log('key', key)
})
}, [])
return (
<div className="container mt-7 mb-14">
<div class="flex mb-4">
<div className="flex mb-4">
{/* <Link
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
href="/"
@@ -73,7 +67,13 @@ const Main = () => {
<Route>404: No such page!</Route>
</Switch>
<CommandPalette />
<Toaster />
</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,
"baseUrl": ".",
"paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"],
"@/*": ["./src/*"]
},
@@ -22,7 +20,6 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
/* Linting */
"strict": true,

View File

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