mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-09 20:26:40 +00:00
🚧 wip: export limits
This commit is contained in:
@@ -2067,6 +2067,8 @@
|
|||||||
"timestamp": "Timestamp",
|
"timestamp": "Timestamp",
|
||||||
"accessLogs": "Access Logs",
|
"accessLogs": "Access Logs",
|
||||||
"exportCsv": "Export CSV",
|
"exportCsv": "Export CSV",
|
||||||
|
"exportError": "Unknown error when exporting CSV",
|
||||||
|
"exportCsvTooltip": "Within Time Range",
|
||||||
"actorId": "Actor ID",
|
"actorId": "Actor ID",
|
||||||
"allowedByRule": "Allowed by Rule",
|
"allowedByRule": "Allowed by Rule",
|
||||||
"allowedNoAuth": "Allowed No Auth",
|
"allowedNoAuth": "Allowed No Auth",
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ import logger from "@server/logger";
|
|||||||
import {
|
import {
|
||||||
queryAccessAuditLogsParams,
|
queryAccessAuditLogsParams,
|
||||||
queryAccessAuditLogsQuery,
|
queryAccessAuditLogsQuery,
|
||||||
queryAccess
|
queryAccess,
|
||||||
|
countAccessQuery
|
||||||
} from "./queryAccessAuditLog";
|
} from "./queryAccessAuditLog";
|
||||||
import { generateCSV } from "@server/routers/auditLogs/generateCSV";
|
import { generateCSV } from "@server/routers/auditLogs/generateCSV";
|
||||||
|
import { MAX_EXPORT_LIMIT } from "@server/routers/auditLogs";
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
@@ -65,6 +67,15 @@ export async function exportAccessAuditLogs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = { ...parsedQuery.data, ...parsedParams.data };
|
const data = { ...parsedQuery.data, ...parsedParams.data };
|
||||||
|
const [{ count }] = await countAccessQuery(data);
|
||||||
|
if (count > MAX_EXPORT_LIMIT) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Export limit exceeded. Your selection contains ${count} rows, but the maximum is [${MAX_EXPORT_LIMIT}] rows. Please select a shorter time range to reduce the data.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const baseQuery = queryAccess(data);
|
const baseQuery = queryAccess(data);
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,23 @@ import logger from "@server/logger";
|
|||||||
import {
|
import {
|
||||||
queryAccessAuditLogsQuery,
|
queryAccessAuditLogsQuery,
|
||||||
queryRequestAuditLogsParams,
|
queryRequestAuditLogsParams,
|
||||||
queryRequest
|
queryRequest,
|
||||||
|
countRequestQuery
|
||||||
} from "./queryRequestAuditLog";
|
} from "./queryRequestAuditLog";
|
||||||
import { generateCSV } from "./generateCSV";
|
import { generateCSV } from "./generateCSV";
|
||||||
|
|
||||||
|
export const MAX_EXPORT_LIMIT = 50_000;
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/logs/request",
|
path: "/org/{orgId}/logs/request",
|
||||||
description: "Query the request audit log for an organization",
|
description: "Query the request audit log for an organization",
|
||||||
tags: [OpenAPITags.Org],
|
tags: [OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
query: queryAccessAuditLogsQuery,
|
query: queryAccessAuditLogsQuery.omit({
|
||||||
|
limit: true,
|
||||||
|
offset: true
|
||||||
|
}),
|
||||||
params: queryRequestAuditLogsParams
|
params: queryRequestAuditLogsParams
|
||||||
},
|
},
|
||||||
responses: {}
|
responses: {}
|
||||||
@@ -53,9 +59,19 @@ export async function exportRequestAuditLogs(
|
|||||||
|
|
||||||
const data = { ...parsedQuery.data, ...parsedParams.data };
|
const data = { ...parsedQuery.data, ...parsedParams.data };
|
||||||
|
|
||||||
|
const [{ count }] = await countRequestQuery(data);
|
||||||
|
if (count > MAX_EXPORT_LIMIT) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Export limit exceeded. Your selection contains ${count} rows, but the maximum is [${MAX_EXPORT_LIMIT}] rows. Please select a shorter time range to reduce the data.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const baseQuery = queryRequest(data);
|
const baseQuery = queryRequest(data);
|
||||||
|
|
||||||
const log = await baseQuery.limit(data.limit).offset(data.offset);
|
const log = await baseQuery.limit(MAX_EXPORT_LIMIT);
|
||||||
|
|
||||||
const csvData = generateCSV(log);
|
const csvData = generateCSV(log);
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import { Button } from "@app/components/ui/button";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import axios from "axios";
|
||||||
import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react";
|
import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -29,7 +29,7 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
const [rows, setRows] = useState<any[]>([]);
|
const [rows, setRows] = useState<any[]>([]);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isExporting, startTransition] = useTransition();
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const [totalCount, setTotalCount] = useState<number>(0);
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
@@ -303,8 +303,6 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
const exportData = async () => {
|
const exportData = async () => {
|
||||||
try {
|
try {
|
||||||
setIsExporting(true);
|
|
||||||
|
|
||||||
// Prepare query params for export
|
// Prepare query params for export
|
||||||
const params: any = {
|
const params: any = {
|
||||||
timeStart: dateRange.startDate?.date
|
timeStart: dateRange.startDate?.date
|
||||||
@@ -336,11 +334,21 @@ export default function GeneralPage() {
|
|||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.parentNode?.removeChild(link);
|
link.parentNode?.removeChild(link);
|
||||||
setIsExporting(false);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
let apiErrorMessage: string | null = null;
|
||||||
|
if (axios.isAxiosError(error) && error.response) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
toast({
|
toast({
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
description: t("exportError"),
|
description: apiErrorMessage ?? t("exportError"),
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -774,7 +782,7 @@ export default function GeneralPage() {
|
|||||||
searchColumn="host"
|
searchColumn="host"
|
||||||
onRefresh={refreshData}
|
onRefresh={refreshData}
|
||||||
isRefreshing={isRefreshing}
|
isRefreshing={isRefreshing}
|
||||||
onExport={exportData}
|
onExport={() => startTransition(exportData)}
|
||||||
isExporting={isExporting}
|
isExporting={isExporting}
|
||||||
onDateRangeChange={handleDateRangeChange}
|
onDateRangeChange={handleDateRangeChange}
|
||||||
dateRange={{
|
dateRange={{
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ import {
|
|||||||
import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { DateRangePicker, DateTimeValue } from "@app/components/DateTimePicker";
|
import { DateRangePicker, DateTimeValue } from "@app/components/DateTimePicker";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "./ui/tooltip";
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
PAGE_SIZE: "datatable-page-size",
|
PAGE_SIZE: "datatable-page-size",
|
||||||
@@ -51,6 +57,7 @@ const STORAGE_KEYS = {
|
|||||||
|
|
||||||
export const getStoredPageSize = (
|
export const getStoredPageSize = (
|
||||||
tableId?: string,
|
tableId?: string,
|
||||||
|
|
||||||
defaultSize = 20
|
defaultSize = 20
|
||||||
): number => {
|
): number => {
|
||||||
if (typeof window === "undefined") return defaultSize;
|
if (typeof window === "undefined") return defaultSize;
|
||||||
|
|||||||
Reference in New Issue
Block a user