diff --git a/app/(dashboard)/access-lists/AccessListsClient.tsx b/app/(dashboard)/access-lists/AccessListsClient.tsx index 1c848f2c..170612c1 100644 --- a/app/(dashboard)/access-lists/AccessListsClient.tsx +++ b/app/(dashboard)/access-lists/AccessListsClient.tsx @@ -1,6 +1,7 @@ "use client"; import { Trash2 } from "lucide-react"; +import { PageHeader } from "@/components/ui/PageHeader"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -35,10 +36,10 @@ export default function AccessListsClient({ lists, pagination }: Props) { return (
-
-

Access Lists

-

Protect proxy hosts with HTTP basic authentication credentials.

-
+
{lists.map((list) => ( diff --git a/app/(dashboard)/audit-log/AuditLogClient.tsx b/app/(dashboard)/audit-log/AuditLogClient.tsx index 2005dd3c..d1592bcc 100644 --- a/app/(dashboard)/audit-log/AuditLogClient.tsx +++ b/app/(dashboard)/audit-log/AuditLogClient.tsx @@ -6,6 +6,7 @@ import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { DataTable } from "@/components/ui/DataTable"; import { SearchField } from "@/components/ui/SearchField"; +import { PageHeader } from "@/components/ui/PageHeader"; type EventRow = { id: number; @@ -96,10 +97,13 @@ export default function AuditLogClient({ events, pagination, initialSearch }: Pr ); return ( -
-

Audit Log

-

Review configuration changes and user activity.

+
+ +
{ @@ -108,6 +112,7 @@ export default function AuditLogClient({ events, pagination, initialSearch }: Pr }} placeholder="Search audit log..." /> +
- {/* Page header */} -
-

SSL/TLS Certificates

-

- Caddy automatically handles HTTPS certificates for all proxy hosts using Let's Encrypt. - Import custom certificates only when needed (internal CA, special requirements, etc.). -

-
+
+ {/* Status summary bar */} setBannerRefresh(n => n + 1); - useEffect(() => { - setSearchTerm(initialSearch); - }, [initialSearch]); + useEffect(() => { setSearchTerm(initialSearch); }, [initialSearch]); function handleSearchChange(value: string) { setSearchTerm(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { const params = new URLSearchParams(searchParams.toString()); - if (value.trim()) { - params.set("search", value.trim()); - } else { - params.delete("search"); - } + if (value.trim()) { params.set("search", value.trim()); } else { params.delete("search"); } params.set("page", "1"); router.push(`${pathname}?${params.toString()}`); }, 400); @@ -79,13 +75,16 @@ export default function L4ProxyHostsClient({ hosts, pagination, initialSearch }: id: "name", label: "Name", render: (host: L4ProxyHost) => ( - {host.name} +
+

{host.name}

+

{formatMatcher(host)}

+
), }, { id: "protocol", label: "Protocol", - width: 80, + width: 90, render: (host: L4ProxyHost) => ( {host.protocol.toUpperCase()} @@ -99,76 +98,55 @@ export default function L4ProxyHostsClient({ hosts, pagination, initialSearch }: {host.listen_address} ), }, - { - id: "matcher", - label: "Matcher", - render: (host: L4ProxyHost) => ( - {formatMatcher(host)} - ), - }, { id: "upstreams", label: "Upstreams", render: (host: L4ProxyHost) => ( - {host.upstreams[0]} - {host.upstreams.length > 1 && ` +${host.upstreams.length - 1} more`} + {host.upstreams[0]}{host.upstreams.length > 1 && ` +${host.upstreams.length - 1} more`} ), }, { - id: "actions", - label: "Actions", - align: "right" as const, - width: 150, + id: "status", + label: "Status", + width: 100, render: (host: L4ProxyHost) => ( -
+ + {host.enabled ? "Active" : "Paused"} + + ), + }, + { + id: "actions", + label: "", + align: "right" as const, + width: 80, + render: (host: L4ProxyHost) => ( +
handleToggleEnabled(host.id, checked)} /> - - - - - Duplicate - - - - - - Edit - - - - - - Delete - + Delete + + +
), }, @@ -177,89 +155,62 @@ export default function L4ProxyHostsClient({ hosts, pagination, initialSearch }: const mobileCard = (host: L4ProxyHost) => ( -
-
-
- {host.name} +
+
+
+

{host.name}

{host.protocol.toUpperCase()}
-
- handleToggleEnabled(host.id, checked)} - /> - - - - - Duplicate - - - - - - Edit - - - - - - Delete - -
+

+ {host.listen_address} → {host.upstreams[0]}{host.upstreams.length > 1 ? ` +${host.upstreams.length - 1}` : ""} +

+ + {host.enabled ? "Active" : "Paused"} + +
+
+ handleToggleEnabled(host.id, checked)} + /> + + + + + + setEditHost(host)}>Edit + { setDuplicateHost(host); setCreateOpen(true); }}>Duplicate + + setDeleteHost(host)}>Delete + +
- - {host.listen_address} {"\u2192"} {host.upstreams[0]} - {host.upstreams.length > 1 ? ` +${host.upstreams.length - 1}` : ""} -
); return ( -
+
+ setCreateOpen(true), - }} + description="Define TCP/UDP stream proxies powered by caddy-l4. Port mappings are applied automatically." + action={{ label: "Create L4 Host", onClick: () => setCreateOpen(true) }} /> - handleSearchChange(e.target.value)} - placeholder="Search L4 hosts..." - /> +
+ handleSearchChange(e.target.value)} + placeholder="Search L4 hosts..." + /> +
{ - setCreateOpen(false); - setTimeout(() => setDuplicateHost(null), 200); - signalBannerRefresh(); - }} + onClose={() => { setCreateOpen(false); setTimeout(() => setDuplicateHost(null), 200); signalBannerRefresh(); }} initialData={duplicateHost} /> @@ -284,10 +231,7 @@ export default function L4ProxyHostsClient({ hosts, pagination, initialSearch }: { - setEditHost(null); - signalBannerRefresh(); - }} + onClose={() => { setEditHost(null); signalBannerRefresh(); }} /> )} @@ -295,10 +239,7 @@ export default function L4ProxyHostsClient({ hosts, pagination, initialSearch }: { - setDeleteHost(null); - signalBannerRefresh(); - }} + onClose={() => { setDeleteHost(null); signalBannerRefresh(); }} /> )}
diff --git a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx index 2e07d03c..72f52d82 100644 --- a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx +++ b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; -import { Copy, Pencil, Trash2 } from "lucide-react"; +import { MoreHorizontal } from "lucide-react"; import type { AccessList } from "@/lib/models/access-lists"; import type { Certificate } from "@/lib/models/certificates"; import type { ProxyHost } from "@/lib/models/proxy-hosts"; @@ -14,9 +14,16 @@ import { SearchField } from "@/components/ui/SearchField"; import { DataTable } from "@/components/ui/DataTable"; import { CreateHostDialog, EditHostDialog, DeleteHostDialog } from "@/components/proxy-hosts/HostDialogs"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { Card } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; type Props = { hosts: ProxyHost[]; @@ -68,144 +75,102 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC id: "name", label: "Name", render: (host: ProxyHost) => ( -

{host.name}

- ) +
+

{host.name}

+

+ {host.domains[0]}{host.domains.length > 1 && ` +${host.domains.length - 1}`} +

+
+ ), }, { - id: "domains", - label: "Domains", - render: (host: ProxyHost) => ( -

- {host.domains[0]} - {host.domains.length > 1 && ` +${host.domains.length - 1} more`} -

- ) - }, - { - id: "upstreams", + id: "target", label: "Target", render: (host: ProxyHost) => (

- {host.upstreams[0]} - {host.upstreams.length > 1 && ` +${host.upstreams.length - 1} more`} + {host.upstreams[0]}{host.upstreams.length > 1 && ` +${host.upstreams.length - 1} more`}

- ) + ), + }, + { + id: "status", + label: "Status", + width: 100, + render: (host: ProxyHost) => ( + + {host.enabled ? "Active" : "Paused"} + + ), }, { id: "actions", - label: "Actions", + label: "", align: "right" as const, - width: 150, + width: 80, render: (host: ProxyHost) => ( -
+
handleToggleEnabled(host.id, checked)} /> - - - - - Duplicate - - - - - - Edit - - - - - - Delete - + Delete + + +
- ) - } + ), + }, ]; const mobileCard = (host: ProxyHost) => ( - -
-
-

{host.name}

-
+ + +
+
+

{host.name}

+

+ {host.domains[0]}{host.domains.length > 1 ? ` +${host.domains.length - 1}` : ""} → {host.upstreams[0]} +

+ + {host.enabled ? "Active" : "Paused"} + +
+
handleToggleEnabled(host.id, checked)} /> - - - - - Duplicate - - - - - - Edit - - - - - - Delete - + + + setEditHost(host)}>Edit + { setDuplicateHost(host); setCreateOpen(true); }}>Duplicate + + setDeleteHost(host)}>Delete + +
-

- {host.domains[0]}{host.domains.length > 1 ? ` +${host.domains.length - 1}` : ""} → {host.upstreams[0]}{host.upstreams.length > 1 ? ` +${host.upstreams.length - 1}` : ""} -

-
+ ); @@ -214,17 +179,16 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC setCreateOpen(true) - }} + action={{ label: "Create Host", onClick: () => setCreateOpen(true) }} /> - handleSearchChange(e.target.value)} - placeholder="Search hosts..." - /> +
+ handleSearchChange(e.target.value)} + placeholder="Search hosts..." + /> +
{ - setCreateOpen(false); - // Clear duplicate host after dialog transition - setTimeout(() => setDuplicateHost(null), 200); - }} + onClose={() => { setCreateOpen(false); setTimeout(() => setDuplicateHost(null), 200); }} initialData={duplicateHost} certificates={certificates} accessLists={accessLists}