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 { useTranslation } from "next-i18next";
|
||||||
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
@ -128,6 +129,14 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
<span>Enter the terminal</span>
|
<span>Enter the terminal</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DockerTerminalModal> */}
|
</DockerTerminalModal> */}
|
||||||
|
<ManageTraefikPorts serverId={serverId}>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<span>{t("settings.server.webServer.traefik.managePorts")}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</ManageTraefikPorts>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</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.server.label": "Server",
|
||||||
"settings.server.webServer.traefik.label": "Traefik",
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
"settings.server.webServer.traefik.modifyEnv": "Modify Env",
|
"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.label": "Space",
|
||||||
"settings.server.webServer.storage.cleanUnusedImages": "Clean unused images",
|
"settings.server.webServer.storage.cleanUnusedImages": "Clean unused images",
|
||||||
"settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes",
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes",
|
||||||
|
@ -716,6 +716,83 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
throw new Error("Failed to check GPU status");
|
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,
|
// "Parallelism": 1,
|
||||||
|
@ -16,12 +16,18 @@ interface TraefikOptions {
|
|||||||
enableDashboard?: boolean;
|
enableDashboard?: boolean;
|
||||||
env?: string[];
|
env?: string[];
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
additionalPorts?: {
|
||||||
|
targetPort: number;
|
||||||
|
publishedPort: number;
|
||||||
|
publishMode?: "ingress" | "host";
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initializeTraefik = async ({
|
export const initializeTraefik = async ({
|
||||||
enableDashboard = false,
|
enableDashboard = false,
|
||||||
env,
|
env,
|
||||||
serverId,
|
serverId,
|
||||||
|
additionalPorts = [],
|
||||||
}: TraefikOptions = {}) => {
|
}: TraefikOptions = {}) => {
|
||||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||||
const imageName = "traefik:v3.1.2";
|
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