diff --git a/app/(dashboard)/audit-log/AuditLogClient.tsx b/app/(dashboard)/audit-log/AuditLogClient.tsx index 681e77ae..14aa5abd 100644 --- a/app/(dashboard)/audit-log/AuditLogClient.tsx +++ b/app/(dashboard)/audit-log/AuditLogClient.tsx @@ -1,6 +1,8 @@ "use client"; -import { Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material"; +import { useMemo, useState } from "react"; +import { Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography } from "@mui/material"; +import SearchIcon from "@mui/icons-material/Search"; type EventRow = { id: number; @@ -10,12 +12,52 @@ type EventRow = { }; export default function AuditLogClient({ events }: { events: EventRow[] }) { + const [searchTerm, setSearchTerm] = useState(""); + + const filteredEvents = useMemo(() => { + if (!searchTerm.trim()) return events; + + const search = searchTerm.toLowerCase(); + return events.filter((event) => { + // Search in user + if (event.user.toLowerCase().includes(search)) return true; + + // Search in summary + if (event.summary.toLowerCase().includes(search)) return true; + + // Search in timestamp + if (new Date(event.created_at).toLocaleString().toLowerCase().includes(search)) return true; + + return false; + }); + }, [events, searchTerm]); + return ( Audit Log Review configuration changes and user activity. + + setSearchTerm(e.target.value)} + slotProps={{ + input: { + startAdornment: + } + }} + sx={{ + maxWidth: 500, + "& .MuiOutlinedInput-root": { + bgcolor: "rgba(20, 20, 22, 0.6)", + "&:hover": { + bgcolor: "rgba(20, 20, 22, 0.8)" + } + } + }} + /> @@ -26,13 +68,21 @@ export default function AuditLogClient({ events }: { events: EventRow[] }) { - {events.map((event) => ( - - {new Date(event.created_at).toLocaleString()} - {event.user} - {event.summary} + {filteredEvents.length === 0 ? ( + + + {searchTerm ? "No audit log entries match your search." : "No audit log entries found."} + - ))} + ) : ( + filteredEvents.map((event) => ( + + {new Date(event.created_at).toLocaleString()} + {event.user} + {event.summary} + + )) + )}
diff --git a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx index 4eebdbb7..328acf5b 100644 --- a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx +++ b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx @@ -33,6 +33,7 @@ import AddIcon from "@mui/icons-material/Add"; import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; import CloseIcon from "@mui/icons-material/Close"; +import SearchIcon from "@mui/icons-material/Search"; import { useFormState } from "react-dom"; import type { AccessList } from "@/src/lib/models/access-lists"; import type { Certificate } from "@/src/lib/models/certificates"; @@ -70,6 +71,32 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, aut const [createDialogKey, setCreateDialogKey] = useState(0); const [editHost, setEditHost] = useState(null); const [deleteHost, setDeleteHost] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + + const filteredHosts = useMemo(() => { + if (!searchTerm.trim()) return hosts; + + const search = searchTerm.toLowerCase(); + return hosts.filter((host) => { + // Search in name + if (host.name.toLowerCase().includes(search)) return true; + + // Search in domains + if (host.domains.some(domain => domain.toLowerCase().includes(search))) return true; + + // Search in upstreams + if (host.upstreams.some(upstream => upstream.toLowerCase().includes(search))) return true; + + // Search in certificate name + const certificate = host.certificate_id + ? certificates.find(c => c.id === host.certificate_id) + : null; + const certName = certificate?.name ?? "Managed by Caddy (Auto)"; + if (certName.toLowerCase().includes(search)) return true; + + return false; + }); + }, [hosts, certificates, searchTerm]); const handleToggleEnabled = async (id: number, enabled: boolean) => { await toggleProxyHostAction(id, enabled); @@ -111,6 +138,26 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, aut
+ setSearchTerm(e.target.value)} + slotProps={{ + input: { + startAdornment: + } + }} + sx={{ + maxWidth: 500, + "& .MuiOutlinedInput-root": { + bgcolor: "rgba(20, 20, 22, 0.6)", + "&:hover": { + bgcolor: "rgba(20, 20, 22, 0.8)" + } + } + }} + /> + - {hosts.length === 0 ? ( + {filteredHosts.length === 0 ? ( - No proxy hosts configured. Click "Create Host" to add one. + {searchTerm ? "No proxy hosts match your search." : "No proxy hosts configured. Click \"Create Host\" to add one."} ) : ( - hosts.map((host) => { + filteredHosts.map((host) => { const certificate = host.certificate_id ? certificates.find(c => c.id === host.certificate_id) : null; diff --git a/app/(dashboard)/redirects/RedirectsClient.tsx b/app/(dashboard)/redirects/RedirectsClient.tsx index 814a4c3a..dea4c4da 100644 --- a/app/(dashboard)/redirects/RedirectsClient.tsx +++ b/app/(dashboard)/redirects/RedirectsClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { Alert, @@ -31,6 +31,7 @@ import AddIcon from "@mui/icons-material/Add"; import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; import CloseIcon from "@mui/icons-material/Close"; +import SearchIcon from "@mui/icons-material/Search"; import { useFormState } from "react-dom"; import type { RedirectHost } from "@/src/lib/models/redirect-hosts"; import { INITIAL_ACTION_STATE } from "@/src/lib/actions"; @@ -44,6 +45,28 @@ export default function RedirectsClient({ redirects }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editRedirect, setEditRedirect] = useState(null); const [deleteRedirect, setDeleteRedirect] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + + const filteredRedirects = useMemo(() => { + if (!searchTerm.trim()) return redirects; + + const search = searchTerm.toLowerCase(); + return redirects.filter((redirect) => { + // Search in name + if (redirect.name.toLowerCase().includes(search)) return true; + + // Search in domains + if (redirect.domains.some(domain => domain.toLowerCase().includes(search))) return true; + + // Search in destination + if (redirect.destination.toLowerCase().includes(search)) return true; + + // Search in status code + if (redirect.status_code.toString().includes(search)) return true; + + return false; + }); + }, [redirects, searchTerm]); const handleToggleEnabled = async (id: number, enabled: boolean) => { await toggleRedirectAction(id, enabled); @@ -80,6 +103,26 @@ export default function RedirectsClient({ redirects }: Props) { + setSearchTerm(e.target.value)} + slotProps={{ + input: { + startAdornment: + } + }} + sx={{ + maxWidth: 500, + "& .MuiOutlinedInput-root": { + bgcolor: "rgba(20, 20, 22, 0.6)", + "&:hover": { + bgcolor: "rgba(20, 20, 22, 0.8)" + } + } + }} + /> + - {redirects.length === 0 ? ( + {filteredRedirects.length === 0 ? ( - No redirects configured. Click "Create Redirect" to add one. + {searchTerm ? "No redirects match your search." : "No redirects configured. Click \"Create Redirect\" to add one."} ) : ( - redirects.map((redirect) => ( + filteredRedirects.map((redirect) => (