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) => (