From 2934bbdd203960688e22dea79c3f273db4c6cd4d Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 21 May 2026 23:27:02 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20use=20react=20query=20for?= =?UTF-8?q?=20network=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[orgId]/settings/logs/connection/page.tsx | 381 ++++++------------ src/lib/queries.ts | 58 +++ 2 files changed, 191 insertions(+), 248 deletions(-) diff --git a/src/app/[orgId]/settings/logs/connection/page.tsx b/src/app/[orgId]/settings/logs/connection/page.tsx index 883e3daf4..b21db7465 100644 --- a/src/app/[orgId]/settings/logs/connection/page.tsx +++ b/src/app/[orgId]/settings/logs/connection/page.tsx @@ -9,26 +9,20 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; +import { logQueries } from "@app/lib/queries"; import { build } from "@server/build"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import type { QueryConnectionAuditLogResponse } from "@server/routers/auditLogs/types"; +import { useQuery } from "@tanstack/react-query"; import { ColumnDef } from "@tanstack/react-table"; import axios from "axios"; import { ArrowUpRight, Laptop, User } from "lucide-react"; import Link from "next/link"; import { useTranslations } from "next-intl"; import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState, useTransition } from "react"; - -function formatBytes(bytes: number | null): string { - if (bytes === null || bytes === undefined) return "-"; - if (bytes === 0) return "0 B"; - const units = ["B", "KB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const value = bytes / Math.pow(1024, i); - return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; -} +import { useMemo, useState, useTransition } from "react"; function formatDuration(startedAt: number, endedAt: number | null): string { if (endedAt === null || endedAt === undefined) return "Active"; @@ -54,24 +48,8 @@ export default function ConnectionLogsPage() { const { isPaidUser } = usePaidStatus(); - const [rows, setRows] = useState([]); - const [isRefreshing, setIsRefreshing] = useState(false); const [isExporting, startTransition] = useTransition(); - const [filterAttributes, setFilterAttributes] = useState<{ - protocols: string[]; - destAddrs: string[]; - clients: { id: number; name: string }[]; - resources: { id: number; name: string | null }[]; - users: { id: string; email: string | null }[]; - }>({ - protocols: [], - destAddrs: [], - clients: [], - resources: [], - users: [] - }); - // Filter states - unified object for all filters const [filters, setFilters] = useState<{ protocol?: string; destAddr?: string; @@ -86,43 +64,24 @@ export default function ConnectionLogsPage() { userId: searchParams.get("userId") || 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( "connection-audit-logs", 20 ); - // Set default date range to last 7 days 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() } }; }; @@ -131,78 +90,100 @@ export default function ConnectionLogsPage() { endDate: DateTimeValue; }>(getDefaultDateRange()); - // Trigger search with default values on component mount - useEffect(() => { - if (build === "oss") { - return; + 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(); } - const defaultRange = getDefaultDateRange(); - queryDateTime( - defaultRange.startDate, - defaultRange.endDate, - 0, - pageSize - ); - }, [orgId]); // Re-run if orgId changes + + 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, + clientId: filters.clientId ? Number(filters.clientId) : undefined, + siteResourceId: filters.siteResourceId + ? Number(filters.siteResourceId) + : undefined + }; + }, [dateRange, currentPage, pageSize, filters]); + + const { data, isFetching, isLoading, refetch } = useQuery({ + ...logQueries.connection({ + orgId: orgId as string, + filters: queryFilters + }), + enabled: isPaidUser(tierMatrix.connectionLogs) && build !== "oss" + }); + + const rows = isLoading + ? generateSampleConnectionLogs() + : (data?.log ?? []); + const totalCount = data?.pagination?.total ?? 0; + const filterAttributes = data?.filterAttributes ?? { + protocols: [], + destAddrs: [], + clients: [], + resources: [], + users: [] + }; 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 = ( @@ -224,109 +205,8 @@ export default function ConnectionLogsPage() { router.replace(`?${params.toString()}`, { scroll: false }); }; - const queryDateTime = async ( - startDate: DateTimeValue, - endDate: DateTimeValue, - page: number = currentPage, - size: number = pageSize, - filtersParam?: typeof filters - ) => { - console.log("Date range changed:", { startDate, endDate, page, size }); - if (!isPaidUser(tierMatrix.connectionLogs)) { - 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/connection`, { - 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 connection logs:", res.data); - } - } catch (error) { - toast({ - title: t("error"), - description: formatAxiosError(error), - variant: "destructive" - }); - } finally { - setIsLoading(false); - } - }; - - const refreshData = async () => { - console.log("Data refreshed"); - setIsRefreshing(true); - 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" - }); - } finally { - setIsRefreshing(false); - } - }; - const exportData = async () => { try { - // Prepare query params for export const params: any = { timeStart: dateRange.startDate?.date ? new Date(dateRange.startDate.date).toISOString() @@ -345,7 +225,6 @@ export default function ConnectionLogsPage() { } ); - // 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; @@ -363,7 +242,6 @@ export default function ConnectionLogsPage() { 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; @@ -380,7 +258,7 @@ export default function ConnectionLogsPage() { const columns: ColumnDef[] = [ { accessorKey: "startedAt", - header: ({ column }) => { + header: () => { return t("timestamp"); }, cell: ({ row }) => { @@ -395,7 +273,7 @@ export default function ConnectionLogsPage() { }, { accessorKey: "protocol", - header: ({ column }) => { + header: () => { return (
{t("protocol")} @@ -426,7 +304,7 @@ export default function ConnectionLogsPage() { }, { accessorKey: "resourceName", - header: ({ column }) => { + header: () => { return (
{t("resource")} @@ -467,7 +345,7 @@ export default function ConnectionLogsPage() { }, { accessorKey: "clientName", - header: ({ column }) => { + header: () => { return (
{t("client")} @@ -510,7 +388,7 @@ export default function ConnectionLogsPage() { }, { accessorKey: "userEmail", - header: ({ column }) => { + header: () => { return (
{t("user")} @@ -543,7 +421,7 @@ export default function ConnectionLogsPage() { }, { accessorKey: "sourceAddr", - header: ({ column }) => { + header: () => { return t("sourceAddress"); }, cell: ({ row }) => { @@ -556,7 +434,7 @@ export default function ConnectionLogsPage() { }, { accessorKey: "destAddr", - header: ({ column }) => { + header: () => { return (
{t("destinationAddress")} @@ -585,7 +463,7 @@ export default function ConnectionLogsPage() { }, { accessorKey: "duration", - header: ({ column }) => { + header: () => { return t("duration"); }, cell: ({ row }) => { @@ -606,9 +484,6 @@ export default function ConnectionLogsPage() {
- {/*
- Connection Details -
*/}
Session ID:{" "} @@ -633,18 +508,6 @@ export default function ConnectionLogsPage() {
- {/*
- Resource & Site -
*/} - {/*
- Resource:{" "} - {row.resourceName ?? "-"} - {row.resourceNiceId && ( - - ({row.resourceNiceId}) - - )} -
*/}
Client Endpoint:{" "} @@ -680,30 +543,8 @@ export default function ConnectionLogsPage() { Duration:{" "} {formatDuration(row.startedAt, row.endedAt)}
- {/*
- Resource ID:{" "} - {row.siteResourceId ?? "-"} -
*/} -
-
- {/*
- Client & Transfer -
*/} - {/*
- Bytes Sent (TX):{" "} - {formatBytes(row.bytesTx)} -
*/} - {/*
- Bytes Received (RX):{" "} - {formatBytes(row.bytesRx)} -
*/} - {/*
- Total Transfer:{" "} - {formatBytes( - (row.bytesTx ?? 0) + (row.bytesRx ?? 0) - )} -
*/}
+
); @@ -724,8 +565,8 @@ export default function ConnectionLogsPage() { title={t("connectionLogs")} searchPlaceholder={t("searchLogs")} searchColumn="protocol" - onRefresh={refreshData} - isRefreshing={isRefreshing} + onRefresh={() => refetch()} + isRefreshing={isFetching} onExport={() => startTransition(exportData)} isExporting={isExporting} onDateRangeChange={handleDateRangeChange} @@ -737,14 +578,12 @@ export default function ConnectionLogsPage() { id: "startedAt", 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={ @@ -754,3 +593,49 @@ export default function ConnectionLogsPage() { ); } + +function generateSampleConnectionLogs(): QueryConnectionAuditLogResponse["log"] { + const protocols = ["tcp", "udp", "icmp"]; + const destAddrs = [ + "10.0.0.1:22", + "10.0.0.2:80", + "10.0.0.3:443", + "192.168.1.10:3306" + ]; + + const now = Math.floor(Date.now() / 1000); + const sevenDaysAgo = now - 7 * 24 * 60 * 60; + + return Array.from({ length: 10 }, (_, i) => { + const startedAt = Math.floor( + sevenDaysAgo + Math.random() * (now - sevenDaysAgo) + ); + const active = Math.random() > 0.3; + + return { + sessionId: `session-${i}`, + siteResourceId: (i % 3) + 1, + orgId: "sample-org", + siteId: 1, + clientId: (i % 4) + 1, + clientEndpoint: `10.0.0.${i + 1}:51820`, + userId: i % 2 === 0 ? `user-${i}` : null, + sourceAddr: `192.168.1.${i + 1}:${40000 + i}`, + destAddr: destAddrs[Math.floor(Math.random() * destAddrs.length)], + protocol: + protocols[Math.floor(Math.random() * protocols.length)], + startedAt, + endedAt: active ? null : startedAt + Math.floor(Math.random() * 3600), + bytesTx: active ? null : Math.floor(Math.random() * 1024 * 1024), + bytesRx: active ? null : Math.floor(Math.random() * 1024 * 1024), + resourceName: `Resource ${(i % 3) + 1}`, + resourceNiceId: `resource-${(i % 3) + 1}`, + siteName: "Sample Site", + siteNiceId: "sample-site", + clientName: `Client ${(i % 4) + 1}`, + clientNiceId: `client-${(i % 4) + 1}`, + clientType: i % 2 === 0 ? "user" : "machine", + userEmail: i % 2 === 0 ? `user${i}@example.com` : null + }; + }); +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index c6c4bab5d..a8725354c 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -5,6 +5,7 @@ import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs"; import type { QueryAccessAuditLogResponse, QueryActionAuditLogResponse, + QueryConnectionAuditLogResponse, QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types"; import type { ListClientsResponse } from "@server/routers/client"; @@ -674,6 +675,32 @@ export const actionLogsFiltersSchema = z.object({ export type ActionLogFilters = z.output; +export const connectionLogsFiltersSchema = z.object({ + timeStart: z + .string() + .refine((val) => !isNaN(Date.parse(val)), { + error: "timeStart must be a valid ISO date string" + }) + .optional() + .catch(undefined), + timeEnd: z + .string() + .refine((val) => !isNaN(Date.parse(val)), { + error: "timeEnd must be a valid ISO date string" + }) + .optional() + .catch(undefined), + page: z.coerce.number().optional().catch(0).default(0), + pageSize: z.coerce.number().optional().catch(20).default(20), + protocol: z.string().optional().catch(undefined), + destAddr: z.string().optional().catch(undefined), + clientId: z.coerce.number().optional().catch(undefined), + siteResourceId: z.coerce.number().optional().catch(undefined), + userId: z.string().optional().catch(undefined) +}); + +export type ConnectionLogFilters = z.output; + export const logQueries = { requestAnalytics: ({ orgId, @@ -786,6 +813,37 @@ export const logQueries = { } return false; } + }), + + connection: ({ + orgId, + filters + }: { + orgId: string; + filters: ConnectionLogFilters; + }) => + queryOptions({ + queryKey: ["CONNECTION_LOGS", orgId, "ALL", filters] as const, + queryFn: async ({ signal, meta }) => { + const { page, pageSize, ...rest } = filters; + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/logs/connection`, { + params: { + ...rest, + limit: pageSize, + offset: page * pageSize + }, + signal + }); + return res.data.data; + }, + refetchInterval: (query) => { + if (query.state.data) { + return durationToMs(30, "seconds"); + } + return false; + } }) };