mirror of
https://github.com/fankes/beszel.git
synced 2025-10-20 02:09:28 +08:00
feat: edit system dialog in dashboard
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
@@ -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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user