mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: add missing additional ports
This commit is contained in:
parent
065963857c
commit
6e2b2d564b
@ -26,6 +26,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
@ -128,6 +129,14 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||
<span>Enter the terminal</span>
|
||||
</DropdownMenuItem>
|
||||
</DockerTerminalModal> */}
|
||||
<ManageTraefikPorts serverId={serverId}>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<span>{t("settings.server.webServer.traefik.managePorts")}</span>
|
||||
</DropdownMenuItem>
|
||||
</ManageTraefikPorts>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
@ -0,0 +1,215 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* Props for the ManageTraefikPorts component
|
||||
* @interface Props
|
||||
* @property {React.ReactNode} children - The trigger element that opens the ports management modal
|
||||
* @property {string} [serverId] - Optional ID of the server whose ports are being managed
|
||||
*/
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a port mapping configuration for Traefik
|
||||
* @interface AdditionalPort
|
||||
* @property {number} targetPort - The internal port that the service is listening on
|
||||
* @property {number} publishedPort - The external port that will be exposed
|
||||
* @property {"ingress" | "host"} publishMode - The Docker Swarm publish mode:
|
||||
* - "host": Publishes the port directly on the host
|
||||
* - "ingress": Publishes the port through the Swarm routing mesh
|
||||
*/
|
||||
interface AdditionalPort {
|
||||
targetPort: number;
|
||||
publishedPort: number;
|
||||
publishMode: "ingress" | "host";
|
||||
}
|
||||
|
||||
/**
|
||||
* ManageTraefikPorts is a component that provides a modal interface for managing
|
||||
* additional port mappings for Traefik in a Docker Swarm environment.
|
||||
*
|
||||
* Features:
|
||||
* - Add, remove, and edit port mappings
|
||||
* - Configure target port, published port, and publish mode for each mapping
|
||||
* - Persist port configurations through API calls
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ManageTraefikPorts serverId="server-123">
|
||||
* <Button>Manage Ports</Button>
|
||||
* </ManageTraefikPorts>
|
||||
* ```
|
||||
*/
|
||||
export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
const { t } = useTranslation("settings");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [additionalPorts, setAdditionalPorts] = useState<AdditionalPort[]>([]);
|
||||
|
||||
const { data: currentPorts, refetch: refetchPorts } =
|
||||
api.settings.getTraefikPorts.useQuery({
|
||||
serverId,
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePorts, isLoading } =
|
||||
api.settings.updateTraefikPorts.useMutation({
|
||||
onSuccess: () => {
|
||||
refetchPorts();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPorts) {
|
||||
setAdditionalPorts(currentPorts);
|
||||
}
|
||||
}, [currentPorts]);
|
||||
|
||||
const handleAddPort = () => {
|
||||
setAdditionalPorts([
|
||||
...additionalPorts,
|
||||
{ targetPort: 0, publishedPort: 0, publishMode: "host" },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleUpdatePorts = async () => {
|
||||
try {
|
||||
await updatePorts({
|
||||
serverId,
|
||||
additionalPorts,
|
||||
});
|
||||
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t("settings.server.webServer.traefik.portsUpdateError"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => setOpen(true)}>{children}</div>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("settings.server.webServer.traefik.managePorts")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("settings.server.webServer.traefik.managePortsDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
{additionalPorts.map((port, index) => (
|
||||
<div key={index} className="grid grid-cols-[120px_120px_minmax(120px,1fr)_80px] gap-4 items-end">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`target-port-${index}`}>
|
||||
{t("settings.server.webServer.traefik.targetPort")}
|
||||
</Label>
|
||||
<input
|
||||
id={`target-port-${index}`}
|
||||
type="number"
|
||||
value={port.targetPort}
|
||||
onChange={(e) => {
|
||||
const newPorts = [...additionalPorts];
|
||||
newPorts[index].targetPort = Number.parseInt(
|
||||
e.target.value,
|
||||
);
|
||||
setAdditionalPorts(newPorts);
|
||||
}}
|
||||
className="w-full rounded border p-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`published-port-${index}`}>
|
||||
{t("settings.server.webServer.traefik.publishedPort")}
|
||||
</Label>
|
||||
<input
|
||||
id={`published-port-${index}`}
|
||||
type="number"
|
||||
value={port.publishedPort}
|
||||
onChange={(e) => {
|
||||
const newPorts = [...additionalPorts];
|
||||
newPorts[index].publishedPort = Number.parseInt(
|
||||
e.target.value,
|
||||
);
|
||||
setAdditionalPorts(newPorts);
|
||||
}}
|
||||
className="w-full rounded border p-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`publish-mode-${index}`}>
|
||||
{t("settings.server.webServer.traefik.publishMode")}
|
||||
</Label>
|
||||
<Select
|
||||
value={port.publishMode}
|
||||
onValueChange={(value: "ingress" | "host") => {
|
||||
const newPorts = [...additionalPorts];
|
||||
newPorts[index].publishMode = value;
|
||||
setAdditionalPorts(newPorts);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id={`publish-mode-${index}`} className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="host">Host</SelectItem>
|
||||
<SelectItem value="ingress">Ingress</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const newPorts = additionalPorts.filter(
|
||||
(_, i) => i !== index,
|
||||
);
|
||||
setAdditionalPorts(newPorts);
|
||||
}}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-4 flex justify-between">
|
||||
<Button onClick={handleAddPort} variant="outline" size="sm">
|
||||
{t("settings.server.webServer.traefik.addPort")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdatePorts}
|
||||
size="sm"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
@ -18,6 +18,14 @@
|
||||
"settings.server.webServer.server.label": "Server",
|
||||
"settings.server.webServer.traefik.label": "Traefik",
|
||||
"settings.server.webServer.traefik.modifyEnv": "Modify Env",
|
||||
"settings.server.webServer.traefik.managePorts": "Additional Ports",
|
||||
"settings.server.webServer.traefik.managePortsDescription": "Add or remove additional ports for Traefik",
|
||||
"settings.server.webServer.traefik.targetPort": "Target Port",
|
||||
"settings.server.webServer.traefik.publishedPort": "Published Port",
|
||||
"settings.server.webServer.traefik.addPort": "Add Port",
|
||||
"settings.server.webServer.traefik.portsUpdated": "Ports updated successfully",
|
||||
"settings.server.webServer.traefik.portsUpdateError": "Failed to update ports",
|
||||
"settings.server.webServer.traefik.publishMode": "Publish Mode",
|
||||
"settings.server.webServer.storage.label": "Space",
|
||||
"settings.server.webServer.storage.cleanUnusedImages": "Clean unused images",
|
||||
"settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes",
|
||||
|
@ -716,6 +716,83 @@ export const settingsRouter = createTRPCRouter({
|
||||
throw new Error("Failed to check GPU status");
|
||||
}
|
||||
}),
|
||||
updateTraefikPorts: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
additionalPorts: z.array(
|
||||
z.object({
|
||||
targetPort: z.number(),
|
||||
publishedPort: z.number(),
|
||||
publishMode: z.enum(["ingress", "host"]).default("host"),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Please set a serverId to update Traefik ports",
|
||||
});
|
||||
}
|
||||
await initializeTraefik({
|
||||
serverId: input.serverId,
|
||||
additionalPorts: input.additionalPorts,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Error to update Traefik ports",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
getTraefikPorts: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.query(async ({ input }) => {
|
||||
const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`;
|
||||
|
||||
try {
|
||||
let stdout = "";
|
||||
if (input?.serverId) {
|
||||
const result = await execAsyncRemote(input.serverId, command);
|
||||
stdout = result.stdout;
|
||||
} else if (!IS_CLOUD) {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
}
|
||||
|
||||
const ports: {
|
||||
Protocol: string;
|
||||
TargetPort: number;
|
||||
PublishedPort: number;
|
||||
PublishMode: string;
|
||||
}[] = JSON.parse(stdout.trim());
|
||||
|
||||
// Filter out the default ports (80, 443, and optionally 8080)
|
||||
const additionalPorts = ports
|
||||
.filter((port) => ![80, 443, 8080].includes(port.PublishedPort))
|
||||
.map((port) => ({
|
||||
targetPort: port.TargetPort,
|
||||
publishedPort: port.PublishedPort,
|
||||
publishMode: port.PublishMode.toLowerCase() as "host" | "ingress",
|
||||
}));
|
||||
|
||||
return additionalPorts;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to get Traefik ports",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
// {
|
||||
// "Parallelism": 1,
|
||||
|
@ -16,12 +16,18 @@ interface TraefikOptions {
|
||||
enableDashboard?: boolean;
|
||||
env?: string[];
|
||||
serverId?: string;
|
||||
additionalPorts?: {
|
||||
targetPort: number;
|
||||
publishedPort: number;
|
||||
publishMode?: "ingress" | "host";
|
||||
}[];
|
||||
}
|
||||
|
||||
export const initializeTraefik = async ({
|
||||
enableDashboard = false,
|
||||
env,
|
||||
serverId,
|
||||
additionalPorts = [],
|
||||
}: TraefikOptions = {}) => {
|
||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||
const imageName = "traefik:v3.1.2";
|
||||
@ -84,6 +90,11 @@ export const initializeTraefik = async ({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...additionalPorts.map((port) => ({
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
PublishMode: port.publishMode || ("host" as const),
|
||||
})),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user