diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..58825f900 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## What is this PR about? + +Please describe in a short paragraph what this PR is about. + +## Checklist + +Before submitting this PR, please make sure that: + +- [ ] You created a dedicated branch based on the `canary` branch. +- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request +- [ ] You have tested this PR in your local instance. + +## Issues related (if applicable) + +Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER` + +Example: `closes #123` + +## Screenshots (if applicable) + +If you include a video or screenshot, would be awesome so we can see the changes in action. \ No newline at end of file diff --git a/.github/workflows/create-pr.yml b/.github/workflows/create-pr.yml index e3f6aa234..248b98d5a 100644 --- a/.github/workflows/create-pr.yml +++ b/.github/workflows/create-pr.yml @@ -19,17 +19,14 @@ jobs: fetch-depth: 0 - name: Get version from package.json - id: package_version run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV - name: Get latest GitHub tag - id: latest_tag run: | LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1) echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV echo $LATEST_TAG - name: Compare versions - id: compare_versions run: | if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then VERSION_CHANGED="true" @@ -42,7 +39,6 @@ jobs: echo "Latest tag: ${{ env.LATEST_TAG }}" echo "Version changed: $VERSION_CHANGED" - name: Check if a PR already exists - id: check_pr run: | PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length') echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ac5a3581..38a36345e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,8 @@ pnpm run dokploy:dev Go to http://localhost:3000 to see the development server -Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off. +> [!NOTE] +> This project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off. ## Build @@ -117,10 +118,10 @@ In the case you lost your password, you can reset it using the following command pnpm run reset-password ``` -If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel` +If you want to test the webhooks on development mode using localtunnel, make sure to install [`localtunnel`](https://localtunnel.app/) ```bash -bunx lt --port 3000 +pnpm dlx localtunnel --port 3000 ``` If you run into permission issues of docker run the following command @@ -152,7 +153,7 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0. ## Pull Request -- The `main` branch is the source of truth and should always reflect the latest stable release. +- The `canary` branch is the source of truth and should always reflect the latest stable release. - Create a new branch for each feature or bug fix. - Make sure to add tests for your changes. - Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes. @@ -161,6 +162,12 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0. - If your pull request fixes an open issue, please reference the issue in the pull request description. - Once your pull request is merged, you will be automatically added as a contributor to the project. +**Important Considerations for Pull Requests:** + +- **Focus and Scope:** Each Pull Request should ideally address a single, well-defined problem or introduce one new feature. This greatly facilitates review and reduces the chances of introducing unintended side effects. +- **Avoid Unfocused Changes:** Please avoid submitting Pull Requests that contain only minor changes such as whitespace adjustments, IDE-generated formatting, or removal of unused variables, unless these are part of a larger, clearly defined refactor or a dedicated "cleanup" Pull Request that addresses a specific `good first issue` or maintenance task. +- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`). + Thank you for your contribution! ## Templates diff --git a/apps/dokploy/__test__/drop/drop.test.ts b/apps/dokploy/__test__/drop/drop.test.ts index 496949481..79dd6c6cc 100644 --- a/apps/dokploy/__test__/drop/drop.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.ts @@ -25,6 +25,7 @@ if (typeof window === "undefined") { } const baseApp: ApplicationNested = { + railpackVersion: "0.2.2", applicationId: "", herokuVersion: "", giteaBranch: "", diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index bcf42ad54..bbedd286f 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -3,6 +3,7 @@ import { createRouterConfig } from "@dokploy/server"; import { expect, test } from "vitest"; const baseApp: ApplicationNested = { + railpackVersion: "0.2.2", rollbackActive: false, applicationId: "", herokuVersion: "", diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index acd40c794..a3c3179d3 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { HelpCircle, Settings } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; @@ -27,12 +33,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { HelpCircle, Settings } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const HealthCheckSwarmSchema = z .object({ @@ -183,21 +183,38 @@ const addSwarmSettings = z.object({ type AddSwarmSettings = z.infer; interface Props { - applicationId: string; + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; } -export const AddSwarmSettings = ({ applicationId }: Props) => { - const { data, refetch } = api.application.one.useQuery( - { - applicationId, - }, - { - enabled: !!applicationId, - }, - ); +export const AddSwarmSettings = ({ id, type }: Props) => { + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); - const { mutateAsync, isError, error, isLoading } = - api.application.update.useMutation(); + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync, isError, error, isLoading } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); const form = useForm({ defaultValues: { @@ -247,7 +264,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { const onSubmit = async (data: AddSwarmSettings) => { await mutateAsync({ - applicationId, + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", healthCheckSwarm: data.healthCheckSwarm, restartPolicySwarm: data.restartPolicySwarm, placementSwarm: data.placementSwarm, @@ -274,7 +296,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { Swarm Settings - + Swarm Settings @@ -282,10 +304,10 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { {isError && {error?.message}} -
+
- Changing settings such as placements may cause the logs/monitoring - to be unavailable. + Changing settings such as placements may cause the logs/monitoring, + backups and other features to be unavailable.
@@ -293,13 +315,13 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
( - + Health Check @@ -355,7 +377,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="restartPolicySwarm" render={({ field }) => ( - + Restart Policy @@ -409,7 +431,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="placementSwarm" render={({ field }) => ( - + Placement @@ -475,7 +497,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="updateConfigSwarm" render={({ field }) => ( - + Update Config @@ -533,7 +555,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="rollbackConfigSwarm" render={({ field }) => ( - + Rollback Config @@ -591,7 +613,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="modeSwarm" render={({ field }) => ( - + Mode @@ -654,7 +676,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="networkSwarm" render={({ field }) => ( - + Network @@ -713,7 +735,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="labelsSwarm" render={({ field }) => ( - + Labels diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx index 57f851c9e..a3bc8079a 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx @@ -1,3 +1,10 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Server } from "lucide-react"; +import Link from "next/link"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -26,43 +33,57 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Server } from "lucide-react"; -import Link from "next/link"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; import { AddSwarmSettings } from "./modify-swarm-settings"; interface Props { - applicationId: string; + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; } const AddRedirectchema = z.object({ replicas: z.number().min(1, "Replicas must be at least 1"), - registryId: z.string(), + registryId: z.string().optional(), }); type AddCommand = z.infer; -export const ShowClusterSettings = ({ applicationId }: Props) => { - const { data } = api.application.one.useQuery( - { - applicationId, - }, - { enabled: !!applicationId }, - ); - +export const ShowClusterSettings = ({ id, type }: Props) => { + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); const { data: registries } = api.registry.all.useQuery(); - const utils = api.useUtils(); + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; - const { mutateAsync, isLoading } = api.application.update.useMutation(); + const { mutateAsync, isLoading } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); const form = useForm({ defaultValues: { - registryId: data?.registryId || "", + ...(type === "application" && data && "registryId" in data + ? { + registryId: data?.registryId || "", + } + : {}), replicas: data?.replicas || 1, }, resolver: zodResolver(AddRedirectchema), @@ -71,7 +92,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => { useEffect(() => { if (data?.command) { form.reset({ - registryId: data?.registryId || "", + ...(type === "application" && data && "registryId" in data + ? { + registryId: data?.registryId || "", + } + : {}), replicas: data?.replicas || 1, }); } @@ -79,18 +104,25 @@ export const ShowClusterSettings = ({ applicationId }: Props) => { const onSubmit = async (data: AddCommand) => { await mutateAsync({ - applicationId, - registryId: - data?.registryId === "none" || !data?.registryId - ? null - : data?.registryId, + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + ...(type === "application" + ? { + registryId: + data?.registryId === "none" || !data?.registryId + ? null + : data?.registryId, + } + : {}), replicas: data?.replicas, }) .then(async () => { toast.success("Command Updated"); - await utils.application.one.invalidate({ - applicationId, - }); + await refetch(); }) .catch(() => { toast.error("Error updating the command"); @@ -103,10 +135,10 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
Cluster Settings - Add the registry and the replicas of the application + Modify swarm settings for the service.
- + @@ -144,58 +176,62 @@ export const ShowClusterSettings = ({ applicationId }: Props) => { />
- {registries && registries?.length === 0 ? ( -
-
- - - To use a cluster feature, you need to configure at least a - registry first. Please, go to{" "} - - Settings - {" "} - to do so. - -
-
- ) : ( + {type === "application" && ( <> - ( - - Select a registry - - - )} - /> + {registries && registries?.length === 0 ? ( +
+
+ + + To use a cluster feature, you need to configure at least + a registry first. Please, go to{" "} + + Settings + {" "} + to do so. + +
+
+ ) : ( + <> + ( + + Select a registry + + + )} + /> + + )} )} diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx index 81c1f32c5..568792461 100644 --- a/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon, PlusIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm, useWatch } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -26,12 +32,6 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon, PlusIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const AddPortSchema = z.object({ publishedPort: z.number().int().min(1).max(65535), @@ -80,6 +80,11 @@ export const HandlePorts = ({ resolver: zodResolver(AddPortSchema), }); + const publishMode = useWatch({ + control: form.control, + name: "publishMode", + }); + useEffect(() => { form.reset({ publishedPort: data?.publishedPort ?? 0, @@ -253,6 +258,16 @@ export const HandlePorts = ({ + {publishMode === "host" && ( + + Host Mode Limitation: When using Host publish + mode, Docker Swarm has limitations that prevent proper container + updates during deployments. Old containers may not be replaced + automatically. Consider using Ingress mode instead, or be prepared + to manually stop/start the application after deployments. + + )} + +
+ + +
+ {isPaused && ( + +
+ + + Logs paused + {messageBuffer.length > 0 && ( + + ({messageBuffer.length} messages buffered) + + )} + +
+
+ )}
{ const { data: auth } = api.user.get.useQuery(); const { mutateAsync } = api.project.remove.useMutation(); const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem("projectsSort") || "createdAt-desc"; + } + return "createdAt-desc"; + }); + + useEffect(() => { + localStorage.setItem("projectsSort", sortBy); + }, [sortBy]); const filteredProjects = useMemo(() => { if (!data) return []; - return data.filter( + + // First filter by search query + const filtered = data.filter( (project) => project.name.toLowerCase().includes(searchQuery.toLowerCase()) || project.description?.toLowerCase().includes(searchQuery.toLowerCase()), ); - }, [data, searchQuery]); + + // Then sort the filtered results + const [field, direction] = sortBy.split("-"); + return [...filtered].sort((a, b) => { + let comparison = 0; + switch (field) { + case "name": + comparison = a.name.localeCompare(b.name); + break; + case "createdAt": + comparison = + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + break; + case "services": { + const aTotalServices = + a.mariadb.length + + a.mongo.length + + a.mysql.length + + a.postgres.length + + a.redis.length + + a.applications.length + + a.compose.length; + const bTotalServices = + b.mariadb.length + + b.mongo.length + + b.mysql.length + + b.postgres.length + + b.redis.length + + b.applications.length + + b.compose.length; + comparison = aTotalServices - bTotalServices; + break; + } + default: + comparison = 0; + } + return direction === "asc" ? comparison : -comparison; + }); + }, [data, searchQuery, sortBy]); return ( <> @@ -98,14 +156,40 @@ export const ShowProjects = () => {
) : ( <> -
- setSearchQuery(e.target.value)} - className="pr-10" - /> - +
+
+ setSearchQuery(e.target.value)} + className="pr-10" + /> + +
+
+ + +
{filteredProjects?.length === 0 && (
diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx index c0c45e147..38039cc6b 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -1,5 +1,6 @@ +import { useTranslation } from "next-i18next"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; - import { DropdownMenu, DropdownMenuContent, @@ -10,8 +11,6 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; -import { useTranslation } from "next-i18next"; -import { toast } from "sonner"; import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports"; import { ShowModalLogs } from "../../web-server/show-modal-logs"; diff --git a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx index a5cfb6308..282f1fddd 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx @@ -1,3 +1,11 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { ArrowRightLeft, Plus, Trash2 } from "lucide-react"; +import { useTranslation } from "next-i18next"; +import type React from "react"; +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -19,15 +27,15 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { ArrowRightLeft, Plus, Trash2 } from "lucide-react"; -import { useTranslation } from "next-i18next"; -import type React from "react"; -import { useEffect, useState } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; interface Props { children: React.ReactNode; @@ -37,6 +45,7 @@ interface Props { const PortSchema = z.object({ targetPort: z.number().min(1, "Target port is required"), publishedPort: z.number().min(1, "Published port is required"), + protocol: z.enum(["tcp", "udp", "sctp"]), }); const TraefikPortsSchema = z.object({ @@ -75,12 +84,17 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { useEffect(() => { if (currentPorts) { - form.reset({ ports: currentPorts }); + form.reset({ + ports: currentPorts.map((port) => ({ + ...port, + protocol: port.protocol as "tcp" | "udp" | "sctp", + })), + }); } }, [currentPorts, form]); const handleAddPort = () => { - append({ targetPort: 0, publishedPort: 0 }); + append({ targetPort: 0, publishedPort: 0, protocol: "tcp" }); }; const onSubmit = async (data: TraefikPortsForm) => { @@ -96,7 +110,9 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { return ( <> -
setOpen(true)}>{children}
+ @@ -143,8 +159,8 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
{fields.map((field, index) => ( - - + + { ); }} value={field.value || ""} - className="w-full dark:bg-black" placeholder="e.g. 8080" /> @@ -200,7 +215,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { ); }} value={field.value || ""} - className="w-full dark:bg-black" placeholder="e.g. 80" /> @@ -208,6 +222,42 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { )} /> + ( + + + Protocol + + + + + + + )} + />