feat: edit system dialog in dashboard

This commit is contained in:
henrygd
2025-02-11 14:12:25 -05:00
parent a94cfff965
commit 6b2a9463ca
4 changed files with 211 additions and 172 deletions

View File

@@ -237,8 +237,8 @@ func (h *Hub) Run() {
oldRecord := newRecord.Original() oldRecord := newRecord.Original()
newStatus := newRecord.GetString("status") newStatus := newRecord.GetString("status")
// if system is disconnected and connection exists, remove it // if system is not up and connection exists, remove it
if newStatus == "down" || newStatus == "paused" { if newStatus != "up" {
h.deleteSystemConnection(newRecord) h.deleteSystemConnection(newRecord)
} }

View File

@@ -16,15 +16,46 @@ import { Label } from "@/components/ui/label"
import { $publicKey, pb } from "@/lib/stores" import { $publicKey, pb } from "@/lib/stores"
import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils" import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils"
import { i18n } from "@lingui/core" import { i18n } from "@lingui/core"
import { Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { ChevronDownIcon, Copy, PlusIcon } from "lucide-react" import { ChevronDownIcon, Copy, PlusIcon } from "lucide-react"
import { MutableRefObject, useRef, useState } from "react" import { memo, MutableRefObject, useRef, useState } from "react"
import { basePath, navigate } from "./router" import { basePath, navigate } from "./router"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"
import { SystemRecord } from "@/types"
export function AddSystemButton({ className }: { className?: string }) { export function AddSystemButton({ className }: { className?: string }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
let opened = useRef(false)
if (open) {
opened.current = true
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
className={cn("flex gap-1 max-xs:h-[2.4rem]", className, isReadOnlyUser() && "hidden")}
>
<PlusIcon className="h-4 w-4 -ms-1" />
<Trans>
Add <span className="hidden sm:inline">System</span>
</Trans>
</Button>
</DialogTrigger>
{opened.current && <SystemDialog setOpen={setOpen} />}
</Dialog>
)
}
/**
* SystemDialog component for adding or editing a system.
* @param {Object} props - The component props.
* @param {function} props.setOpen - Function to set the open state of the dialog.
* @param {SystemRecord} [props.system] - Optional system record for editing an existing system.
*/
export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean) => void; system?: SystemRecord }) => {
const port = useRef() as MutableRefObject<HTMLInputElement> const port = useRef() as MutableRefObject<HTMLInputElement>
const publicKey = useStore($publicKey) const publicKey = useStore($publicKey)
@@ -43,6 +74,7 @@ export function AddSystemButton({ className }: { className?: string }) {
PORT: ${port} PORT: ${port}
KEY: "${publicKey}"`) KEY: "${publicKey}"`)
} }
function copyDockerRun(port: string) { function copyDockerRun(port: string) {
copyToClipboard( copyToClipboard(
`docker run -d --name beszel-agent --network host --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock:ro -e KEY="${publicKey}" -e PORT=${port} henrygd/beszel-agent:latest` `docker run -d --name beszel-agent --network host --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock:ro -e KEY="${publicKey}" -e PORT=${port} henrygd/beszel-agent:latest`
@@ -65,7 +97,11 @@ export function AddSystemButton({ className }: { className?: string }) {
data.users = pb.authStore.record!.id data.users = pb.authStore.record!.id
try { try {
setOpen(false) setOpen(false)
if (system) {
await pb.collection("systems").update(system.id, { ...data, status: "pending" })
} else {
await pb.collection("systems").create(data) await pb.collection("systems").create(data)
}
navigate(basePath) navigate(basePath)
// console.log(record) // console.log(record)
} catch (e) { } catch (e) {
@@ -74,23 +110,11 @@ export function AddSystemButton({ className }: { className?: string }) {
} }
return ( return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
className={cn("flex gap-1 max-xs:h-[2.4rem]", className, isReadOnlyUser() && "hidden")}
>
<PlusIcon className="h-4 w-4 -ms-1" />
<Trans>
Add <span className="hidden sm:inline">System</span>
</Trans>
</Button>
</DialogTrigger>
<DialogContent className="w-[90%] sm:w-auto sm:ns-dialog max-w-full rounded-lg"> <DialogContent className="w-[90%] sm:w-auto sm:ns-dialog max-w-full rounded-lg">
<Tabs defaultValue="docker"> <Tabs defaultValue="docker">
<DialogHeader> <DialogHeader>
<DialogTitle className="mb-2"> <DialogTitle className="mb-2">
<Trans>Add New System</Trans> {system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
</DialogTitle> </DialogTitle>
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="docker">Docker</TabsTrigger> <TabsTrigger value="docker">Docker</TabsTrigger>
@@ -99,8 +123,8 @@ export function AddSystemButton({ className }: { className?: string }) {
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</DialogHeader> </DialogHeader>
{/* Docker */} {/* Docker (set tab index to prevent auto focusing content in edit system dialog) */}
<TabsContent value="docker"> <TabsContent value="docker" tabIndex={-1}>
<DialogDescription className="mb-4 leading-normal w-0 min-w-full"> <DialogDescription className="mb-4 leading-normal w-0 min-w-full">
<Trans> <Trans>
The agent must be running on the system to connect. Copy the The agent must be running on the system to connect. Copy the
@@ -121,20 +145,20 @@ export function AddSystemButton({ className }: { className?: string }) {
<Label htmlFor="name" className="xs:text-end"> <Label htmlFor="name" className="xs:text-end">
<Trans>Name</Trans> <Trans>Name</Trans>
</Label> </Label>
<Input id="name" name="name" className="" required /> <Input id="name" name="name" defaultValue={system?.name} required />
<Label htmlFor="host" className="xs:text-end"> <Label htmlFor="host" className="xs:text-end">
<Trans>Host / IP</Trans> <Trans>Host / IP</Trans>
</Label> </Label>
<Input id="host" name="host" className="" required /> <Input id="host" name="host" defaultValue={system?.host} required />
<Label htmlFor="port" className="xs:text-end"> <Label htmlFor="port" className="xs:text-end">
<Trans>Port</Trans> <Trans>Port</Trans>
</Label> </Label>
<Input ref={port} name="port" id="port" defaultValue="45876" className="" required /> <Input ref={port} name="port" id="port" defaultValue={system?.port || "45876"} required />
<Label htmlFor="pkey" className="xs:text-end whitespace-pre"> <Label htmlFor="pkey" className="xs:text-end whitespace-pre">
<Trans comment="Use 'Key' if your language requires many more characters">Public Key</Trans> <Trans comment="Use 'Key' if your language requires many more characters">Public Key</Trans>
</Label> </Label>
<div className="relative"> <div className="relative">
<Input readOnly id="pkey" value={publicKey} className="" required></Input> <Input readOnly id="pkey" value={publicKey} required></Input>
<div <div
className={ className={
"h-6 w-24 bg-gradient-to-r rtl:bg-gradient-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none" "h-6 w-24 bg-gradient-to-r rtl:bg-gradient-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none"
@@ -161,9 +185,9 @@ export function AddSystemButton({ className }: { className?: string }) {
</TooltipProvider> </TooltipProvider>
</div> </div>
</div> </div>
<DialogFooter className="flex justify-end gap-x-2 gap-y-3 flex-col mt-5">
{/* Docker */} {/* Docker */}
<TabsContent value="docker"> <TabsContent value="docker" className="contents">
<DialogFooter className="flex justify-end gap-x-2 gap-y-3 flex-col">
<div className="flex gap-0 rounded-lg"> <div className="flex gap-0 rounded-lg">
<Button <Button
type="button" type="button"
@@ -187,25 +211,18 @@ export function AddSystemButton({ className }: { className?: string }) {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
<Button>
<Trans>Add system</Trans>
</Button>
</DialogFooter>
</TabsContent> </TabsContent>
{/* Binary */} {/* Binary */}
<TabsContent value="binary"> <TabsContent value="binary" className="contents">
<DialogFooter className="flex justify-end gap-x-2 gap-y-3 flex-col">
<Button type="button" variant="outline" onClick={() => copyInstallCommand(port.current.value)}> <Button type="button" variant="outline" onClick={() => copyInstallCommand(port.current.value)}>
<Trans>Copy Linux command</Trans> <Trans>Copy Linux command</Trans>
</Button> </Button>
<Button>
<Trans>Add system</Trans>
</Button>
</DialogFooter>
</TabsContent> </TabsContent>
{/* Save */}
<Button>{system ? <Trans>Save system</Trans> : <Trans>Add system</Trans>}</Button>
</DialogFooter>
</form> </form>
</Tabs> </Tabs>
</DialogContent> </DialogContent>
</Dialog>
) )
} })

View File

@@ -37,7 +37,6 @@ import {
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { SystemRecord } from "@/types" import { SystemRecord } from "@/types"
@@ -59,8 +58,9 @@ import {
ArrowUpIcon, ArrowUpIcon,
Settings2Icon, Settings2Icon,
EyeIcon, EyeIcon,
PenBoxIcon,
} from "lucide-react" } from "lucide-react"
import { useEffect, useMemo, useState } from "react" import { memo, useEffect, useMemo, useRef, useState } from "react"
import { $hubVersion, $systems, pb } from "@/lib/stores" import { $hubVersion, $systems, pb } from "@/lib/stores"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils" import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
@@ -73,6 +73,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui
import { Input } from "../ui/input" import { Input } from "../ui/input"
import { ClassValue } from "clsx" import { ClassValue } from "clsx"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
import { SystemDialog } from "../add-system"
import { Dialog } from "../ui/dialog"
type ViewMode = "table" | "grid" type ViewMode = "table" | "grid"
@@ -559,11 +561,15 @@ function IndicatorDot({ system, className }: { system: SystemRecord; className?:
) )
} }
function ActionsButton({ system }: { system: SystemRecord }) { const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
// const [opened, setOpened] = useState(false) const [deleteOpen, setDeleteOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
let editOpened = useRef(false)
const { id, status, host, name } = system const { id, status, host, name } = system
return ( return (
<AlertDialog> <>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size={"icon"} data-nolink> <Button variant="ghost" size={"icon"} data-nolink>
@@ -574,6 +580,17 @@ function ActionsButton({ system }: { system: SystemRecord }) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
{!isReadOnlyUser() && (
<DropdownMenuItem
onSelect={() => {
editOpened.current = true
setEditOpen(true)
}}
>
<PenBoxIcon className="me-2.5 size-4" />
<Trans>Edit</Trans>
</DropdownMenuItem>
)}
<DropdownMenuItem <DropdownMenuItem
className={cn(isReadOnlyUser() && "hidden")} className={cn(isReadOnlyUser() && "hidden")}
onClick={() => { onClick={() => {
@@ -599,14 +616,18 @@ function ActionsButton({ system }: { system: SystemRecord }) {
<Trans>Copy host</Trans> <Trans>Copy host</Trans>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} /> <DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
<AlertDialogTrigger asChild> <DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")} onSelect={() => setDeleteOpen(true)}>
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")}>
<Trash2Icon className="me-2.5 size-4" /> <Trash2Icon className="me-2.5 size-4" />
<Trans>Delete</Trans> <Trans>Delete</Trans>
</DropdownMenuItem> </DropdownMenuItem>
</AlertDialogTrigger>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* edit dialog */}
<Dialog open={editOpen} onOpenChange={setEditOpen}>
{editOpened.current && <SystemDialog system={system} setOpen={setEditOpen} />}
</Dialog>
{/* deletion dialog */}
<AlertDialog open={deleteOpen} onOpenChange={(open) => setDeleteOpen(open)}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
@@ -632,5 +653,6 @@ function ActionsButton({ system }: { system: SystemRecord }) {
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</>
) )
} })

View File

@@ -53,7 +53,7 @@ export const updateSystemList = (() => {
try { try {
const records = await pb const records = await pb
.collection<SystemRecord>("systems") .collection<SystemRecord>("systems")
.getFullList({ sort: "+name", fields: "id,name,host,info,status" }) .getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
if (records.length) { if (records.length) {
$systems.set(records) $systems.set(records)