From e3954f7915c9bb3df08e6b1c4db2056e28c71fe8 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 10 Sep 2026 01:16:55 -0600 Subject: [PATCH] feat(sandbox): sandboxes list and detail UI --- .../dashboard/sandbox/add-sandbox.tsx | 383 ++++++++++++++++++ .../sandbox/extend-sandbox-timeout.tsx | 120 ++++++ .../dashboard/sandbox/sandbox-console.tsx | 185 +++++++++ .../sandbox/sandbox-status-badge.tsx | 16 + .../dashboard/sandbox/show-sandboxes.tsx | 134 ++++++ .../environment/[environmentId].tsx | 9 + .../[environmentId]/sandboxes/[sandboxId].tsx | 230 +++++++++++ .../[environmentId]/sandboxes/index.tsx | 135 ++++++ 8 files changed, 1212 insertions(+) create mode 100644 apps/dokploy/components/dashboard/sandbox/add-sandbox.tsx create mode 100644 apps/dokploy/components/dashboard/sandbox/extend-sandbox-timeout.tsx create mode 100644 apps/dokploy/components/dashboard/sandbox/sandbox-console.tsx create mode 100644 apps/dokploy/components/dashboard/sandbox/sandbox-status-badge.tsx create mode 100644 apps/dokploy/components/dashboard/sandbox/show-sandboxes.tsx create mode 100644 apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/sandboxes/[sandboxId].tsx create mode 100644 apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/sandboxes/index.tsx diff --git a/apps/dokploy/components/dashboard/sandbox/add-sandbox.tsx b/apps/dokploy/components/dashboard/sandbox/add-sandbox.tsx new file mode 100644 index 000000000..98aceb7ba --- /dev/null +++ b/apps/dokploy/components/dashboard/sandbox/add-sandbox.tsx @@ -0,0 +1,383 @@ +import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; +import { Box, PlusIcon } from "lucide-react"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/utils/api"; + +const schema = z + .object({ + name: z.string().max(64).optional(), + source: z.enum(["template", "image"]), + template: z.enum(["base", "python", "node"]), + image: z.string().max(255).optional(), + serverId: z.string().nullable(), + cpu: z.number().min(0.1).max(64), + memoryMb: z.number().int().min(64).max(262_144), + timeoutMinutes: z.number().min(1).max(1440), + networkMode: z.enum(["isolated", "internet"]), + envVars: z.string().optional(), + }) + .refine((value) => value.source === "template" || !!value.image?.trim(), { + message: "Image is required", + path: ["image"], + }); + +type FormValues = z.infer; + +interface Props { + environmentId: string; + projectId: string; +} + +export const AddSandbox = ({ environmentId, projectId }: Props) => { + const router = useRouter(); + const utils = api.useUtils(); + const [open, setOpen] = useState(false); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const { data: webServerSettings } = + api.settings.getWebServerSettings.useQuery(); + const { data: servers } = api.server.withSSHKey.useQuery(); + const { data: templates } = api.sandbox.templates.useQuery(); + const { mutateAsync, isPending, error } = api.sandbox.create.useMutation(); + + const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly; + + const form = useForm({ + defaultValues: { + name: "", + source: "template", + template: "python", + image: "", + serverId: null, + cpu: 1, + memoryMb: 512, + timeoutMinutes: 5, + networkMode: "isolated", + envVars: "", + }, + resolver: zodResolver(schema), + }); + const source = form.watch("source"); + + const onSubmit = async (values: FormValues) => { + try { + const sandbox = await mutateAsync({ + environmentId, + name: values.name?.trim() || undefined, + template: values.source === "template" ? values.template : undefined, + image: values.source === "image" ? values.image?.trim() : undefined, + serverId: values.serverId, + cpu: values.cpu, + memoryMb: values.memoryMb, + timeoutMs: Math.round(values.timeoutMinutes * 60_000), + networkMode: values.networkMode, + envVars: values.envVars?.trim() || undefined, + }); + toast.success("Sandbox created"); + await utils.sandbox.list.invalidate({ environmentId }); + setOpen(false); + form.reset(); + router.push( + `/dashboard/project/${projectId}/environment/${environmentId}/sandboxes/${sandbox.sandboxId}`, + ); + } catch { + toast.error("Error creating the sandbox"); + } + }; + + return ( + + + + + + + + + New Sandbox + + + An ephemeral, resource-limited container you can drive through the + API. It is removed automatically when its timeout expires. + + + {error && {error.message}} +
+ + ( + + Name + + + + + + )} + /> + + ( + + Image + + + Template + Custom image + + + + )} + /> + + {source === "template" ? ( + ( + + + + + )} + /> + ) : ( + ( + + + + + + The image must contain a POSIX shell (sh) and tail. + + + + )} + /> + )} + + {(servers?.length ?? 0) > 0 && ( + ( + + Server + + + + )} + /> + )} + +
+ ( + + CPU cores + + + field.onChange(event.target.valueAsNumber) + } + /> + + + + )} + /> + ( + + Memory (MB) + + + field.onChange(event.target.valueAsNumber) + } + /> + + + + )} + /> + ( + + Timeout (minutes) + + + field.onChange(event.target.valueAsNumber) + } + /> + + + + )} + /> +
+ + ( + + Network + + + Sandboxes are never attached to dokploy-network or any + application network. + + + + )} + /> + + ( + + Environment variables + +