mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
react + updates
This commit is contained in:
26
main.go
26
main.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
site/bun.lockb
BIN
site/bun.lockb
Binary file not shown.
@@ -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",
|
||||||
|
@@ -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'
|
||||||
|
|
||||||
|
export function AddServerButton() {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const port = useRef() as MutableRefObject<HTMLInputElement>
|
||||||
|
const publicKey = useStore($publicKey)
|
||||||
|
|
||||||
function copyDockerCompose(port: string) {
|
function copyDockerCompose(port: string) {
|
||||||
console.log('copying docker compose')
|
copyToClipboard(`services:
|
||||||
navigator.clipboard.writeText(`services:
|
|
||||||
agent:
|
agent:
|
||||||
image: 'henrygd/monitor-agent'
|
image: 'henrygd/monitor-agent'
|
||||||
container_name: 'monitor-agent'
|
container_name: 'monitor-agent'
|
||||||
@@ -29,14 +35,22 @@ function copyDockerCompose(port: string) {
|
|||||||
- /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>
|
||||||
|
@@ -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('/')
|
||||||
|
@@ -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>
|
||||||
|
@@ -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 (
|
||||||
|
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -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}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
127
site/src/components/ui/toast.tsx
Normal file
127
site/src/components/ui/toast.tsx
Normal 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,
|
||||||
|
}
|
33
site/src/components/ui/toaster.tsx
Normal file
33
site/src/components/ui/toaster.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
192
site/src/components/ui/use-toast.ts
Normal file
192
site/src/components/ui/use-toast.ts
Normal 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 }
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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>
|
||||||
|
)
|
||||||
|
@@ -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,
|
||||||
|
@@ -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',
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user