From 5bf8b336c50b27c9578828846a372a103678751e Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 21 May 2026 23:05:34 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20useQuery=20for=20fetching?= =?UTF-8?q?=20access=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/logs/access/page.tsx | 341 +++++++----------- src/components/LogDataTable.tsx | 2 +- src/lib/queries.ts | 5 +- 3 files changed, 128 insertions(+), 220 deletions(-) diff --git a/src/app/[orgId]/settings/logs/access/page.tsx b/src/app/[orgId]/settings/logs/access/page.tsx index 72b49d3f4..267ad8556 100644 --- a/src/app/[orgId]/settings/logs/access/page.tsx +++ b/src/app/[orgId]/settings/logs/access/page.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@app/components/ui/button"; import { toast } from "@app/hooks/useToast"; -import { useState, useRef, useEffect, useTransition } from "react"; +import { useState, useTransition, useMemo } from "react"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useParams, useRouter, useSearchParams } from "next/navigation"; @@ -20,6 +20,9 @@ import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { logQueries } from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; +import type { QueryAccessAuditLogResponse } from "@server/routers/auditLogs/types"; export default function GeneralPage() { const router = useRouter(); @@ -30,23 +33,8 @@ export default function GeneralPage() { const { isPaidUser } = usePaidStatus(); - const [rows, setRows] = useState([]); - const [isRefreshing, startRefreshTransition] = useTransition(); const [isExporting, startTransition] = useTransition(); - const [filterAttributes, setFilterAttributes] = useState<{ - actors: string[]; - resources: { - id: number; - name: string | null; - }[]; - locations: string[]; - }>({ - actors: [], - resources: [], - locations: [] - }); - // Filter states - unified object for all filters const [filters, setFilters] = useState<{ action?: string; type?: string; @@ -61,40 +49,21 @@ export default function GeneralPage() { actor: searchParams.get("actor") || undefined }); - // Pagination state - const [totalCount, setTotalCount] = useState(0); const [currentPage, setCurrentPage] = useState(0); - const [isLoading, setIsLoading] = useState(false); - - // Initialize page size from storage or default const [pageSize, setPageSize] = useStoredPageSize("access-audit-logs", 20); - // Set default date range to last 24 hours const getDefaultDateRange = () => { - // if the time is in the url params, use that instead const startParam = searchParams.get("start"); const endParam = searchParams.get("end"); if (startParam && endParam) { return { - startDate: { - date: new Date(startParam) - }, - endDate: { - date: new Date(endParam) - } + startDate: { date: new Date(startParam) }, + endDate: { date: new Date(endParam) } }; } - - const now = new Date(); - const lastWeek = getSevenDaysAgo(); - return { - startDate: { - date: lastWeek - }, - endDate: { - date: now - } + startDate: { date: getSevenDaysAgo() }, + endDate: { date: new Date() } }; }; @@ -103,75 +72,95 @@ export default function GeneralPage() { endDate: DateTimeValue; }>(getDefaultDateRange()); - // Trigger search with default values on component mount - useEffect(() => { - const defaultRange = getDefaultDateRange(); - queryDateTime( - defaultRange.startDate, - defaultRange.endDate, - 0, - pageSize - ); - }, [orgId]); // Re-run if orgId changes + const queryFilters = useMemo(() => { + let timeStart: string | undefined; + let timeEnd: string | undefined; + + if (dateRange.startDate?.date) { + const dt = new Date(dateRange.startDate.date); + if (dateRange.startDate.time) { + const [h, m, s] = dateRange.startDate.time + .split(":") + .map(Number); + dt.setHours(h, m, s || 0); + } + timeStart = dt.toISOString(); + } + + if (dateRange.endDate?.date) { + const dt = new Date(dateRange.endDate.date); + if (dateRange.endDate.time) { + const [h, m, s] = dateRange.endDate.time.split(":").map(Number); + dt.setHours(h, m, s || 0); + } else { + const now = new Date(); + dt.setHours( + now.getHours(), + now.getMinutes(), + now.getSeconds(), + now.getMilliseconds() + ); + } + timeEnd = dt.toISOString(); + } + + return { + timeStart, + timeEnd, + page: currentPage, + pageSize, + ...filters, + resourceId: filters.resourceId + ? Number(filters.resourceId) + : undefined + }; + }, [dateRange, currentPage, pageSize, filters]); + + const { data, isFetching, isLoading, refetch } = useQuery({ + ...logQueries.access({ + orgId: orgId as string, + filters: queryFilters + }), + enabled: isPaidUser(tierMatrix.accessLogs) && build !== "oss" + }); + + const rows = isLoading ? generateSampleAccessLogs() : (data?.log ?? []); + const totalCount = data?.pagination?.total ?? 0; + const filterAttributes = data?.filterAttributes ?? { + actors: [], + resources: [], + locations: [] + }; const handleDateRangeChange = ( startDate: DateTimeValue, endDate: DateTimeValue ) => { setDateRange({ startDate, endDate }); - setCurrentPage(0); // Reset to first page when filtering - // put the search params in the url for the time + setCurrentPage(0); updateUrlParamsForAllFilters({ start: startDate.date?.toISOString() || "", end: endDate.date?.toISOString() || "" }); - - queryDateTime(startDate, endDate, 0, pageSize); }; - // Handle page changes const handlePageChange = (newPage: number) => { setCurrentPage(newPage); - queryDateTime( - dateRange.startDate, - dateRange.endDate, - newPage, - pageSize - ); }; - // Handle page size changes const handlePageSizeChange = (newPageSize: number) => { setPageSize(newPageSize); - setCurrentPage(0); // Reset to first page when changing page size - queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize); + setCurrentPage(0); }; - // Handle filter changes generically const handleFilterChange = ( filterType: keyof typeof filters, value: string | undefined ) => { - // Create new filters object with updated value - const newFilters = { - ...filters, - [filterType]: value - }; - + const newFilters = { ...filters, [filterType]: value }; setFilters(newFilters); - setCurrentPage(0); // Reset to first page when filtering - - // Update URL params + setCurrentPage(0); updateUrlParamsForAllFilters(newFilters); - - // Trigger new query with updated filters (pass directly to avoid async state issues) - queryDateTime( - dateRange.startDate, - dateRange.endDate, - 0, - pageSize, - newFilters - ); }; const updateUrlParamsForAllFilters = ( @@ -193,110 +182,8 @@ export default function GeneralPage() { router.replace(`?${params.toString()}`, { scroll: false }); }; - const queryDateTime = async ( - startDate: DateTimeValue, - endDate: DateTimeValue, - page: number = currentPage, - size: number = pageSize, - filtersParam?: { - action?: string; - type?: string; - resourceId?: string; - location?: string; - actor?: string; - } - ) => { - console.log("Date range changed:", { startDate, endDate, page, size }); - if (!isPaidUser(tierMatrix.accessLogs) || build === "oss") { - console.log( - "Access denied: subscription inactive or license locked" - ); - return; - } - - setIsLoading(true); - - try { - // Use the provided filters or fall back to current state - const activeFilters = filtersParam || filters; - - // Convert the date/time values to API parameters - const params: any = { - limit: size, - offset: page * size, - ...activeFilters - }; - - if (startDate?.date) { - const startDateTime = new Date(startDate.date); - if (startDate.time) { - const [hours, minutes, seconds] = startDate.time - .split(":") - .map(Number); - startDateTime.setHours(hours, minutes, seconds || 0); - } - params.timeStart = startDateTime.toISOString(); - } - - if (endDate?.date) { - const endDateTime = new Date(endDate.date); - if (endDate.time) { - const [hours, minutes, seconds] = endDate.time - .split(":") - .map(Number); - endDateTime.setHours(hours, minutes, seconds || 0); - } else { - // If no time is specified, set to NOW - const now = new Date(); - endDateTime.setHours( - now.getHours(), - now.getMinutes(), - now.getSeconds(), - now.getMilliseconds() - ); - } - params.timeEnd = endDateTime.toISOString(); - } - - const res = await api.get(`/org/${orgId}/logs/access`, { params }); - if (res.status === 200) { - setRows(res.data.data.log || []); - setTotalCount(res.data.data.pagination?.total || 0); - setFilterAttributes(res.data.data.filterAttributes); - console.log("Fetched logs:", res.data); - } - } catch (error) { - toast({ - title: t("error"), - description: t("Failed to filter logs"), - variant: "destructive" - }); - } finally { - setIsLoading(false); - } - }; - - const refreshData = async () => { - try { - // Refresh data with current date range and pagination - await queryDateTime( - dateRange.startDate, - dateRange.endDate, - currentPage, - pageSize - ); - } catch (error) { - toast({ - title: t("error"), - description: t("refreshError"), - variant: "destructive" - }); - } - }; - const exportData = async () => { try { - // Prepare query params for export const params: any = { timeStart: dateRange.startDate?.date ? new Date(dateRange.startDate.date).toISOString() @@ -312,7 +199,6 @@ export default function GeneralPage() { params }); - // Create a URL for the blob and trigger a download const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement("a"); link.href = url; @@ -330,7 +216,6 @@ export default function GeneralPage() { const data = error.response.data; if (data instanceof Blob && data.type === "application/json") { - // Parse the Blob as JSON const text = await data.text(); const errorData = JSON.parse(text); apiErrorMessage = errorData.message; @@ -347,7 +232,7 @@ export default function GeneralPage() { const columns: ColumnDef[] = [ { accessorKey: "timestamp", - header: ({ column }) => { + header: () => { return t("timestamp"); }, cell: ({ row }) => { @@ -362,7 +247,7 @@ export default function GeneralPage() { }, { accessorKey: "action", - header: ({ column }) => { + header: () => { return (
{t("action")} @@ -375,7 +260,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("action", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -392,13 +276,11 @@ export default function GeneralPage() { }, { accessorKey: "ip", - header: ({ column }) => { - return t("ip"); - } + header: () => t("ip") }, { accessorKey: "location", - header: ({ column }) => { + header: () => { return (
{t("location")} @@ -413,7 +295,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("location", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -438,7 +319,7 @@ export default function GeneralPage() { }, { accessorKey: "resourceName", - header: ({ column }) => { + header: () => { return (
{t("resource")} @@ -451,7 +332,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("resourceId", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -477,7 +357,7 @@ export default function GeneralPage() { }, { accessorKey: "type", - header: ({ column }) => { + header: () => { return (
{t("type")} @@ -496,7 +376,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("type", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -514,7 +393,7 @@ export default function GeneralPage() { }, { accessorKey: "actor", - header: ({ column }) => { + header: () => { return (
{t("actor")} @@ -527,7 +406,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("actor", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -555,16 +433,12 @@ export default function GeneralPage() { }, { accessorKey: "actorId", - header: ({ column }) => { - return t("actorId"); - }, - cell: ({ row }) => { - return ( - - {row.original.actorId || "-"} - - ); - } + header: () => t("actorId"), + cell: ({ row }) => ( + + {row.original.actorId || "-"} + + ) } ]; @@ -610,13 +484,10 @@ export default function GeneralPage() { columns={columns} data={rows} title={t("accessLogs")} - onRefresh={() => startRefreshTransition(refreshData)} - isRefreshing={isRefreshing} + onRefresh={() => refetch()} + isRefreshing={isFetching} onExport={() => startTransition(exportData)} isExporting={isExporting} - // isExportDisabled={ // not disabling this because the user should be able to click the button and get the feedback about needing to upgrade the plan - // !isPaidUser(tierMatrix.accessLogs) || build === "oss" - // } onDateRangeChange={handleDateRangeChange} dateRange={{ start: dateRange.startDate, @@ -626,14 +497,12 @@ export default function GeneralPage() { id: "timestamp", desc: true }} - // Server-side pagination props totalCount={totalCount} currentPage={currentPage} pageSize={pageSize} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} isLoading={isLoading} - // Row expansion props expandable={true} renderExpandedRow={renderExpandedRow} disabled={!isPaidUser(tierMatrix.accessLogs) || build === "oss"} @@ -641,3 +510,41 @@ export default function GeneralPage() { ); } + +function generateSampleAccessLogs(): QueryAccessAuditLogResponse["log"] { + const locations = ["US", "DE", "GB", "FR", "JP", "CA", "AU"]; + const types = ["password", "pincode", "login", "whitelistedEmail", "ssh"]; + const actors = [ + "alice@example.com", + "bob@example.com", + "carol@example.com", + null + ]; + + const now = Math.floor(Date.now() / 1000); + const sevenDaysAgo = now - 7 * 24 * 60 * 60; + + return Array.from({ length: 10 }, (_, i) => { + const action = Math.random() > 0.3; + const actor = actors[Math.floor(Math.random() * actors.length)]; + + return { + timestamp: Math.floor( + sevenDaysAgo + Math.random() * (now - sevenDaysAgo) + ), + action, + orgId: "sample-org", + actorType: actor ? "user" : null, + actor, + actorId: actor ? `user-${i}` : null, + resourceId: Math.floor(Math.random() * 5) + 1, + resourceNiceId: `resource-${(i % 3) + 1}`, + resourceName: `Resource ${(i % 3) + 1}`, + ip: `${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, + location: locations[Math.floor(Math.random() * locations.length)], + userAgent: "Mozilla/5.0", + metadata: null, + type: types[Math.floor(Math.random() * types.length)] + }; + }); +} diff --git a/src/components/LogDataTable.tsx b/src/components/LogDataTable.tsx index 767f0a145..f2bc5b0da 100644 --- a/src/components/LogDataTable.tsx +++ b/src/components/LogDataTable.tsx @@ -539,7 +539,7 @@ export function LogDataTable({ {isLoading && ( <> -
+
diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 671f6d926..99c3af21d 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -644,7 +644,8 @@ export const accessLogsFiltersSchema = z.object({ resourceId: z.coerce.number().optional().catch(undefined), action: z.string().optional().catch(undefined), location: z.string().optional().catch(undefined), - actor: z.string().optional().catch(undefined) + actor: z.string().optional().catch(undefined), + type: z.string().optional().catch(undefined) }); export type AccessLogFilters = z.output; @@ -707,7 +708,7 @@ export const logQueries = { } }), - access: ({ orgId, filters }: { orgId: string; filters: HttpLogFilters }) => + access: ({ orgId, filters }: { orgId: string; filters: AccessLogFilters }) => queryOptions({ queryKey: ["ACCESS_LOGS", orgId, "ALL", filters] as const, queryFn: async ({ signal, meta }) => {