import { useEffect, useState, FC, useCallback, useMemo } from "react"; import { ColumnDef, getCoreRowModel, useReactTable, flexRender, getFilteredRowModel, VisibilityState } from "@tanstack/react-table"; import { Button } from "@/components/ui/button"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@/components/Credenza"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuCheckboxItem } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Search, RefreshCw, Filter, Columns } from "lucide-react"; import { GetSiteResponse, Container } from "@server/routers/site"; import { useDockerSocket } from "@app/hooks/useDockerSocket"; import { FaDocker } from "react-icons/fa"; // Type definitions based on the JSON structure interface ContainerSelectorProps { site: GetSiteResponse; onContainerSelect?: (hostname: string, port?: number) => void; } export const ContainersSelector: FC = ({ site, onContainerSelect }) => { const [open, setOpen] = useState(false); const { isAvailable, containers, fetchContainers } = useDockerSocket(site); useEffect(() => { console.log("DockerSocket isAvailable:", isAvailable); if (isAvailable) { fetchContainers(); } }, [isAvailable]); if (!site || !isAvailable) { return null; } const handleContainerSelect = (container: Container, port?: number) => { // Extract hostname - prefer IP address from networks, fallback to container name const hostname = getContainerHostname(container); onContainerSelect?.(hostname, port); setOpen(false); }; return ( <> setOpen(true)} > View Docker Containers Containers in {site.name} Select any container to use as a hostname for this target. Click a port to use a port.
fetchContainers()} />
); }; const DockerContainersTable: FC<{ containers: Container[]; onContainerSelect: (container: Container, port?: number) => void; onRefresh: () => void; }> = ({ containers, onContainerSelect, onRefresh }) => { const [searchInput, setSearchInput] = useState(""); const [globalFilter, setGlobalFilter] = useState(""); const [hideContainersWithoutPorts, setHideContainersWithoutPorts] = useState(true); const [hideStoppedContainers, setHideStoppedContainers] = useState(false); const [columnVisibility, setColumnVisibility] = useState({ labels: false }); useEffect(() => { const timer = setTimeout(() => { setGlobalFilter(searchInput); }, 100); return () => clearTimeout(timer); }, [searchInput]); const getExposedPorts = useCallback((container: Container): number[] => { const ports: number[] = []; container.ports?.forEach((port) => { if (port.privatePort) { ports.push(port.privatePort); } }); return [...new Set(ports)]; // Remove duplicates }, []); const globalFilterFunction = useCallback( (row: any, columnId: string, value: string) => { const container = row.original as Container; const searchValue = value.toLowerCase(); // Search across all relevant fields const searchableFields = [ container.name, container.image, container.state, container.status, getContainerHostname(container), ...Object.keys(container.networks), ...Object.values(container.networks) .map((n) => n.ipAddress) .filter(Boolean), ...getExposedPorts(container).map((p) => p.toString()), ...Object.entries(container.labels).flat() ]; return searchableFields.some((field) => field?.toString().toLowerCase().includes(searchValue) ); }, [getExposedPorts] ); const columns: ColumnDef[] = [ { accessorKey: "name", header: "Name", cell: ({ row }) => (
{row.original.name}
) }, { accessorKey: "image", header: "Image", cell: ({ row }) => (
{row.original.image}
) }, { accessorKey: "state", header: "State", cell: ({ row }) => ( {row.original.state} ) }, { accessorKey: "networks", header: "Networks", cell: ({ row }) => { const networks = Object.keys(row.original.networks); return (
{networks.length > 0 ? networks.map((n) => ( {n} )) : "-"}
); } }, { accessorKey: "hostname", header: "Hostname/IP", enableHiding: false, cell: ({ row }) => (
{getContainerHostname(row.original)}
) }, { accessorKey: "labels", header: "Labels", cell: ({ row }) => { const labels = row.original.labels || {}; const labelEntries = Object.entries(labels); if (labelEntries.length === 0) { return -; } return (

Container Labels

{labelEntries.map(([key, value]) => (
{key}
{value || ""}
))}
); } }, { accessorKey: "ports", header: "Ports", enableHiding: false, cell: ({ row }) => { const ports = getExposedPorts(row.original); return (
{ports.slice(0, 2).map((port) => ( ))} {ports.length > 2 && ( {ports.slice(2).map((port) => ( ))} )}
); } }, { id: "actions", header: "Actions", cell: ({ row }) => { const ports = getExposedPorts(row.original); return ( ); } } ]; const initialFilters = useMemo(() => { let filtered = containers; // Filter by port visibility if (hideContainersWithoutPorts) { filtered = filtered.filter((container) => { const ports = getExposedPorts(container); return ports.length > 0; // Show only containers WITH ports }); } // Filter by container state if (hideStoppedContainers) { filtered = filtered.filter((container) => { return container.state === "running"; }); } return filtered; }, [ containers, hideContainersWithoutPorts, hideStoppedContainers, getExposedPorts ]); const table = useReactTable({ data: initialFilters, columns, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), globalFilterFn: globalFilterFunction, state: { globalFilter, columnVisibility }, onGlobalFilterChange: setGlobalFilter, onColumnVisibilityChange: setColumnVisibility }); if (initialFilters.length === 0) { return (
{(hideContainersWithoutPorts || hideStoppedContainers) && containers.length > 0 ? ( <>

No containers found matching the current filters.

{hideContainersWithoutPorts && ( )} {hideStoppedContainers && ( )}
) : (

No containers found. Make sure Docker containers are running.

)}
); } return (
setSearchInput(event.target.value) } className="pl-8" /> {searchInput && table.getFilteredRowModel().rows.length > 0 && (
{table.getFilteredRowModel().rows.length}{" "} result {table.getFilteredRowModel().rows.length !== 1 ? "s" : ""}
)}
Filter Options Ports Stopped {(hideContainersWithoutPorts || hideStoppedContainers) && ( <>
)}
Toggle Columns {table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { return ( column.toggleVisibility( !!value ) } > {column.id === "hostname" ? "Hostname/IP" : column.id} ); })}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef .header, header.getContext() )} ))} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext() )} ))} )) ) : ( {searchInput && !globalFilter ? (
Searching...
) : ( `No containers found matching "${globalFilter}".` )} )}
); }; function getContainerHostname(container: Container): string { // First, try to get IP from networks const networks = Object.values(container.networks); for (const network of networks) { if (network.ipAddress) { return network.ipAddress; } } // Fallback to container name (works in Docker networks) return container.name; }