From 5b620469c7b4736c4931a36bc195d8ecc7a01ea4 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 10 Dec 2025 03:42:53 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20set=20export=20logs=20limi?= =?UTF-8?q?ts=20to=20`50=20000`=20everywhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/auditLogs/exportActionAuditLog.ts | 13 ++- src/app/[orgId]/settings/logs/access/page.tsx | 35 +++--- src/app/[orgId]/settings/logs/action/page.tsx | 35 +++--- .../[orgId]/settings/logs/request/page.tsx | 12 +- src/components/LogDataTable.tsx | 106 +++++------------- 5 files changed, 79 insertions(+), 122 deletions(-) diff --git a/server/private/routers/auditLogs/exportActionAuditLog.ts b/server/private/routers/auditLogs/exportActionAuditLog.ts index 1fc4d743..a0b4997a 100644 --- a/server/private/routers/auditLogs/exportActionAuditLog.ts +++ b/server/private/routers/auditLogs/exportActionAuditLog.ts @@ -22,9 +22,11 @@ import logger from "@server/logger"; import { queryActionAuditLogsParams, queryActionAuditLogsQuery, - queryAction + queryAction, + countActionQuery } from "./queryActionAuditLog"; import { generateCSV } from "@server/routers/auditLogs/generateCSV"; +import { MAX_EXPORT_LIMIT } from "@server/routers/auditLogs"; registry.registerPath({ method: "get", @@ -65,6 +67,15 @@ export async function exportActionAuditLogs( } const data = { ...parsedQuery.data, ...parsedParams.data }; + const [{ count }] = await countActionQuery(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 = queryAction(data); diff --git a/src/app/[orgId]/settings/logs/access/page.tsx b/src/app/[orgId]/settings/logs/access/page.tsx index e7358c8e..d5b12ddb 100644 --- a/src/app/[orgId]/settings/logs/access/page.tsx +++ b/src/app/[orgId]/settings/logs/access/page.tsx @@ -1,16 +1,12 @@ "use client"; import { Button } from "@app/components/ui/button"; import { toast } from "@app/hooks/useToast"; -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, useTransition } from "react"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; -import { - getStoredPageSize, - LogDataTable, - setStoredPageSize -} from "@app/components/LogDataTable"; +import { LogDataTable } from "@app/components/LogDataTable"; import { ColumnDef } from "@tanstack/react-table"; import { DateTimeValue } from "@app/components/DateTimePicker"; import { ArrowUpRight, Key, User } from "lucide-react"; @@ -22,6 +18,8 @@ import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { build } from "@server/build"; import { Alert, AlertDescription } from "@app/components/ui/alert"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; +import axios from "axios"; +import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; export default function GeneralPage() { const router = useRouter(); @@ -34,7 +32,7 @@ export default function GeneralPage() { const [rows, setRows] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); - const [isExporting, setIsExporting] = useState(false); + const [isExporting, startTransition] = useTransition(); const [filterAttributes, setFilterAttributes] = useState<{ actors: string[]; resources: { @@ -69,9 +67,7 @@ export default function GeneralPage() { const [isLoading, setIsLoading] = useState(false); // Initialize page size from storage or default - const [pageSize, setPageSize] = useState(() => { - return getStoredPageSize("access-audit-logs", 20); - }); + const [pageSize, setPageSize] = useStoredPageSize("access-audit-logs", 20); // Set default date range to last 24 hours const getDefaultDateRange = () => { @@ -147,7 +143,6 @@ export default function GeneralPage() { // Handle page size changes const handlePageSizeChange = (newPageSize: number) => { setPageSize(newPageSize); - setStoredPageSize(newPageSize, "access-audit-logs"); setCurrentPage(0); // Reset to first page when changing page size queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize); }; @@ -308,8 +303,6 @@ export default function GeneralPage() { const exportData = async () => { try { - setIsExporting(true); - // Prepare query params for export const params: any = { timeStart: dateRange.startDate?.date @@ -338,11 +331,21 @@ export default function GeneralPage() { document.body.appendChild(link); link.click(); link.parentNode?.removeChild(link); - setIsExporting(false); } 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({ title: t("error"), - description: t("exportError"), + description: apiErrorMessage ?? t("exportError"), variant: "destructive" }); } @@ -630,7 +633,7 @@ export default function GeneralPage() { title={t("accessLogs")} onRefresh={refreshData} isRefreshing={isRefreshing} - onExport={exportData} + onExport={() => startTransition(exportData)} isExporting={isExporting} onDateRangeChange={handleDateRangeChange} dateRange={{ diff --git a/src/app/[orgId]/settings/logs/action/page.tsx b/src/app/[orgId]/settings/logs/action/page.tsx index 66e7db59..344866bb 100644 --- a/src/app/[orgId]/settings/logs/action/page.tsx +++ b/src/app/[orgId]/settings/logs/action/page.tsx @@ -1,25 +1,23 @@ "use client"; import { ColumnFilter } from "@app/components/ColumnFilter"; import { DateTimeValue } from "@app/components/DateTimePicker"; -import { - getStoredPageSize, - LogDataTable, - setStoredPageSize -} from "@app/components/LogDataTable"; +import { LogDataTable } from "@app/components/LogDataTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Alert, AlertDescription } from "@app/components/ui/alert"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; import { build } from "@server/build"; 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 } from "react"; +import { useEffect, useState, useTransition } from "react"; export default function GeneralPage() { const router = useRouter(); @@ -32,7 +30,7 @@ export default function GeneralPage() { const [rows, setRows] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); - const [isExporting, setIsExporting] = useState(false); + const [isExporting, startTransition] = useTransition(); const [filterAttributes, setFilterAttributes] = useState<{ actors: string[]; actions: string[]; @@ -56,9 +54,7 @@ export default function GeneralPage() { const [isLoading, setIsLoading] = useState(false); // Initialize page size from storage or default - const [pageSize, setPageSize] = useState(() => { - return getStoredPageSize("action-audit-logs", 20); - }); + const [pageSize, setPageSize] = useStoredPageSize("action-audit-logs", 20); // Set default date range to last 24 hours const getDefaultDateRange = () => { @@ -134,7 +130,6 @@ export default function GeneralPage() { // Handle page size changes const handlePageSizeChange = (newPageSize: number) => { setPageSize(newPageSize); - setStoredPageSize(newPageSize, "action-audit-logs"); setCurrentPage(0); // Reset to first page when changing page size queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize); }; @@ -291,8 +286,6 @@ export default function GeneralPage() { const exportData = async () => { try { - setIsExporting(true); - // Prepare query params for export const params: any = { timeStart: dateRange.startDate?.date @@ -321,11 +314,21 @@ export default function GeneralPage() { document.body.appendChild(link); link.click(); link.parentNode?.removeChild(link); - setIsExporting(false); } 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({ title: t("error"), - description: t("exportError"), + description: apiErrorMessage ?? t("exportError"), variant: "destructive" }); } @@ -482,7 +485,7 @@ export default function GeneralPage() { searchColumn="action" onRefresh={refreshData} isRefreshing={isRefreshing} - onExport={exportData} + onExport={() => startTransition(exportData)} isExporting={isExporting} onDateRangeChange={handleDateRangeChange} dateRange={{ diff --git a/src/app/[orgId]/settings/logs/request/page.tsx b/src/app/[orgId]/settings/logs/request/page.tsx index 31b4bd72..741dd994 100644 --- a/src/app/[orgId]/settings/logs/request/page.tsx +++ b/src/app/[orgId]/settings/logs/request/page.tsx @@ -1,11 +1,7 @@ "use client"; import { ColumnFilter } from "@app/components/ColumnFilter"; import { DateTimeValue } from "@app/components/DateTimePicker"; -import { - getStoredPageSize, - LogDataTable, - setStoredPageSize -} from "@app/components/LogDataTable"; +import { LogDataTable } from "@app/components/LogDataTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Button } from "@app/components/ui/button"; import { useEnvContext } from "@app/hooks/useEnvContext"; @@ -19,6 +15,7 @@ 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 { useStoredPageSize } from "@app/hooks/useStoredPageSize"; export default function GeneralPage() { const router = useRouter(); @@ -37,9 +34,7 @@ export default function GeneralPage() { const [isLoading, setIsLoading] = useState(false); // Initialize page size from storage or default - const [pageSize, setPageSize] = useState(() => { - return getStoredPageSize("request-audit-logs", 20); - }); + const [pageSize, setPageSize] = useStoredPageSize("request-audit-logs", 20); const [filterAttributes, setFilterAttributes] = useState<{ actors: string[]; @@ -153,7 +148,6 @@ export default function GeneralPage() { // Handle page size changes const handlePageSizeChange = (newPageSize: number) => { setPageSize(newPageSize); - setStoredPageSize(newPageSize, "request-audit-logs"); setCurrentPage(0); // Reset to first page when changing page size queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize); }; diff --git a/src/components/LogDataTable.tsx b/src/components/LogDataTable.tsx index 7156e8c3..2c46a325 100644 --- a/src/components/LogDataTable.tsx +++ b/src/components/LogDataTable.tsx @@ -1,16 +1,5 @@ "use client"; -import { - ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, - getPaginationRowModel, - SortingState, - getSortedRowModel, - ColumnFiltersState, - getFilteredRowModel -} from "@tanstack/react-table"; import { Table, TableBody, @@ -19,75 +8,30 @@ import { TableHeader, TableRow } from "@/components/ui/table"; -import { Button } from "@app/components/ui/button"; -import { useEffect, useMemo, useState } from "react"; -import { Input } from "@app/components/ui/input"; import { DataTablePagination } from "@app/components/DataTablePagination"; -import { - Plus, - Search, - RefreshCw, - Filter, - X, - Download, - ChevronRight, - ChevronDown -} from "lucide-react"; -import { - Card, - CardContent, - CardHeader, - CardTitle -} from "@app/components/ui/card"; -import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs"; -import { useTranslations } from "next-intl"; import { DateRangePicker, DateTimeValue } from "@app/components/DateTimePicker"; +import { Button } from "@app/components/ui/button"; +import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger -} from "./ui/tooltip"; - -const STORAGE_KEYS = { - PAGE_SIZE: "datatable-page-size", - getTablePageSize: (tableId?: string) => - tableId ? `${tableId}-size` : STORAGE_KEYS.PAGE_SIZE -}; - -export const getStoredPageSize = ( - tableId?: string, - - defaultSize = 20 -): number => { - if (typeof window === "undefined") return defaultSize; - - try { - const key = STORAGE_KEYS.getTablePageSize(tableId); - const stored = localStorage.getItem(key); - if (stored) { - const parsed = parseInt(stored, 10); - // Validate that it's a reasonable page size - if (parsed > 0 && parsed <= 1000) { - return parsed; - } - } - } catch (error) { - console.warn("Failed to read page size from localStorage:", error); - } - return defaultSize; -}; - -export const setStoredPageSize = (pageSize: number, tableId?: string): void => { - if (typeof window === "undefined") return; - - try { - const key = STORAGE_KEYS.getTablePageSize(tableId); - localStorage.setItem(key, pageSize.toString()); - } catch (error) { - console.warn("Failed to save page size to localStorage:", error); - } -}; + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable +} from "@tanstack/react-table"; +import { + ChevronDown, + ChevronRight, + Download, + Loader, + RefreshCw +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useEffect, useMemo, useState } from "react"; type TabFilter = { id: string; @@ -411,9 +355,11 @@ export function LogDataTable({ onClick={() => !disabled && onExport()} disabled={isExporting || disabled} > - + {isExporting ? ( + + ) : ( + + )} {t("exportCsv")} )}