mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-26 02:32:36 +00:00
♻️ use react query for network logs
This commit is contained in:
@@ -9,26 +9,20 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||||
import { toast } from "@app/hooks/useToast";
|
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 { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||||
|
import { logQueries } from "@app/lib/queries";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
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 { ColumnDef } from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { ArrowUpRight, Laptop, User } from "lucide-react";
|
import { ArrowUpRight, Laptop, User } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useMemo, 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]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(startedAt: number, endedAt: number | null): string {
|
function formatDuration(startedAt: number, endedAt: number | null): string {
|
||||||
if (endedAt === null || endedAt === undefined) return "Active";
|
if (endedAt === null || endedAt === undefined) return "Active";
|
||||||
@@ -54,24 +48,8 @@ export default function ConnectionLogsPage() {
|
|||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
|
||||||
const [rows, setRows] = useState<any[]>([]);
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
||||||
const [isExporting, startTransition] = useTransition();
|
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<{
|
const [filters, setFilters] = useState<{
|
||||||
protocol?: string;
|
protocol?: string;
|
||||||
destAddr?: string;
|
destAddr?: string;
|
||||||
@@ -86,43 +64,24 @@ export default function ConnectionLogsPage() {
|
|||||||
userId: searchParams.get("userId") || undefined
|
userId: searchParams.get("userId") || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pagination state
|
|
||||||
const [totalCount, setTotalCount] = useState<number>(0);
|
|
||||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
// Initialize page size from storage or default
|
|
||||||
const [pageSize, setPageSize] = useStoredPageSize(
|
const [pageSize, setPageSize] = useStoredPageSize(
|
||||||
"connection-audit-logs",
|
"connection-audit-logs",
|
||||||
20
|
20
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set default date range to last 7 days
|
|
||||||
const getDefaultDateRange = () => {
|
const getDefaultDateRange = () => {
|
||||||
// if the time is in the url params, use that instead
|
|
||||||
const startParam = searchParams.get("start");
|
const startParam = searchParams.get("start");
|
||||||
const endParam = searchParams.get("end");
|
const endParam = searchParams.get("end");
|
||||||
if (startParam && endParam) {
|
if (startParam && endParam) {
|
||||||
return {
|
return {
|
||||||
startDate: {
|
startDate: { date: new Date(startParam) },
|
||||||
date: new Date(startParam)
|
endDate: { date: new Date(endParam) }
|
||||||
},
|
|
||||||
endDate: {
|
|
||||||
date: new Date(endParam)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const lastWeek = getSevenDaysAgo();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startDate: {
|
startDate: { date: getSevenDaysAgo() },
|
||||||
date: lastWeek
|
endDate: { date: new Date() }
|
||||||
},
|
|
||||||
endDate: {
|
|
||||||
date: now
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,78 +90,100 @@ export default function ConnectionLogsPage() {
|
|||||||
endDate: DateTimeValue;
|
endDate: DateTimeValue;
|
||||||
}>(getDefaultDateRange());
|
}>(getDefaultDateRange());
|
||||||
|
|
||||||
// Trigger search with default values on component mount
|
const queryFilters = useMemo(() => {
|
||||||
useEffect(() => {
|
let timeStart: string | undefined;
|
||||||
if (build === "oss") {
|
let timeEnd: string | undefined;
|
||||||
return;
|
|
||||||
|
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(
|
if (dateRange.endDate?.date) {
|
||||||
defaultRange.startDate,
|
const dt = new Date(dateRange.endDate.date);
|
||||||
defaultRange.endDate,
|
if (dateRange.endDate.time) {
|
||||||
0,
|
const [h, m, s] = dateRange.endDate.time.split(":").map(Number);
|
||||||
pageSize
|
dt.setHours(h, m, s || 0);
|
||||||
);
|
} else {
|
||||||
}, [orgId]); // Re-run if orgId changes
|
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 = (
|
const handleDateRangeChange = (
|
||||||
startDate: DateTimeValue,
|
startDate: DateTimeValue,
|
||||||
endDate: DateTimeValue
|
endDate: DateTimeValue
|
||||||
) => {
|
) => {
|
||||||
setDateRange({ startDate, endDate });
|
setDateRange({ startDate, endDate });
|
||||||
setCurrentPage(0); // Reset to first page when filtering
|
setCurrentPage(0);
|
||||||
// put the search params in the url for the time
|
|
||||||
updateUrlParamsForAllFilters({
|
updateUrlParamsForAllFilters({
|
||||||
start: startDate.date?.toISOString() || "",
|
start: startDate.date?.toISOString() || "",
|
||||||
end: endDate.date?.toISOString() || ""
|
end: endDate.date?.toISOString() || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
queryDateTime(startDate, endDate, 0, pageSize);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle page changes
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
setCurrentPage(newPage);
|
setCurrentPage(newPage);
|
||||||
queryDateTime(
|
|
||||||
dateRange.startDate,
|
|
||||||
dateRange.endDate,
|
|
||||||
newPage,
|
|
||||||
pageSize
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle page size changes
|
|
||||||
const handlePageSizeChange = (newPageSize: number) => {
|
const handlePageSizeChange = (newPageSize: number) => {
|
||||||
setPageSize(newPageSize);
|
setPageSize(newPageSize);
|
||||||
setCurrentPage(0); // Reset to first page when changing page size
|
setCurrentPage(0);
|
||||||
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle filter changes generically
|
|
||||||
const handleFilterChange = (
|
const handleFilterChange = (
|
||||||
filterType: keyof typeof filters,
|
filterType: keyof typeof filters,
|
||||||
value: string | undefined
|
value: string | undefined
|
||||||
) => {
|
) => {
|
||||||
// Create new filters object with updated value
|
const newFilters = { ...filters, [filterType]: value };
|
||||||
const newFilters = {
|
|
||||||
...filters,
|
|
||||||
[filterType]: value
|
|
||||||
};
|
|
||||||
|
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
setCurrentPage(0); // Reset to first page when filtering
|
setCurrentPage(0);
|
||||||
|
|
||||||
// Update URL params
|
|
||||||
updateUrlParamsForAllFilters(newFilters);
|
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 = (
|
const updateUrlParamsForAllFilters = (
|
||||||
@@ -224,109 +205,8 @@ export default function ConnectionLogsPage() {
|
|||||||
router.replace(`?${params.toString()}`, { scroll: false });
|
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 () => {
|
const exportData = async () => {
|
||||||
try {
|
try {
|
||||||
// Prepare query params for export
|
|
||||||
const params: any = {
|
const params: any = {
|
||||||
timeStart: dateRange.startDate?.date
|
timeStart: dateRange.startDate?.date
|
||||||
? new Date(dateRange.startDate.date).toISOString()
|
? 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 url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@@ -363,7 +242,6 @@ export default function ConnectionLogsPage() {
|
|||||||
const data = error.response.data;
|
const data = error.response.data;
|
||||||
|
|
||||||
if (data instanceof Blob && data.type === "application/json") {
|
if (data instanceof Blob && data.type === "application/json") {
|
||||||
// Parse the Blob as JSON
|
|
||||||
const text = await data.text();
|
const text = await data.text();
|
||||||
const errorData = JSON.parse(text);
|
const errorData = JSON.parse(text);
|
||||||
apiErrorMessage = errorData.message;
|
apiErrorMessage = errorData.message;
|
||||||
@@ -380,7 +258,7 @@ export default function ConnectionLogsPage() {
|
|||||||
const columns: ColumnDef<any>[] = [
|
const columns: ColumnDef<any>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "startedAt",
|
accessorKey: "startedAt",
|
||||||
header: ({ column }) => {
|
header: () => {
|
||||||
return t("timestamp");
|
return t("timestamp");
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -395,7 +273,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "protocol",
|
accessorKey: "protocol",
|
||||||
header: ({ column }) => {
|
header: () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("protocol")}</span>
|
<span>{t("protocol")}</span>
|
||||||
@@ -426,7 +304,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "resourceName",
|
accessorKey: "resourceName",
|
||||||
header: ({ column }) => {
|
header: () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("resource")}</span>
|
<span>{t("resource")}</span>
|
||||||
@@ -467,7 +345,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "clientName",
|
accessorKey: "clientName",
|
||||||
header: ({ column }) => {
|
header: () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("client")}</span>
|
<span>{t("client")}</span>
|
||||||
@@ -510,7 +388,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "userEmail",
|
accessorKey: "userEmail",
|
||||||
header: ({ column }) => {
|
header: () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("user")}</span>
|
<span>{t("user")}</span>
|
||||||
@@ -543,7 +421,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "sourceAddr",
|
accessorKey: "sourceAddr",
|
||||||
header: ({ column }) => {
|
header: () => {
|
||||||
return t("sourceAddress");
|
return t("sourceAddress");
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -556,7 +434,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "destAddr",
|
accessorKey: "destAddr",
|
||||||
header: ({ column }) => {
|
header: () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("destinationAddress")}</span>
|
<span>{t("destinationAddress")}</span>
|
||||||
@@ -585,7 +463,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "duration",
|
accessorKey: "duration",
|
||||||
header: ({ column }) => {
|
header: () => {
|
||||||
return t("duration");
|
return t("duration");
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -606,9 +484,6 @@ export default function ConnectionLogsPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
|
||||||
Connection Details
|
|
||||||
</div>*/}
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Session ID:</strong>{" "}
|
<strong>Session ID:</strong>{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
@@ -633,18 +508,6 @@ export default function ConnectionLogsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
|
||||||
Resource & Site
|
|
||||||
</div>*/}
|
|
||||||
{/*<div>
|
|
||||||
<strong>Resource:</strong>{" "}
|
|
||||||
{row.resourceName ?? "-"}
|
|
||||||
{row.resourceNiceId && (
|
|
||||||
<span className="text-muted-foreground ml-1">
|
|
||||||
({row.resourceNiceId})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>*/}
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Client Endpoint:</strong>{" "}
|
<strong>Client Endpoint:</strong>{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
@@ -680,30 +543,8 @@ export default function ConnectionLogsPage() {
|
|||||||
<strong>Duration:</strong>{" "}
|
<strong>Duration:</strong>{" "}
|
||||||
{formatDuration(row.startedAt, row.endedAt)}
|
{formatDuration(row.startedAt, row.endedAt)}
|
||||||
</div>
|
</div>
|
||||||
{/*<div>
|
|
||||||
<strong>Resource ID:</strong>{" "}
|
|
||||||
{row.siteResourceId ?? "-"}
|
|
||||||
</div>*/}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
|
||||||
Client & Transfer
|
|
||||||
</div>*/}
|
|
||||||
{/*<div>
|
|
||||||
<strong>Bytes Sent (TX):</strong>{" "}
|
|
||||||
{formatBytes(row.bytesTx)}
|
|
||||||
</div>*/}
|
|
||||||
{/*<div>
|
|
||||||
<strong>Bytes Received (RX):</strong>{" "}
|
|
||||||
{formatBytes(row.bytesRx)}
|
|
||||||
</div>*/}
|
|
||||||
{/*<div>
|
|
||||||
<strong>Total Transfer:</strong>{" "}
|
|
||||||
{formatBytes(
|
|
||||||
(row.bytesTx ?? 0) + (row.bytesRx ?? 0)
|
|
||||||
)}
|
|
||||||
</div>*/}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -724,8 +565,8 @@ export default function ConnectionLogsPage() {
|
|||||||
title={t("connectionLogs")}
|
title={t("connectionLogs")}
|
||||||
searchPlaceholder={t("searchLogs")}
|
searchPlaceholder={t("searchLogs")}
|
||||||
searchColumn="protocol"
|
searchColumn="protocol"
|
||||||
onRefresh={refreshData}
|
onRefresh={() => refetch()}
|
||||||
isRefreshing={isRefreshing}
|
isRefreshing={isFetching}
|
||||||
onExport={() => startTransition(exportData)}
|
onExport={() => startTransition(exportData)}
|
||||||
isExporting={isExporting}
|
isExporting={isExporting}
|
||||||
onDateRangeChange={handleDateRangeChange}
|
onDateRangeChange={handleDateRangeChange}
|
||||||
@@ -737,14 +578,12 @@ export default function ConnectionLogsPage() {
|
|||||||
id: "startedAt",
|
id: "startedAt",
|
||||||
desc: true
|
desc: true
|
||||||
}}
|
}}
|
||||||
// Server-side pagination props
|
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
// Row expansion props
|
|
||||||
expandable={true}
|
expandable={true}
|
||||||
renderExpandedRow={renderExpandedRow}
|
renderExpandedRow={renderExpandedRow}
|
||||||
disabled={
|
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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
|
|||||||
import type {
|
import type {
|
||||||
QueryAccessAuditLogResponse,
|
QueryAccessAuditLogResponse,
|
||||||
QueryActionAuditLogResponse,
|
QueryActionAuditLogResponse,
|
||||||
|
QueryConnectionAuditLogResponse,
|
||||||
QueryRequestAuditLogResponse
|
QueryRequestAuditLogResponse
|
||||||
} from "@server/routers/auditLogs/types";
|
} from "@server/routers/auditLogs/types";
|
||||||
import type { ListClientsResponse } from "@server/routers/client";
|
import type { ListClientsResponse } from "@server/routers/client";
|
||||||
@@ -674,6 +675,32 @@ export const actionLogsFiltersSchema = z.object({
|
|||||||
|
|
||||||
export type ActionLogFilters = z.output<typeof actionLogsFiltersSchema>;
|
export type ActionLogFilters = z.output<typeof actionLogsFiltersSchema>;
|
||||||
|
|
||||||
|
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<typeof connectionLogsFiltersSchema>;
|
||||||
|
|
||||||
export const logQueries = {
|
export const logQueries = {
|
||||||
requestAnalytics: ({
|
requestAnalytics: ({
|
||||||
orgId,
|
orgId,
|
||||||
@@ -786,6 +813,37 @@ export const logQueries = {
|
|||||||
}
|
}
|
||||||
return false;
|
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<QueryConnectionAuditLogResponse>
|
||||||
|
>(`/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;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user