From 65e4fe91b995f42604faa8b02defcc51deecd4c8 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 20 Aug 2026 23:59:31 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20add=20`ip=20is`=20column?= =?UTF-8?q?=20filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/auditLogs/queryRequestAuditLog.ts | 5 +-- .../[orgId]/settings/logs/request/page.tsx | 33 +++++++++++++++---- src/components/ColumnMultiFilterButton.tsx | 26 +++++++++++++-- src/lib/queries.ts | 5 +-- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/server/routers/auditLogs/queryRequestAuditLog.ts b/server/routers/auditLogs/queryRequestAuditLog.ts index 24116aa31..aa7769bdf 100644 --- a/server/routers/auditLogs/queryRequestAuditLog.ts +++ b/server/routers/auditLogs/queryRequestAuditLog.ts @@ -82,7 +82,7 @@ export const queryAccessAuditLogsQuery = z.strictObject({ .default("0") .transform(Number) .pipe(z.int().nonnegative()), - ips: z + ip: z .preprocess((val) => { if (val === undefined || val === null || val === "") { return undefined; @@ -146,7 +146,8 @@ function getWhere(data: Q) { data.path ? eq(requestAuditLog.path, data.path) : undefined, data.action !== undefined ? eq(requestAuditLog.action, data.action) - : undefined + : undefined, + data.ip ? inArray(requestAuditLog.ip, data.ip) : undefined ); } diff --git a/src/app/[orgId]/settings/logs/request/page.tsx b/src/app/[orgId]/settings/logs/request/page.tsx index d1300f73f..5918060fe 100644 --- a/src/app/[orgId]/settings/logs/request/page.tsx +++ b/src/app/[orgId]/settings/logs/request/page.tsx @@ -22,6 +22,7 @@ import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types"; import { ColumnFilterButton } from "@app/components/ColumnFilterButton"; import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji"; +import { ColumnMultiFilterButton } from "@app/components/ColumnMultiFilterButton"; export default function GeneralPage() { const router = useRouter(); @@ -44,6 +45,7 @@ export default function GeneralPage() { method?: string; reason?: string; path?: string; + ip?: string[]; }>({ action: searchParams.get("action") || undefined, host: searchParams.get("host") || undefined, @@ -52,7 +54,8 @@ export default function GeneralPage() { actor: searchParams.get("actor") || undefined, method: searchParams.get("method") || undefined, reason: searchParams.get("reason") || undefined, - path: searchParams.get("path") || undefined + path: searchParams.get("path") || undefined, + ip: searchParams.getAll("ip") || undefined }); const getDefaultDateRange = () => { @@ -159,7 +162,7 @@ export default function GeneralPage() { const handleFilterChange = ( filterType: keyof typeof filters, - value: string | undefined + value: string | string[] | undefined ) => { const newFilters = { ...filters, [filterType]: value }; setFilters(newFilters); @@ -177,10 +180,13 @@ export default function GeneralPage() { ) => { const params = new URLSearchParams(searchParams); Object.entries(newFilters).forEach(([key, value]) => { - if (value) { + params.delete(key); + if (typeof value === "string") { params.set(key, value); - } else { - params.delete(key); + } else if (typeof value !== "undefined" && "length" in value) { + for (const element of value) { + params.append(key, element); + } } }); router.replace(`?${params.toString()}`, { scroll: false }); @@ -329,7 +335,22 @@ export default function GeneralPage() { }, { accessorKey: "ip", - header: ({ column }) => {t("ip")} + header: ({ column }) => ( + + ({ + label: ip, + value: ip + }))} + label={t("ip")} + allowArbitraryValues + selectedValues={filters.ip ?? []} + onSelectedValuesChange={(value) => + handleFilterChange("ip", value) + } + /> + + ) }, { accessorKey: "location", diff --git a/src/components/ColumnMultiFilterButton.tsx b/src/components/ColumnMultiFilterButton.tsx index d6ebaa482..ba1c4b06a 100644 --- a/src/components/ColumnMultiFilterButton.tsx +++ b/src/components/ColumnMultiFilterButton.tsx @@ -35,6 +35,7 @@ type ColumnMultiFilterButtonProps = { emptyMessage?: string; className?: string; label: string; + allowArbitraryValues?: boolean; }; export function ColumnMultiFilterButton({ @@ -44,11 +45,26 @@ export function ColumnMultiFilterButton({ searchPlaceholder = "Search...", emptyMessage = "No options found", className, - label + label, + allowArbitraryValues }: ColumnMultiFilterButtonProps) { const [open, setOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); const t = useTranslations(); + const visibleOptions = useMemo(() => { + const newOptions = [...options]; + + if (allowArbitraryValues && searchQuery.trim().length > 0) { + newOptions.push({ + label: searchQuery, + value: searchQuery + }); + } + + return newOptions; + }, [options, allowArbitraryValues, searchQuery]); + const selectedSet = useMemo( () => new Set(selectedValues), [selectedValues] @@ -108,7 +124,11 @@ export function ColumnMultiFilterButton({ align="start" > - + {emptyMessage} @@ -123,7 +143,7 @@ export function ColumnMultiFilterButton({ {t("accessFilterClear")} )} - {options.map((option) => ( + {visibleOptions.map((option) => ( ;