mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-13 18:05:31 +02:00
Compare commits
6 Commits
feat/add-n
...
feat/compo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
385850f354 | ||
|
|
a48306a2c6 | ||
|
|
89737e7b65 | ||
|
|
00c708483e | ||
|
|
ddf570a807 | ||
|
|
f8eb2ba4ba |
@@ -0,0 +1,268 @@
|
|||||||
|
import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
const DockerLogsId = dynamic(
|
||||||
|
() =>
|
||||||
|
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||||
|
(e) => e.DockerLogsId,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
appName: string;
|
||||||
|
serverId?: string;
|
||||||
|
appType: "stack" | "docker-compose";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowComposeContainers = ({
|
||||||
|
appName,
|
||||||
|
appType,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
|
const { data, isPending, refetch } =
|
||||||
|
api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
|
{
|
||||||
|
appName,
|
||||||
|
appType,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!appName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-xl">Containers</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Inspect each container in this compose and run basic lifecycle
|
||||||
|
actions.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 ${isPending ? "animate-spin" : ""}`} />
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isPending ? (
|
||||||
|
<div className="flex items-center justify-center h-[20vh]">
|
||||||
|
<Loader2 className="animate-spin h-6 w-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : !data || data.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-[20vh]">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
No containers found. Deploy the compose to see containers here.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>State</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Container ID</TableHead>
|
||||||
|
<TableHead className="text-right" />
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((container) => (
|
||||||
|
<ContainerRow
|
||||||
|
key={container.containerId}
|
||||||
|
container={container}
|
||||||
|
serverId={serverId}
|
||||||
|
onActionComplete={() => refetch()}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ContainerRowProps {
|
||||||
|
container: {
|
||||||
|
containerId: string;
|
||||||
|
name: string;
|
||||||
|
state: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
serverId?: string;
|
||||||
|
onActionComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContainerRow = ({
|
||||||
|
container,
|
||||||
|
serverId,
|
||||||
|
onActionComplete,
|
||||||
|
}: ContainerRowProps) => {
|
||||||
|
const [logsOpen, setLogsOpen] = useState(false);
|
||||||
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const restartMutation = api.docker.restartContainer.useMutation();
|
||||||
|
const startMutation = api.docker.startContainer.useMutation();
|
||||||
|
const stopMutation = api.docker.stopContainer.useMutation();
|
||||||
|
const killMutation = api.docker.killContainer.useMutation();
|
||||||
|
|
||||||
|
const handleAction = async (
|
||||||
|
action: string,
|
||||||
|
mutationFn: typeof restartMutation,
|
||||||
|
) => {
|
||||||
|
setActionLoading(action);
|
||||||
|
try {
|
||||||
|
await mutationFn.mutateAsync({
|
||||||
|
containerId: container.containerId,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
toast.success(`Container ${action} successfully`);
|
||||||
|
onActionComplete();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
`Failed to ${action} container: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setActionLoading(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="font-medium">{container.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
container.state === "running"
|
||||||
|
? "default"
|
||||||
|
: container.state === "exited"
|
||||||
|
? "secondary"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{container.status}</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm text-muted-foreground">
|
||||||
|
{container.containerId}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Dialog open={logsOpen} onOpenChange={setLogsOpen}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
{actionLoading ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
View Logs
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
disabled={actionLoading !== null}
|
||||||
|
onClick={() => handleAction("restart", restartMutation)}
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
disabled={actionLoading !== null}
|
||||||
|
onClick={() => handleAction("start", startMutation)}
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
disabled={actionLoading !== null}
|
||||||
|
onClick={() => handleAction("stop", stopMutation)}
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer text-red-500 focus:text-red-600"
|
||||||
|
disabled={actionLoading !== null}
|
||||||
|
onClick={() => handleAction("kill", killMutation)}
|
||||||
|
>
|
||||||
|
Kill
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<DialogContent className="sm:max-w-7xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>View Logs</DialogTitle>
|
||||||
|
<DialogDescription>Logs for {container.name}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<DockerLogsId
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={serverId}
|
||||||
|
runType="native"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,563 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Network, Pencil, Plus } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
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 { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
const networkDriverEnum = [
|
|
||||||
"bridge",
|
|
||||||
"host",
|
|
||||||
"overlay",
|
|
||||||
"macvlan",
|
|
||||||
"none",
|
|
||||||
"ipvlan",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/** Sentinel for "no scope" */
|
|
||||||
const SCOPE_EMPTY = "__scope_none__";
|
|
||||||
|
|
||||||
const ipamConfigEntrySchema = z.object({
|
|
||||||
subnet: z.string().optional(),
|
|
||||||
ipRange: z.string().optional(),
|
|
||||||
gateway: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const networkFormSchema = z.object({
|
|
||||||
name: z.string().min(1, "Name is required"),
|
|
||||||
driver: z.enum(networkDriverEnum).default("bridge"),
|
|
||||||
scope: z.string().optional(),
|
|
||||||
serverId: z.string().optional(),
|
|
||||||
internal: z.boolean().default(false),
|
|
||||||
attachable: z.boolean().default(false),
|
|
||||||
ingress: z.boolean().default(false),
|
|
||||||
configOnly: z.boolean().default(false),
|
|
||||||
enableIPv4: z.boolean().default(true),
|
|
||||||
enableIPv6: z.boolean().default(false),
|
|
||||||
ipamDriver: z.string().optional(),
|
|
||||||
ipamConfig: z.array(ipamConfigEntrySchema).default([]),
|
|
||||||
});
|
|
||||||
|
|
||||||
type NetworkFormValues = z.infer<typeof networkFormSchema>;
|
|
||||||
|
|
||||||
const defaultValues: NetworkFormValues = {
|
|
||||||
name: "",
|
|
||||||
driver: "bridge",
|
|
||||||
scope: SCOPE_EMPTY,
|
|
||||||
serverId: undefined,
|
|
||||||
internal: false,
|
|
||||||
attachable: false,
|
|
||||||
ingress: false,
|
|
||||||
configOnly: false,
|
|
||||||
enableIPv4: true,
|
|
||||||
enableIPv6: false,
|
|
||||||
ipamDriver: "",
|
|
||||||
ipamConfig: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
interface HandleNetworkProps {
|
|
||||||
networkId?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const isEdit = !!networkId;
|
|
||||||
|
|
||||||
const { data: servers } = api.server.all.useQuery();
|
|
||||||
const { data: network, isLoading: isLoadingNetwork } =
|
|
||||||
api.network.one.useQuery(
|
|
||||||
{ networkId: networkId! },
|
|
||||||
{ enabled: isEdit && !!networkId },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading: isPending } = networkId
|
|
||||||
? api.network.update.useMutation()
|
|
||||||
: api.network.create.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<NetworkFormValues>({
|
|
||||||
resolver: zodResolver(networkFormSchema),
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ipamConfigFieldArray = useFieldArray({
|
|
||||||
control: form.control,
|
|
||||||
name: "ipamConfig",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEdit && network && isOpen) {
|
|
||||||
const ipam = network.ipam ?? {};
|
|
||||||
const ipamConfigArr = (ipam.config ?? []).map((c) => ({
|
|
||||||
subnet: c.subnet ?? "",
|
|
||||||
ipRange: c.ipRange ?? "",
|
|
||||||
gateway: c.gateway ?? "",
|
|
||||||
}));
|
|
||||||
form.reset({
|
|
||||||
...defaultValues,
|
|
||||||
name: network.name,
|
|
||||||
driver: network.driver,
|
|
||||||
scope: network.scope ?? SCOPE_EMPTY,
|
|
||||||
serverId: network.serverId || undefined,
|
|
||||||
internal: network.internal,
|
|
||||||
attachable: network.attachable,
|
|
||||||
enableIPv4: network.enableIPv4,
|
|
||||||
enableIPv6: network.enableIPv6,
|
|
||||||
ipamDriver: ipam.driver ?? "",
|
|
||||||
ipamConfig: ipamConfigArr,
|
|
||||||
ingress: network.ingress,
|
|
||||||
configOnly: network.configOnly,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [isEdit, isOpen, network, form]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: NetworkFormValues) => {
|
|
||||||
const scope =
|
|
||||||
data.scope && data.scope !== SCOPE_EMPTY ? data.scope : undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mutateAsync({
|
|
||||||
networkId: networkId ?? "",
|
|
||||||
name: data.name,
|
|
||||||
driver: data.driver,
|
|
||||||
scope,
|
|
||||||
serverId: data.serverId || undefined,
|
|
||||||
internal: data.internal,
|
|
||||||
attachable: data.attachable,
|
|
||||||
ingress: data.ingress,
|
|
||||||
configOnly: data.configOnly,
|
|
||||||
enableIPv4: data.enableIPv4,
|
|
||||||
enableIPv6: data.enableIPv6,
|
|
||||||
ipam: {
|
|
||||||
driver: data.ipamDriver,
|
|
||||||
config: data.ipamConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.network.all.invalidate();
|
|
||||||
if (networkId) await utils.network.one.invalidate({ networkId });
|
|
||||||
setIsOpen(false);
|
|
||||||
form.reset(defaultValues);
|
|
||||||
} catch {
|
|
||||||
toast.error(isEdit ? "Error updating network" : "Error creating network");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const trigger =
|
|
||||||
children ??
|
|
||||||
(isEdit ? (
|
|
||||||
<Button size="sm" variant="outline">
|
|
||||||
<Pencil className=" size-4" />
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button>
|
|
||||||
<Plus className=" size-4" />
|
|
||||||
Add network
|
|
||||||
</Button>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center gap-2">
|
|
||||||
<Network className="size-5 text-muted-foreground" />
|
|
||||||
{isEdit ? "Edit network" : "Add network"}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{isEdit
|
|
||||||
? "Update this Docker network. Changes apply to name, driver, and server assignment."
|
|
||||||
: "Create a new Docker network for your organization. You can optionally assign it to a server."}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isEdit && isLoadingNetwork ? (
|
|
||||||
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
|
||||||
Loading network…
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="flex w-full flex-col gap-6"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="my-network" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="driver"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Driver</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select driver" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{networkDriverEnum.map((d) => (
|
|
||||||
<SelectItem key={d} value={d}>
|
|
||||||
{d}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="serverId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Server</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value ?? undefined}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select server" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{!isCloud && (
|
|
||||||
<SelectItem value={undefined}>
|
|
||||||
Dokploy server
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
{servers?.map((server) => (
|
|
||||||
<SelectItem
|
|
||||||
key={server.serverId}
|
|
||||||
value={server.serverId}
|
|
||||||
>
|
|
||||||
{server.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription className="text-muted-foreground">
|
|
||||||
{isCloud
|
|
||||||
? "Server where this network will be created."
|
|
||||||
: "Dokploy server is the default local server; or choose a specific server."}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="scope"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Scope (optional)</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value ?? SCOPE_EMPTY}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select scope" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value={SCOPE_EMPTY}>None</SelectItem>
|
|
||||||
<SelectItem value="local">local</SelectItem>
|
|
||||||
<SelectItem value="swarm">swarm</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="internal"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">Internal</FormLabel>
|
|
||||||
<FormDescription className="text-muted-foreground">
|
|
||||||
Restrict external access; containers on this network
|
|
||||||
cannot reach external networks.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="attachable"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">Attachable</FormLabel>
|
|
||||||
<FormDescription className="text-muted-foreground">
|
|
||||||
Allow standalone containers to attach to this network
|
|
||||||
(e.g. in Swarm, not only services).
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="enableIPv4"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">Enable IPv4</FormLabel>
|
|
||||||
<FormDescription className="text-muted-foreground">
|
|
||||||
Enable IPv4 addressing on the network.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="enableIPv6"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">Enable IPv6</FormLabel>
|
|
||||||
<FormDescription className="text-muted-foreground">
|
|
||||||
Enable IPv6 addressing on the network.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="ingress"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">Ingress</FormLabel>
|
|
||||||
<FormDescription className="text-muted-foreground">
|
|
||||||
Use as the routing-mesh network in Swarm mode (load
|
|
||||||
balancing between nodes).
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="configOnly"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">Config only</FormLabel>
|
|
||||||
<FormDescription className="text-muted-foreground">
|
|
||||||
Create a placeholder network whose config is reused by
|
|
||||||
other networks; cannot run containers on it.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 rounded-lg border p-4">
|
|
||||||
<FormLabel>IPAM</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="ipamDriver"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="text-muted-foreground">
|
|
||||||
Driver (optional)
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="default" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<FormLabel className="text-muted-foreground">
|
|
||||||
Config (subnet / gateway / IP range)
|
|
||||||
</FormLabel>
|
|
||||||
{ipamConfigFieldArray.fields.map((field, index) => (
|
|
||||||
<div key={field.id} className="flex flex-wrap gap-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={`ipamConfig.${index}.subnet`}
|
|
||||||
render={({ field: f }) => (
|
|
||||||
<FormItem className="min-w-[140px] flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...f}
|
|
||||||
placeholder="Subnet (e.g. 172.20.0.0/16)"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={`ipamConfig.${index}.ipRange`}
|
|
||||||
render={({ field: f }) => (
|
|
||||||
<FormItem className="min-w-[120px] flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Input {...f} placeholder="IP range" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={`ipamConfig.${index}.gateway`}
|
|
||||||
render={({ field: f }) => (
|
|
||||||
<FormItem className="min-w-[120px] flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Input {...f} placeholder="Gateway" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => ipamConfigFieldArray.remove(index)}
|
|
||||||
>
|
|
||||||
−
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
ipamConfigFieldArray.append({
|
|
||||||
subnet: "",
|
|
||||||
ipRange: "",
|
|
||||||
gateway: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Add IPAM config
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isPending}>
|
|
||||||
{isPending
|
|
||||||
? isEdit
|
|
||||||
? "Updating…"
|
|
||||||
: "Creating…"
|
|
||||||
: isEdit
|
|
||||||
? "Update network"
|
|
||||||
: "Create network"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Loader2, Network } from "lucide-react";
|
|
||||||
import { HandleNetwork } from "@/components/dashboard/networks/handle-network";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
export const ShowNetworks = () => {
|
|
||||||
const { data: networks, isLoading } = api.network.all.useQuery();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
|
||||||
<div className="flex flex-row justify-between items-center">
|
|
||||||
<CardHeader className="">
|
|
||||||
<CardTitle className="text-xl flex flex-row gap-2">
|
|
||||||
<Network className="size-6 text-muted-foreground self-center" />
|
|
||||||
Networks
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Manage Docker networks for your organization. Networks can be
|
|
||||||
scoped to a server (optional).
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
{networks && networks?.length > 0 && <HandleNetwork />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
|
||||||
<div className="gap-4 pb-20 w-full">
|
|
||||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
|
||||||
<div className="rounded-md border">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground h-[55vh]">
|
|
||||||
<span>Loading...</span>
|
|
||||||
<Loader2 className="animate-spin size-4" />
|
|
||||||
</div>
|
|
||||||
) : !networks?.length ? (
|
|
||||||
<div className="flex min-h-[55vh] w-full flex-col items-center justify-center gap-4 rounded-lg border border-dashed p-8">
|
|
||||||
<div className="rounded-full bg-muted p-4">
|
|
||||||
<Network className="size-10 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 text-center">
|
|
||||||
<p className="text-sm font-medium">No networks yet</p>
|
|
||||||
<p className="max-w-sm text-sm text-muted-foreground">
|
|
||||||
Create Docker networks for your organization and
|
|
||||||
optionally attach them to a server. Add your first
|
|
||||||
network to get started.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<HandleNetwork />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>Driver</TableHead>
|
|
||||||
<TableHead>Scope</TableHead>
|
|
||||||
<TableHead>Internal</TableHead>
|
|
||||||
<TableHead>Attachable</TableHead>
|
|
||||||
<TableHead>Server</TableHead>
|
|
||||||
<TableHead>Created</TableHead>
|
|
||||||
<TableHead className="w-[80px]">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{networks.map((n) => (
|
|
||||||
<TableRow key={n.networkId}>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
{n.name}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{n.driver}</TableCell>
|
|
||||||
<TableCell>{n.scope ?? "—"}</TableCell>
|
|
||||||
<TableCell>{n.internal ? "Yes" : "No"}</TableCell>
|
|
||||||
<TableCell>{n.attachable ? "Yes" : "No"}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{n.serverId ?? "Dokploy server"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">
|
|
||||||
{new Date(n.createdAt).toLocaleDateString()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<HandleNetwork networkId={n.networkId}>
|
|
||||||
<Button variant="ghost" size="sm">
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</HandleNetwork>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
LogIn,
|
LogIn,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Network,
|
|
||||||
Package,
|
Package,
|
||||||
Palette,
|
Palette,
|
||||||
PieChart,
|
PieChart,
|
||||||
@@ -207,20 +206,6 @@ const MENU: Menu = {
|
|||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!(permissions?.docker.read && !isCloud),
|
!!(permissions?.docker.read && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
isSingle: true,
|
|
||||||
title: "Networks",
|
|
||||||
url: "/dashboard/networks",
|
|
||||||
icon: Network,
|
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
|
||||||
!!(
|
|
||||||
(auth?.role === "owner" ||
|
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canAccessToDocker) &&
|
|
||||||
!isCloud
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Requests",
|
title: "Requests",
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
CREATE TYPE "public"."networkDriver" AS ENUM('bridge', 'host', 'overlay', 'macvlan', 'none', 'ipvlan');--> statement-breakpoint
|
|
||||||
CREATE TABLE "network" (
|
|
||||||
"networkId" text PRIMARY KEY NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
"driver" "networkDriver" DEFAULT 'bridge' NOT NULL,
|
|
||||||
"scope" text,
|
|
||||||
"internal" boolean DEFAULT false NOT NULL,
|
|
||||||
"attachable" boolean DEFAULT false NOT NULL,
|
|
||||||
"ingress" boolean DEFAULT false NOT NULL,
|
|
||||||
"configOnly" boolean DEFAULT false NOT NULL,
|
|
||||||
"enableIPv4" boolean DEFAULT true NOT NULL,
|
|
||||||
"enableIPv6" boolean DEFAULT false NOT NULL,
|
|
||||||
"ipam" jsonb DEFAULT '{}'::jsonb,
|
|
||||||
"createdAt" text NOT NULL,
|
|
||||||
"organizationId" text NOT NULL,
|
|
||||||
"serverId" text
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "application" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
|
||||||
ALTER TABLE "compose" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
|
||||||
ALTER TABLE "mariadb" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
|
||||||
ALTER TABLE "mongo" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
|
||||||
ALTER TABLE "mysql" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
|
||||||
ALTER TABLE "postgres" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
|
||||||
ALTER TABLE "redis" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
|
||||||
ALTER TABLE "network" ADD CONSTRAINT "network_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "network" ADD CONSTRAINT "network_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1163,13 +1163,6 @@
|
|||||||
"when": 1775845419261,
|
"when": 1775845419261,
|
||||||
"tag": "0165_abnormal_greymalkin",
|
"tag": "0165_abnormal_greymalkin",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 166,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1776117573148,
|
|
||||||
"tag": "0166_pale_multiple_man",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
|
||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import type { ReactElement } from "react";
|
|
||||||
import superjson from "superjson";
|
|
||||||
import { ShowNetworks } from "@/components/dashboard/networks/show-networks";
|
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
|
||||||
return <ShowNetworks />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Dashboard;
|
|
||||||
|
|
||||||
Dashboard.getLayout = (page: ReactElement) => {
|
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getServerSideProps(
|
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
|
||||||
) {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: true,
|
|
||||||
destination: "/dashboard/projects",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { user, session } = await validateRequest(ctx.req);
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: true,
|
|
||||||
destination: "/",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { req } = ctx;
|
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
|
||||||
router: appRouter,
|
|
||||||
ctx: {
|
|
||||||
req: req as any,
|
|
||||||
res: ctx.res as any,
|
|
||||||
db: null as any,
|
|
||||||
session: session as any,
|
|
||||||
user: user as any,
|
|
||||||
},
|
|
||||||
transformer: superjson,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await helpers.project.all.prefetch();
|
|
||||||
|
|
||||||
if (user.role === "member") {
|
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToDocker) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: true,
|
|
||||||
destination: "/",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await helpers.network.all.prefetch();
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: helpers.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,6 +22,7 @@ import { ShowSchedules } from "@/components/dashboard/application/schedules/show
|
|||||||
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
|
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
|
||||||
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
||||||
import { IsolatedDeploymentTab } from "@/components/dashboard/compose/advanced/add-isolation";
|
import { IsolatedDeploymentTab } from "@/components/dashboard/compose/advanced/add-isolation";
|
||||||
|
import { ShowComposeContainers } from "@/components/dashboard/compose/containers/show-compose-containers";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
||||||
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
||||||
@@ -60,6 +61,7 @@ type TabState =
|
|||||||
| "advanced"
|
| "advanced"
|
||||||
| "deployments"
|
| "deployments"
|
||||||
| "domains"
|
| "domains"
|
||||||
|
| "containers"
|
||||||
| "monitoring"
|
| "monitoring"
|
||||||
| "volumeBackups";
|
| "volumeBackups";
|
||||||
|
|
||||||
@@ -231,6 +233,9 @@ const Service = (
|
|||||||
Deployments
|
Deployments
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
|
{permissions?.service.read && (
|
||||||
|
<TabsTrigger value="containers">Containers</TabsTrigger>
|
||||||
|
)}
|
||||||
{permissions?.service.create && (
|
{permissions?.service.create && (
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
@@ -298,6 +303,18 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
)}
|
||||||
|
{permissions?.service.read && (
|
||||||
|
<TabsContent value="containers">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowComposeContainers
|
||||||
|
serverId={data?.serverId || undefined}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
appType={data?.composeType || "docker-compose"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
{permissions?.monitoring.read && (
|
{permissions?.monitoring.read && (
|
||||||
<TabsContent value="monitoring">
|
<TabsContent value="monitoring">
|
||||||
<div className="pt-2.5">
|
<div className="pt-2.5">
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { mariadbRouter } from "./routers/mariadb";
|
|||||||
import { mongoRouter } from "./routers/mongo";
|
import { mongoRouter } from "./routers/mongo";
|
||||||
import { mountRouter } from "./routers/mount";
|
import { mountRouter } from "./routers/mount";
|
||||||
import { mysqlRouter } from "./routers/mysql";
|
import { mysqlRouter } from "./routers/mysql";
|
||||||
import { networkRouter } from "./routers/network";
|
|
||||||
import { notificationRouter } from "./routers/notification";
|
import { notificationRouter } from "./routers/notification";
|
||||||
import { organizationRouter } from "./routers/organization";
|
import { organizationRouter } from "./routers/organization";
|
||||||
import { patchRouter } from "./routers/patch";
|
import { patchRouter } from "./routers/patch";
|
||||||
@@ -59,7 +58,6 @@ export const appRouter = createTRPCRouter({
|
|||||||
application: applicationRouter,
|
application: applicationRouter,
|
||||||
backup: backupRouter,
|
backup: backupRouter,
|
||||||
bitbucket: bitbucketRouter,
|
bitbucket: bitbucketRouter,
|
||||||
network: networkRouter,
|
|
||||||
certificates: certificateRouter,
|
certificates: certificateRouter,
|
||||||
cluster: clusterRouter,
|
cluster: clusterRouter,
|
||||||
compose: composeRouter,
|
compose: composeRouter,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
|
containerKill,
|
||||||
containerRemove,
|
containerRemove,
|
||||||
containerRestart,
|
containerRestart,
|
||||||
|
containerStart,
|
||||||
|
containerStop,
|
||||||
findServerById,
|
findServerById,
|
||||||
getConfig,
|
getConfig,
|
||||||
getContainers,
|
getContainers,
|
||||||
@@ -35,24 +38,108 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
return await getContainers(input.serverId);
|
return await getContainers(input.serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
restartContainer: withPermission("docker", "read")
|
restartContainer: withPermission("service", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
containerId: z
|
containerId: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.regex(containerIdRegex, "Invalid container id."),
|
.regex(containerIdRegex, "Invalid container id."),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const result = await containerRestart(input.containerId);
|
if (input.serverId) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await containerRestart(input.containerId, input.serverId);
|
||||||
await audit(ctx, {
|
await audit(ctx, {
|
||||||
action: "start",
|
action: "start",
|
||||||
resourceType: "docker",
|
resourceType: "docker",
|
||||||
resourceId: input.containerId,
|
resourceId: input.containerId,
|
||||||
resourceName: input.containerId,
|
resourceName: input.containerId,
|
||||||
});
|
});
|
||||||
return result;
|
}),
|
||||||
|
|
||||||
|
startContainer: withPermission("service", "read")
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
containerId: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.regex(containerIdRegex, "Invalid container id."),
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (input.serverId) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await containerStart(input.containerId, input.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "start",
|
||||||
|
resourceType: "docker",
|
||||||
|
resourceId: input.containerId,
|
||||||
|
resourceName: input.containerId,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
stopContainer: withPermission("service", "read")
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
containerId: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.regex(containerIdRegex, "Invalid container id."),
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (input.serverId) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await containerStop(input.containerId, input.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "docker",
|
||||||
|
resourceId: input.containerId,
|
||||||
|
resourceName: input.containerId,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
killContainer: withPermission("service", "read")
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
containerId: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.regex(containerIdRegex, "Invalid container id."),
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (input.serverId) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await containerKill(input.containerId, input.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "docker",
|
||||||
|
resourceId: input.containerId,
|
||||||
|
resourceName: input.containerId,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
removeContainer: withPermission("docker", "read")
|
removeContainer: withPermission("docker", "read")
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import {
|
|
||||||
createNetwork,
|
|
||||||
findNetworkById,
|
|
||||||
removeNetwork,
|
|
||||||
updateNetwork,
|
|
||||||
} from "@dokploy/server";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { desc, eq } from "drizzle-orm";
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
apiCreateNetwork,
|
|
||||||
apiFindOneNetwork,
|
|
||||||
apiRemoveNetwork,
|
|
||||||
apiUpdateNetwork,
|
|
||||||
network as networkTable,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
|
|
||||||
export const networkRouter = createTRPCRouter({
|
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
const rows = await db
|
|
||||||
.select()
|
|
||||||
.from(networkTable)
|
|
||||||
.where(eq(networkTable.organizationId, ctx.session.activeOrganizationId))
|
|
||||||
.orderBy(desc(networkTable.createdAt));
|
|
||||||
return rows;
|
|
||||||
}),
|
|
||||||
|
|
||||||
one: protectedProcedure
|
|
||||||
.input(apiFindOneNetwork)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const row = await findNetworkById(input.networkId);
|
|
||||||
if (row.organizationId !== ctx.session.activeOrganizationId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Network not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return row;
|
|
||||||
}),
|
|
||||||
create: protectedProcedure
|
|
||||||
.input(apiCreateNetwork)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
return createNetwork(input, ctx.session.activeOrganizationId);
|
|
||||||
}),
|
|
||||||
update: protectedProcedure
|
|
||||||
.input(apiUpdateNetwork)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const network = await findNetworkById(input.networkId);
|
|
||||||
if (network.organizationId !== ctx.session.activeOrganizationId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Not authorized to update this network",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return updateNetwork(input);
|
|
||||||
}),
|
|
||||||
|
|
||||||
remove: protectedProcedure
|
|
||||||
.input(apiRemoveNetwork)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const network = await findNetworkById(input.networkId);
|
|
||||||
if (network.organizationId !== ctx.session.activeOrganizationId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Not authorized to delete this network",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return removeNetwork(input.networkId);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
timestamp,
|
timestamp,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { network } from "./network";
|
|
||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import { ssoProvider } from "./sso";
|
import { ssoProvider } from "./sso";
|
||||||
@@ -109,7 +108,6 @@ export const organizationRelations = relations(
|
|||||||
references: [user.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
servers: many(server),
|
servers: many(server),
|
||||||
networks: many(network),
|
|
||||||
projects: many(projects),
|
projects: many(projects),
|
||||||
members: many(member),
|
members: many(member),
|
||||||
ssoProviders: many(ssoProvider),
|
ssoProviders: many(ssoProvider),
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ export const applications = pgTable("application", {
|
|||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
networkIds: text("networkIds").array().default([]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const applicationsRelations = relations(
|
export const applicationsRelations = relations(
|
||||||
@@ -369,7 +368,6 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
||||||
watchPaths: z.array(z.string()).optional().optional(),
|
watchPaths: z.array(z.string()).optional().optional(),
|
||||||
previewLabels: z.array(z.string()).optional(),
|
previewLabels: z.array(z.string()).optional(),
|
||||||
networkIds: z.array(z.string()).optional(),
|
|
||||||
cleanCache: z.boolean().optional(),
|
cleanCache: z.boolean().optional(),
|
||||||
stopGracePeriodSwarm: z.number().nullable(),
|
stopGracePeriodSwarm: z.number().nullable(),
|
||||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export const compose = pgTable("compose", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
networkIds: text("networkIds").array().default([]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const composeRelations = relations(compose, ({ one, many }) => ({
|
export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export * from "./libsql";
|
|||||||
export * from "./mariadb";
|
export * from "./mariadb";
|
||||||
export * from "./mongo";
|
export * from "./mongo";
|
||||||
export * from "./mount";
|
export * from "./mount";
|
||||||
export * from "./network";
|
|
||||||
export * from "./mysql";
|
export * from "./mysql";
|
||||||
export * from "./notification";
|
export * from "./notification";
|
||||||
export * from "./patch";
|
export * from "./patch";
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ export const mariadb = pgTable("mariadb", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
networkIds: text("networkIds").array().default([]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
|
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export const mongo = pgTable("mongo", {
|
|||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
replicaSets: boolean("replicaSets").default(false),
|
replicaSets: boolean("replicaSets").default(false),
|
||||||
networkIds: text("networkIds").array().default([]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ export const mysql = pgTable("mysql", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
networkIds: text("networkIds").array().default([]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mysqlRelations = relations(mysql, ({ one, many }) => ({
|
export const mysqlRelations = relations(mysql, ({ one, many }) => ({
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
import { relations } from "drizzle-orm";
|
|
||||||
import { boolean, jsonb, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { organization } from "./account";
|
|
||||||
import { server } from "./server";
|
|
||||||
|
|
||||||
/** Docker network driver types */
|
|
||||||
export const networkDriver = pgEnum("networkDriver", [
|
|
||||||
"bridge",
|
|
||||||
"host",
|
|
||||||
"overlay",
|
|
||||||
"macvlan",
|
|
||||||
"none",
|
|
||||||
"ipvlan",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const network = pgTable("network", {
|
|
||||||
networkId: text("networkId")
|
|
||||||
.notNull()
|
|
||||||
.primaryKey()
|
|
||||||
.$defaultFn(() => nanoid()),
|
|
||||||
name: text("name").notNull(),
|
|
||||||
driver: networkDriver("driver").notNull().default("bridge"),
|
|
||||||
scope: text("scope"), // e.g. "local", "swarm"
|
|
||||||
internal: boolean("internal").notNull().default(false),
|
|
||||||
attachable: boolean("attachable").notNull().default(false),
|
|
||||||
ingress: boolean("ingress").notNull().default(false),
|
|
||||||
configOnly: boolean("configOnly").notNull().default(false),
|
|
||||||
enableIPv4: boolean("enableIPv4").notNull().default(true),
|
|
||||||
enableIPv6: boolean("enableIPv6").notNull().default(false),
|
|
||||||
ipam: jsonb("ipam")
|
|
||||||
.$type<{
|
|
||||||
driver?: string;
|
|
||||||
config?: Array<{ subnet?: string; gateway?: string; ipRange?: string }>;
|
|
||||||
}>()
|
|
||||||
.default({}),
|
|
||||||
createdAt: text("createdAt")
|
|
||||||
.notNull()
|
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
|
||||||
organizationId: text("organizationId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => organization.id, { onDelete: "cascade" }),
|
|
||||||
serverId: text("serverId").references(() => server.serverId, {
|
|
||||||
onDelete: "cascade",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const networkRelations = relations(network, ({ one }) => ({
|
|
||||||
organization: one(organization, {
|
|
||||||
fields: [network.organizationId],
|
|
||||||
references: [organization.id],
|
|
||||||
}),
|
|
||||||
server: one(server, {
|
|
||||||
fields: [network.serverId],
|
|
||||||
references: [server.serverId],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const createSchema = createInsertSchema(network, {
|
|
||||||
networkId: z.string().min(1),
|
|
||||||
name: z.string().min(1),
|
|
||||||
driver: z
|
|
||||||
.enum(["bridge", "host", "overlay", "macvlan", "none", "ipvlan"])
|
|
||||||
.optional(),
|
|
||||||
scope: z.string().optional(),
|
|
||||||
internal: z.boolean().optional(),
|
|
||||||
attachable: z.boolean().optional(),
|
|
||||||
ingress: z.boolean().optional(),
|
|
||||||
configOnly: z.boolean().optional(),
|
|
||||||
enableIPv4: z.boolean().optional(),
|
|
||||||
enableIPv6: z.boolean().optional(),
|
|
||||||
ipam: z
|
|
||||||
.object({
|
|
||||||
driver: z.string().optional(),
|
|
||||||
config: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
subnet: z.string().optional(),
|
|
||||||
gateway: z.string().optional(),
|
|
||||||
ipRange: z.string().optional(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
organizationId: z.string().min(1),
|
|
||||||
serverId: z.string().optional().nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiCreateNetwork = createSchema
|
|
||||||
.pick({
|
|
||||||
name: true,
|
|
||||||
driver: true,
|
|
||||||
scope: true,
|
|
||||||
internal: true,
|
|
||||||
attachable: true,
|
|
||||||
ingress: true,
|
|
||||||
configOnly: true,
|
|
||||||
enableIPv4: true,
|
|
||||||
enableIPv6: true,
|
|
||||||
ipam: true,
|
|
||||||
serverId: true,
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
.required({ name: true });
|
|
||||||
|
|
||||||
export const apiFindOneNetwork = createSchema
|
|
||||||
.pick({
|
|
||||||
networkId: true,
|
|
||||||
})
|
|
||||||
.required();
|
|
||||||
|
|
||||||
export const apiRemoveNetwork = createSchema
|
|
||||||
.pick({
|
|
||||||
networkId: true,
|
|
||||||
})
|
|
||||||
.required();
|
|
||||||
|
|
||||||
export const apiUpdateNetwork = createSchema
|
|
||||||
.pick({
|
|
||||||
networkId: true,
|
|
||||||
name: true,
|
|
||||||
driver: true,
|
|
||||||
scope: true,
|
|
||||||
internal: true,
|
|
||||||
attachable: true,
|
|
||||||
ingress: true,
|
|
||||||
configOnly: true,
|
|
||||||
enableIPv4: true,
|
|
||||||
enableIPv6: true,
|
|
||||||
ipam: true,
|
|
||||||
serverId: true,
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
.required({ networkId: true });
|
|
||||||
@@ -85,7 +85,6 @@ export const postgres = pgTable("postgres", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
networkIds: text("networkIds").array().default([]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const postgresRelations = relations(postgres, ({ one, many }) => ({
|
export const postgresRelations = relations(postgres, ({ one, many }) => ({
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export const redis = pgTable("redis", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
networkIds: text("networkIds").array().default([]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const redisRelations = relations(redis, ({ one, many }) => ({
|
export const redisRelations = relations(redis, ({ one, many }) => ({
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { libsql } from "./libsql";
|
|||||||
import { mariadb } from "./mariadb";
|
import { mariadb } from "./mariadb";
|
||||||
import { mongo } from "./mongo";
|
import { mongo } from "./mongo";
|
||||||
import { mysql } from "./mysql";
|
import { mysql } from "./mysql";
|
||||||
import { network } from "./network";
|
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
import { redis } from "./redis";
|
import { redis } from "./redis";
|
||||||
import { schedules } from "./schedule";
|
import { schedules } from "./schedule";
|
||||||
@@ -125,7 +124,6 @@ export const serverRelations = relations(server, ({ one, many }) => ({
|
|||||||
mysql: many(mysql),
|
mysql: many(mysql),
|
||||||
postgres: many(postgres),
|
postgres: many(postgres),
|
||||||
certificates: many(certificates),
|
certificates: many(certificates),
|
||||||
networks: many(network),
|
|
||||||
organization: one(organization, {
|
organization: one(organization, {
|
||||||
fields: [server.organizationId],
|
fields: [server.organizationId],
|
||||||
references: [organization.id],
|
references: [organization.id],
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export * from "./services/mariadb";
|
|||||||
export * from "./services/mongo";
|
export * from "./services/mongo";
|
||||||
export * from "./services/mount";
|
export * from "./services/mount";
|
||||||
export * from "./services/mysql";
|
export * from "./services/mysql";
|
||||||
export * from "./services/network";
|
|
||||||
export * from "./services/notification";
|
export * from "./services/notification";
|
||||||
export * from "./services/patch";
|
export * from "./services/patch";
|
||||||
export * from "./services/patch-repo";
|
export * from "./services/patch-repo";
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ export const findApplicationById = async (applicationId: string) => {
|
|||||||
project: true,
|
project: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
domains: true,
|
domains: true,
|
||||||
deployments: true,
|
deployments: true,
|
||||||
mounts: true,
|
mounts: true,
|
||||||
|
|||||||
@@ -417,21 +417,58 @@ export const getContainerLogs = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const containerRestart = async (containerId: string) => {
|
export const containerRestart = async (
|
||||||
try {
|
containerId: string,
|
||||||
const { stdout, stderr } = await execAsync(
|
serverId?: string,
|
||||||
`docker container restart ${containerId}`,
|
) => {
|
||||||
);
|
const command = `docker container restart ${containerId}`;
|
||||||
|
const { stderr } = serverId
|
||||||
|
? await execAsyncRemote(serverId, command)
|
||||||
|
: await execAsync(command);
|
||||||
|
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
console.error(`Error: ${stderr}`);
|
console.error(`Error: ${stderr}`);
|
||||||
return;
|
throw new Error(stderr);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const config = JSON.parse(stdout);
|
export const containerStart = async (
|
||||||
|
containerId: string,
|
||||||
|
serverId?: string,
|
||||||
|
) => {
|
||||||
|
const command = `docker container start ${containerId}`;
|
||||||
|
const { stderr } = serverId
|
||||||
|
? await execAsyncRemote(serverId, command)
|
||||||
|
: await execAsync(command);
|
||||||
|
|
||||||
return config;
|
if (stderr) {
|
||||||
} catch {}
|
console.error(`Error: ${stderr}`);
|
||||||
|
throw new Error(stderr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const containerStop = async (containerId: string, serverId?: string) => {
|
||||||
|
const command = `docker container stop ${containerId}`;
|
||||||
|
const { stderr } = serverId
|
||||||
|
? await execAsyncRemote(serverId, command)
|
||||||
|
: await execAsync(command);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`Error: ${stderr}`);
|
||||||
|
throw new Error(stderr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const containerKill = async (containerId: string, serverId?: string) => {
|
||||||
|
const command = `docker container kill ${containerId}`;
|
||||||
|
const { stderr } = serverId
|
||||||
|
? await execAsyncRemote(serverId, command)
|
||||||
|
: await execAsync(command);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`Error: ${stderr}`);
|
||||||
|
throw new Error(stderr);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const containerRemove = async (
|
export const containerRemove = async (
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
|
||||||
import {
|
|
||||||
type apiCreateNetwork,
|
|
||||||
type apiUpdateNetwork,
|
|
||||||
network,
|
|
||||||
} from "@dokploy/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { IS_CLOUD } from "../constants";
|
|
||||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
|
||||||
|
|
||||||
export const findNetworkById = async (networkId: string) => {
|
|
||||||
const [row] = await db
|
|
||||||
.select()
|
|
||||||
.from(network)
|
|
||||||
.where(eq(network.networkId, networkId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Network not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return row;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createNetwork = async (
|
|
||||||
input: typeof apiCreateNetwork._type,
|
|
||||||
organizationId: string,
|
|
||||||
) => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
if (!input.serverId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Server is required",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = await db.transaction(async (tx) => {
|
|
||||||
const [row] = await tx
|
|
||||||
.insert(network)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
organizationId,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: "Failed to create network",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipam = row.ipam ?? {};
|
|
||||||
const ipamConfig = (ipam.config ?? [])
|
|
||||||
.map((c) => {
|
|
||||||
const entry: Record<string, string> = {};
|
|
||||||
if (c.subnet) entry.Subnet = c.subnet;
|
|
||||||
if (c.gateway) entry.Gateway = c.gateway;
|
|
||||||
if (c.ipRange) entry.IPRange = c.ipRange;
|
|
||||||
return entry;
|
|
||||||
})
|
|
||||||
.filter((e) => Object.keys(e).length > 0);
|
|
||||||
|
|
||||||
const docker = await getRemoteDocker(input.serverId ?? null);
|
|
||||||
await docker.createNetwork({
|
|
||||||
Name: row.name,
|
|
||||||
Driver: row.driver,
|
|
||||||
Internal: row.internal,
|
|
||||||
Attachable: row.attachable,
|
|
||||||
Ingress: row.ingress,
|
|
||||||
EnableIPv6: row.enableIPv6,
|
|
||||||
IPAM: {
|
|
||||||
Driver: ipam.driver ?? "default",
|
|
||||||
Config: ipamConfig.length > 0 ? ipamConfig : undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
|
|
||||||
return created;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateNetwork = async (input: typeof apiUpdateNetwork._type) => {
|
|
||||||
const { networkId, ...rest } = input;
|
|
||||||
const [updated] = await db
|
|
||||||
.update(network)
|
|
||||||
.set(rest)
|
|
||||||
.where(eq(network.networkId, networkId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!updated) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Network not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeNetwork = async (networkId: string) => {
|
|
||||||
const [deleted] = await db
|
|
||||||
.delete(network)
|
|
||||||
.where(eq(network.networkId, networkId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!deleted) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Network not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return deleted;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user