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()
newStatus := newRecord.GetString("status")
// if system is disconnected and connection exists, remove it
if newStatus == "down" || newStatus == "paused" {
// if system is not up and connection exists, remove it
if newStatus != "up" {
h.deleteSystemConnection(newRecord)
}

View File

@@ -16,15 +16,46 @@ import { Label } from "@/components/ui/label"
import { $publicKey, pb } from "@/lib/stores"
import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils"
import { i18n } from "@lingui/core"
import { Trans } from "@lingui/macro"
import { t, Trans } from "@lingui/macro"
import { useStore } from "@nanostores/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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"
import { SystemRecord } from "@/types"
export function AddSystemButton({ className }: { className?: string }) {
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 publicKey = useStore($publicKey)
@@ -43,6 +74,7 @@ export function AddSystemButton({ className }: { className?: string }) {
PORT: ${port}
KEY: "${publicKey}"`)
}
function copyDockerRun(port: string) {
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`
@@ -65,7 +97,11 @@ export function AddSystemButton({ className }: { className?: string }) {
data.users = pb.authStore.record!.id
try {
setOpen(false)
if (system) {
await pb.collection("systems").update(system.id, { ...data, status: "pending" })
} else {
await pb.collection("systems").create(data)
}
navigate(basePath)
// console.log(record)
} catch (e) {
@@ -74,23 +110,11 @@ export function AddSystemButton({ className }: { className?: string }) {
}
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">
<Tabs defaultValue="docker">
<DialogHeader>
<DialogTitle className="mb-2">
<Trans>Add New System</Trans>
{system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
</DialogTitle>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="docker">Docker</TabsTrigger>
@@ -99,8 +123,8 @@ export function AddSystemButton({ className }: { className?: string }) {
</TabsTrigger>
</TabsList>
</DialogHeader>
{/* Docker */}
<TabsContent value="docker">
{/* Docker (set tab index to prevent auto focusing content in edit system dialog) */}
<TabsContent value="docker" tabIndex={-1}>
<DialogDescription className="mb-4 leading-normal w-0 min-w-full">
<Trans>
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">
<Trans>Name</Trans>
</Label>
<Input id="name" name="name" className="" required />
<Input id="name" name="name" defaultValue={system?.name} required />
<Label htmlFor="host" className="xs:text-end">
<Trans>Host / IP</Trans>
</Label>
<Input id="host" name="host" className="" required />
<Input id="host" name="host" defaultValue={system?.host} required />
<Label htmlFor="port" className="xs:text-end">
<Trans>Port</Trans>
</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">
<Trans comment="Use 'Key' if your language requires many more characters">Public Key</Trans>
</Label>
<div className="relative">
<Input readOnly id="pkey" value={publicKey} className="" required></Input>
<Input readOnly id="pkey" value={publicKey} required></Input>
<div
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"
@@ -161,9 +185,9 @@ export function AddSystemButton({ className }: { className?: string }) {
</TooltipProvider>
</div>
</div>
<DialogFooter className="flex justify-end gap-x-2 gap-y-3 flex-col mt-5">
{/* Docker */}
<TabsContent value="docker">
<DialogFooter className="flex justify-end gap-x-2 gap-y-3 flex-col">
<TabsContent value="docker" className="contents">
<div className="flex gap-0 rounded-lg">
<Button
type="button"
@@ -187,25 +211,18 @@ export function AddSystemButton({ className }: { className?: string }) {
</DropdownMenuContent>
</DropdownMenu>
</div>
<Button>
<Trans>Add system</Trans>
</Button>
</DialogFooter>
</TabsContent>
{/* Binary */}
<TabsContent value="binary">
<DialogFooter className="flex justify-end gap-x-2 gap-y-3 flex-col">
<TabsContent value="binary" className="contents">
<Button type="button" variant="outline" onClick={() => copyInstallCommand(port.current.value)}>
<Trans>Copy Linux command</Trans>
</Button>
<Button>
<Trans>Add system</Trans>
</Button>
</DialogFooter>
</TabsContent>
{/* Save */}
<Button>{system ? <Trans>Save system</Trans> : <Trans>Add system</Trans>}</Button>
</DialogFooter>
</form>
</Tabs>
</DialogContent>
</Dialog>
)
}
})

View File

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

View File

@@ -53,7 +53,7 @@ export const updateSystemList = (() => {
try {
const records = await pb
.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) {
$systems.set(records)