Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
516 changes: 516 additions & 0 deletions apps/dokploy/components/dashboard/proxy/add-proxy.tsx

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions apps/dokploy/components/dashboard/proxy/certificate-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Loader2 } from "lucide-react";
import { UseFormReturn } from "react-hook-form";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { WildcardIndicator } from "./wildcard-indicator";

interface Props {
form: UseFormReturn<any>;
host?: string;
}

export const CertificateSelector = ({ form, host }: Props) => {
const { data: certificates, isLoading } = api.certificates.all.useQuery();
const certificateId = form.watch("certificateId");
const isWildcard = host?.startsWith("*.") || false;

// Find matching certificates for wildcard domains
const matchingCertificates = isWildcard && host
? certificates?.filter((cert) => {
if (!cert.domains || cert.domains.length === 0) return false;
const baseDomain = host.substring(2); // Remove *. prefix
return cert.domains.some((domain) => {
if (domain.startsWith("*.")) {
const certBase = domain.substring(2);
return certBase === baseDomain;
}
return domain === baseDomain;
});
})
: certificates;

return (
<FormField
control={form.control}
name="certificateId"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Certificate {isWildcard && <WildcardIndicator />}
</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
disabled={isLoading}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate (optional)" />
</SelectTrigger>
</FormControl>
<SelectContent>
{isLoading ? (
<SelectItem value="loading" disabled>
<Loader2 className="animate-spin size-4 mr-2" />
Loading certificates...
</SelectItem>
) : (
<>
<SelectItem value="">None</SelectItem>
{matchingCertificates && matchingCertificates.length > 0 ? (
matchingCertificates.map((cert) => (
<SelectItem key={cert.certificateId} value={cert.certificateId}>
<div className="flex items-center gap-2">
<span>{cert.name}</span>
{cert.isWildcard && <WildcardIndicator />}
</div>
</SelectItem>
))
) : (
<SelectItem value="no-match" disabled>
No matching certificates found
</SelectItem>
)}
</>
)}
</SelectContent>
</Select>
<FormDescription>
{isWildcard
? "Select a certificate that matches this wildcard domain"
: "Select a certificate for this domain (optional)"}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
);
};

212 changes: 212 additions & 0 deletions apps/dokploy/components/dashboard/proxy/proxy-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {
CheckCircle2,
Edit,
ExternalLink,
GlobeIcon,
Loader2,
PlusIcon,
Server,
Trash2,
XCircle,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { AddProxy } from "./add-proxy";
import { WildcardIndicator } from "./wildcard-indicator";

export const ProxyList = () => {
const { data, isLoading, refetch } = api.proxy.all.useQuery();
const { mutateAsync: deleteProxy, isLoading: isDeleting } =
api.proxy.delete.useMutation();
const { mutateAsync: testProxy, isLoading: isTesting } =
api.proxy.test.useMutation();

const handleDelete = async (proxyId: string) => {
await deleteProxy({ proxyId })
.then(() => {
toast.success("Proxy deleted successfully");
refetch();
})
.catch(() => {
toast.error("Error deleting proxy");
});
};

const handleTest = async (proxyId: string) => {
await testProxy({ proxyId })
.then((result) => {
if (result.success) {
toast.success(result.message || "Proxy test successful");
} else {
toast.error(result.message || "Proxy test failed");
}
})
.catch(() => {
toast.error("Error testing proxy");
});
};

return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-7xl mx-auto">
<div className="rounded-xl bg-background shadow-md">
<CardHeader>
<CardTitle className="text-xl flex flex-row gap-2">
<GlobeIcon className="size-6 text-muted-foreground self-center" />
Reverse Proxies
</CardTitle>
<CardDescription>
Manage reverse proxy configurations for your services
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<>
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<GlobeIcon className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground text-center">
You don't have any proxies configured
</span>
<AddProxy />
</div>
) : (
<div className="flex flex-col gap-4 min-h-[25vh]">
<div className="flex flex-col gap-4 rounded-lg">
{data?.map((proxy) => (
<div
key={proxy.proxyId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center gap-4 flex-1">
<div className="flex flex-col gap-2 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">
{proxy.name}
</span>
{proxy.isWildcard && (
<WildcardIndicator />
)}
<Badge
variant={
proxy.status === "active"
? "default"
: proxy.status === "inactive"
? "secondary"
: "destructive"
}
>
{proxy.status}
</Badge>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<GlobeIcon className="size-3" />
<span>{proxy.host}</span>
{proxy.path && proxy.path !== "/" && (
<span>• {proxy.path}</span>
)}
{proxy.https && (
<Badge variant="outline" className="text-xs">
HTTPS
</Badge>
)}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Server className="size-3" />
<span>
{proxy.targetType === "url"
? proxy.targetUrl
: `${proxy.targetType}: ${proxy.targetId || "N/A"}`}
</span>
</div>
</div>
</div>

<div className="flex flex-row gap-1">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10"
onClick={() => handleTest(proxy.proxyId)}
disabled={isTesting}
>
<ExternalLink className="size-4 text-primary group-hover:text-blue-500" />
</Button>
</TooltipTrigger>
<TooltipContent>Test Proxy</TooltipContent>
</Tooltip>
</TooltipProvider>
<AddProxy proxyId={proxy.proxyId}>
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10"
>
<Edit className="size-4 text-primary group-hover:text-blue-500" />
</Button>
</AddProxy>
<DialogAction
title="Delete Proxy"
description="Are you sure you want to delete this proxy? This action cannot be undone."
type="destructive"
onClick={async () => {
await handleDelete(proxy.proxyId);
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isDeleting}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
))}
</div>

<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
<AddProxy />
</div>
</div>
)}
</>
)}
</CardContent>
</div>
</Card>
</div>
);
};

Loading