diff --git a/messages/en-US.json b/messages/en-US.json index aa8f902ff..2f67eca21 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1646,6 +1646,7 @@ "certificateStatus": "Certificate", "certificateStatusAutoRefreshHint": "Status refreshes automatically.", "loading": "Loading", + "loadingEllipsis": "Loading...", "loadingAnalytics": "Loading Analytics", "restart": "Restart", "domains": "Domains", diff --git a/src/app/[orgId]/settings/logs/access/page.tsx b/src/app/[orgId]/settings/logs/access/page.tsx index 826e11c17..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, setIsRefreshing] = useState(false); 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,114 +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 () => { - 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() @@ -316,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; @@ -334,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; @@ -351,7 +232,7 @@ export default function GeneralPage() { const columns: ColumnDef[] = [ { accessorKey: "timestamp", - header: ({ column }) => { + header: () => { return t("timestamp"); }, cell: ({ row }) => { @@ -366,7 +247,7 @@ export default function GeneralPage() { }, { accessorKey: "action", - header: ({ column }) => { + header: () => { return (
{t("action")} @@ -379,7 +260,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("action", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -396,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")} @@ -417,7 +295,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("location", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -442,7 +319,7 @@ export default function GeneralPage() { }, { accessorKey: "resourceName", - header: ({ column }) => { + header: () => { return (
{t("resource")} @@ -455,7 +332,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("resourceId", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -481,7 +357,7 @@ export default function GeneralPage() { }, { accessorKey: "type", - header: ({ column }) => { + header: () => { return (
{t("type")} @@ -500,7 +376,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("type", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -518,7 +393,7 @@ export default function GeneralPage() { }, { accessorKey: "actor", - header: ({ column }) => { + header: () => { return (
{t("actor")} @@ -531,7 +406,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("actor", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -559,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 || "-"} + + ) } ]; @@ -614,13 +484,10 @@ export default function GeneralPage() { columns={columns} data={rows} title={t("accessLogs")} - onRefresh={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, @@ -630,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"} @@ -645,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/app/[orgId]/settings/logs/action/page.tsx b/src/app/[orgId]/settings/logs/action/page.tsx index 4a8f50f63..7ccce8877 100644 --- a/src/app/[orgId]/settings/logs/action/page.tsx +++ b/src/app/[orgId]/settings/logs/action/page.tsx @@ -10,14 +10,17 @@ import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { toast } from "@app/hooks/useToast"; 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 { QueryActionAuditLogResponse } from "@server/routers/auditLogs/types"; +import { useQuery } from "@tanstack/react-query"; import { ColumnDef } from "@tanstack/react-table"; import axios from "axios"; import { Key, User } from "lucide-react"; import { useTranslations } from "next-intl"; import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState, useTransition } from "react"; +import { useMemo, useState, useTransition } from "react"; export default function GeneralPage() { const router = useRouter(); @@ -28,18 +31,8 @@ export default function GeneralPage() { const { isPaidUser } = usePaidStatus(); - const [rows, setRows] = useState([]); - const [isRefreshing, setIsRefreshing] = useState(false); const [isExporting, startTransition] = useTransition(); - const [filterAttributes, setFilterAttributes] = useState<{ - actors: string[]; - actions: string[]; - }>({ - actors: [], - actions: [] - }); - // Filter states - unified object for all filters const [filters, setFilters] = useState<{ action?: string; actor?: string; @@ -48,40 +41,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("action-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() } }; }; @@ -90,78 +64,90 @@ export default function GeneralPage() { 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 + }; + }, [dateRange, currentPage, pageSize, filters]); + + const { data, isFetching, isLoading, refetch } = useQuery({ + ...logQueries.action({ + orgId: orgId as string, + filters: queryFilters + }), + enabled: isPaidUser(tierMatrix.actionLogs) && build !== "oss" + }); + + const rows = isLoading ? generateSampleActionLogs() : (data?.log ?? []); + const totalCount = data?.pagination?.total ?? 0; + const filterAttributes = { + actors: data?.filterAttributes?.actors ?? [] + }; 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 = ( @@ -183,110 +169,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; - actor?: string; - } - ) => { - console.log("Date range changed:", { startDate, endDate, page, size }); - if (!isPaidUser(tierMatrix.actionLogs)) { - 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/action`, { 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 () => { - 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() @@ -302,7 +186,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; @@ -320,7 +203,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; @@ -337,7 +219,7 @@ export default function GeneralPage() { const columns: ColumnDef[] = [ { accessorKey: "timestamp", - header: ({ column }) => { + header: () => { return t("timestamp"); }, cell: ({ row }) => { @@ -352,22 +234,16 @@ export default function GeneralPage() { }, { accessorKey: "action", - header: ({ column }) => { + header: () => { return (
{t("action")} ({ - label: - action.charAt(0).toUpperCase() + - action.slice(1), - value: action - }))} + options={[]} selectedValue={filters.action} onValueChange={(value) => handleFilterChange("action", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -385,7 +261,7 @@ export default function GeneralPage() { }, { accessorKey: "actor", - header: ({ column }) => { + header: () => { return (
{t("actor")} @@ -398,7 +274,6 @@ export default function GeneralPage() { onValueChange={(value) => handleFilterChange("actor", value) } - // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" /> @@ -420,7 +295,7 @@ export default function GeneralPage() { }, { accessorKey: "actorId", - header: ({ column }) => { + header: () => { return t("actorId"); }, cell: ({ row }) => { @@ -469,12 +344,9 @@ export default function GeneralPage() { title={t("actionLogs")} searchPlaceholder={t("searchLogs")} searchColumn="action" - onRefresh={refreshData} - isRefreshing={isRefreshing} + onRefresh={() => refetch()} + isRefreshing={isFetching} onExport={() => startTransition(exportData)} - // 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.logExport) || build === "oss" - // } isExporting={isExporting} onDateRangeChange={handleDateRangeChange} dateRange={{ @@ -485,14 +357,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.actionLogs) || build === "oss"} @@ -500,3 +370,39 @@ export default function GeneralPage() { ); } + +function generateSampleActionLogs(): QueryActionAuditLogResponse["log"] { + const actions = [ + "createResource", + "deleteResource", + "updateResource", + "createSite", + "deleteSite", + "inviteUser", + "removeUser" + ]; + const actors = [ + "alice@example.com", + "bob@example.com", + "carol@example.com" + ]; + + const now = Math.floor(Date.now() / 1000); + const sevenDaysAgo = now - 7 * 24 * 60 * 60; + + return Array.from({ length: 10 }, (_, i) => { + const actor = actors[Math.floor(Math.random() * actors.length)]; + + return { + timestamp: Math.floor( + sevenDaysAgo + Math.random() * (now - sevenDaysAgo) + ), + action: actions[Math.floor(Math.random() * actions.length)], + orgId: "sample-org", + actorType: "user", + actor, + actorId: `user-${i}`, + metadata: null + }; + }); +} 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/app/[orgId]/settings/logs/request/page.tsx b/src/app/[orgId]/settings/logs/request/page.tsx index 6380d0473..2196a0342 100644 --- a/src/app/[orgId]/settings/logs/request/page.tsx +++ b/src/app/[orgId]/settings/logs/request/page.tsx @@ -9,14 +9,17 @@ import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; import { useTranslations } from "next-intl"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; +import { logQueries } from "@app/lib/queries"; import { ColumnDef } from "@tanstack/react-table"; +import { useQuery } from "@tanstack/react-query"; import axios from "axios"; import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react"; import Link from "next/link"; import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState, useTransition } from "react"; +import { useMemo, useState, useTransition } from "react"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { build } from "@server/build"; +import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types"; export default function GeneralPage() { const router = useRouter(); @@ -25,36 +28,11 @@ export default function GeneralPage() { const { orgId } = useParams(); const searchParams = useSearchParams(); - const [rows, setRows] = useState([]); - const [isRefreshing, setIsRefreshing] = useState(false); const [isExporting, startTransition] = useTransition(); - // 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("request-audit-logs", 20); - const [filterAttributes, setFilterAttributes] = useState<{ - actors: string[]; - resources: { - id: number; - name: string | null; - }[]; - locations: string[]; - hosts: string[]; - paths: string[]; - }>({ - actors: [], - resources: [], - locations: [], - hosts: [], - paths: [] - }); - - // Filter states - unified object for all filters const [filters, setFilters] = useState<{ action?: string; resourceId?: string; @@ -75,32 +53,18 @@ export default function GeneralPage() { path: searchParams.get("path") || undefined }); - // 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() } }; }; @@ -109,80 +73,97 @@ export default function GeneralPage() { 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, + resourceId: filters.resourceId + ? Number(filters.resourceId) + : undefined + }; + }, [dateRange, currentPage, pageSize, filters]); + + const { data, isFetching, isLoading, refetch } = useQuery({ + ...logQueries.requests({ + orgId: orgId as string, + filters: queryFilters + }), + enabled: build !== "oss" + }); + + const rows = isLoading ? generateSampleRequestLogs() : (data?.log ?? []); + const totalCount = data?.pagination?.total ?? 0; + const filterAttributes = data?.filterAttributes ?? { + actors: [], + resources: [], + locations: [], + hosts: [], + paths: [] + }; 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 ) => { - console.log(`${filterType} filter changed:`, value); - - // 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 = ( @@ -204,101 +185,6 @@ 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; - } - ) => { - console.log("Date range changed:", { startDate, endDate, page, size }); - 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/request`, { 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 () => { - 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 @@ -781,8 +667,8 @@ export default function GeneralPage() { title={t("requestLogs")} searchPlaceholder={t("searchLogs")} searchColumn="host" - onRefresh={refreshData} - isRefreshing={isRefreshing} + onRefresh={() => refetch()} + isRefreshing={isFetching} onExport={() => startTransition(exportData)} isExporting={isExporting} onDateRangeChange={handleDateRangeChange} @@ -794,7 +680,6 @@ export default function GeneralPage() { id: "timestamp", desc: true }} - // Server-side pagination props totalCount={totalCount} currentPage={currentPage} onPageChange={handlePageChange} @@ -808,3 +693,63 @@ export default function GeneralPage() { ); } + +function generateSampleRequestLogs(): QueryRequestAuditLogResponse["log"] { + const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"]; + const paths = [ + "/api/v1/users", + "/dashboard", + "/settings", + "/health", + "/metrics" + ]; + const hosts = ["app.example.com", "api.example.com", "admin.example.com"]; + const locations = ["US", "DE", "GB", "FR", "JP", "CA", "AU"]; + const allowedReasons = [100, 101, 102, 103, 104, 105, 106, 107, 108]; + const deniedReasons = [201, 202, 203, 204, 205, 299]; + 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 reason = action + ? allowedReasons[Math.floor(Math.random() * allowedReasons.length)] + : deniedReasons[Math.floor(Math.random() * deniedReasons.length)]; + const actor = actors[Math.floor(Math.random() * actors.length)]; + + return { + timestamp: Math.floor( + sevenDaysAgo + Math.random() * (now - sevenDaysAgo) + ), + action, + reason, + orgId: "sample-org", + actorType: actor ? "user" : null, + actor, + actorId: actor ? `user-${i}` : null, + resourceId: Math.floor(Math.random() * 5) + 1, + siteResourceId: null, + 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, + headers: null, + query: null, + originalRequestURL: null, + scheme: "https", + host: hosts[Math.floor(Math.random() * hosts.length)], + path: paths[Math.floor(Math.random() * paths.length)], + method: methods[Math.floor(Math.random() * methods.length)], + tls: true + }; + }); +} diff --git a/src/components/LogDataTable.tsx b/src/components/LogDataTable.tsx index 14e87ff75..f2bc5b0da 100644 --- a/src/components/LogDataTable.tsx +++ b/src/components/LogDataTable.tsx @@ -28,6 +28,7 @@ import { ChevronRight, Download, Loader, + LoaderIcon, RefreshCw } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -427,7 +428,7 @@ export function LogDataTable({ )}
- + {table.getHeaderGroups().map((headerGroup) => ( @@ -535,6 +536,19 @@ export function LogDataTable({ )}
+ + {isLoading && ( + <> +
+
+
+ + {t("loadingEllipsis")} +
+
+ + )} +
; +export type LogAnalyticsFilters = z.output; + +export const httpLogsFiltersSchema = 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), + resourceId: z.coerce.number().optional().catch(undefined), + action: z.string().optional().catch(undefined), + host: z.string().optional().catch(undefined), + location: z.string().optional().catch(undefined), + actor: z.string().optional().catch(undefined), + method: z.string().optional().catch(undefined), + reason: z.string().optional().catch(undefined), + path: z.string().optional().catch(undefined) +}); + +export type HttpLogFilters = z.output; + +export const accessLogsFiltersSchema = 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), + 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), + type: z.string().optional().catch(undefined) +}); + +export type AccessLogFilters = z.output; + +export const actionLogsFiltersSchema = 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), + action: z.string().optional().catch(undefined), + actor: z.string().optional().catch(undefined) +}); + +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: ({ @@ -600,7 +710,7 @@ export const logQueries = { filters: LogAnalyticsFilters; }) => queryOptions({ - queryKey: ["REQUEST_LOG_ANALYTICS", orgId, filters] as const, + queryKey: ["REQUEST_LOGS", orgId, "ANALYTICS", filters] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse @@ -616,6 +726,124 @@ export const logQueries = { } return false; } + }), + + requests: ({ + orgId, + filters + }: { + orgId: string; + filters: HttpLogFilters; + }) => + queryOptions({ + queryKey: ["REQUEST_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/request`, { + params: { + ...rest, + limit: pageSize, + offset: page * pageSize + }, + signal + }); + return res.data.data; + }, + refetchInterval: (query) => { + if (query.state.data) { + return durationToMs(30, "seconds"); + } + return false; + } + }), + + access: ({ orgId, filters }: { orgId: string; filters: AccessLogFilters }) => + queryOptions({ + queryKey: ["ACCESS_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/access`, { + params: { + ...rest, + limit: pageSize, + offset: page * pageSize + }, + signal + }); + return res.data.data; + }, + refetchInterval: (query) => { + if (query.state.data) { + return durationToMs(30, "seconds"); + } + return false; + } + }), + + action: ({ + orgId, + filters + }: { + orgId: string; + filters: ActionLogFilters; + }) => + queryOptions({ + queryKey: ["ACTION_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/action`, { + params: { + ...rest, + limit: pageSize, + offset: page * pageSize + }, + signal + }); + return res.data.data; + }, + refetchInterval: (query) => { + if (query.state.data) { + return durationToMs(30, "seconds"); + } + 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; + } }) };