Merge pull request #2020 from Fredkiss3/fix/log-analytics-adjustments

refactor: adjustments for logs pages
This commit is contained in:
Owen Schwartz
2025-12-10 20:33:12 -05:00
committed by GitHub
15 changed files with 236 additions and 175 deletions

View File

@@ -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",

View File

@@ -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);

View File

@@ -22,9 +22,11 @@ import logger from "@server/logger";
import { import {
queryActionAuditLogsParams, queryActionAuditLogsParams,
queryActionAuditLogsQuery, queryActionAuditLogsQuery,
queryAction queryAction,
countActionQuery
} from "./queryActionAuditLog"; } from "./queryActionAuditLog";
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 exportActionAuditLogs(
} }
const data = { ...parsedQuery.data, ...parsedParams.data }; 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); const baseQuery = queryAction(data);

View File

@@ -24,6 +24,7 @@ import { fromError } from "zod-validation-error";
import { QueryAccessAuditLogResponse } from "@server/routers/auditLogs/types"; import { QueryAccessAuditLogResponse } from "@server/routers/auditLogs/types";
import response from "@server/lib/response"; import response from "@server/lib/response";
import logger from "@server/logger"; import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
export const queryAccessAuditLogsQuery = z.object({ export const queryAccessAuditLogsQuery = z.object({
// iso string just validate its a parseable date // iso string just validate its a parseable date
@@ -32,7 +33,8 @@ export const queryAccessAuditLogsQuery = z.object({
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {
error: "timeStart must be a valid ISO date string" error: "timeStart must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)), .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.prefault(() => getSevenDaysAgo().toISOString()),
timeEnd: z timeEnd: z
.string() .string()
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {

View File

@@ -24,6 +24,7 @@ import { fromError } from "zod-validation-error";
import { QueryActionAuditLogResponse } from "@server/routers/auditLogs/types"; import { QueryActionAuditLogResponse } from "@server/routers/auditLogs/types";
import response from "@server/lib/response"; import response from "@server/lib/response";
import logger from "@server/logger"; import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
export const queryActionAuditLogsQuery = z.object({ export const queryActionAuditLogsQuery = z.object({
// iso string just validate its a parseable date // iso string just validate its a parseable date
@@ -32,7 +33,8 @@ export const queryActionAuditLogsQuery = z.object({
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {
error: "timeStart must be a valid ISO date string" error: "timeStart must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)), .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.prefault(() => getSevenDaysAgo().toISOString()),
timeEnd: z timeEnd: z
.string() .string()
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {

View File

@@ -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);

View File

@@ -2,7 +2,7 @@ import { db, requestAuditLog, driver } from "@server/db";
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
import { NextFunction } from "express"; import { NextFunction } from "express";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { eq, gt, lt, and, count, sql, desc, not, isNull } from "drizzle-orm"; import { eq, gte, lte, and, count, sql, desc, not, isNull } from "drizzle-orm";
import { OpenAPITags } from "@server/openApi"; import { OpenAPITags } from "@server/openApi";
import { z } from "zod"; import { z } from "zod";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -10,6 +10,7 @@ import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import response from "@server/lib/response"; import response from "@server/lib/response";
import logger from "@server/logger"; import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
const queryAccessAuditLogsQuery = z.object({ const queryAccessAuditLogsQuery = z.object({
// iso string just validate its a parseable date // iso string just validate its a parseable date
@@ -19,7 +20,8 @@ const queryAccessAuditLogsQuery = z.object({
error: "timeStart must be a valid ISO date string" error: "timeStart must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)) .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional(), .optional()
.prefault(() => getSevenDaysAgo().toISOString()),
timeEnd: z timeEnd: z
.string() .string()
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {
@@ -55,15 +57,10 @@ type Q = z.infer<typeof queryRequestAuditLogsCombined>;
async function query(query: Q) { async function query(query: Q) {
let baseConditions = and( let baseConditions = and(
eq(requestAuditLog.orgId, query.orgId), eq(requestAuditLog.orgId, query.orgId),
lt(requestAuditLog.timestamp, query.timeEnd) gte(requestAuditLog.timestamp, query.timeStart),
lte(requestAuditLog.timestamp, query.timeEnd)
); );
if (query.timeStart) {
baseConditions = and(
baseConditions,
gt(requestAuditLog.timestamp, query.timeStart)
);
}
if (query.resourceId) { if (query.resourceId) {
baseConditions = and( baseConditions = and(
baseConditions, baseConditions,

View File

@@ -11,6 +11,7 @@ import { fromError } from "zod-validation-error";
import { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types"; import { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
import response from "@server/lib/response"; import response from "@server/lib/response";
import logger from "@server/logger"; import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
export const queryAccessAuditLogsQuery = z.object({ export const queryAccessAuditLogsQuery = z.object({
// iso string just validate its a parseable date // iso string just validate its a parseable date
@@ -19,7 +20,8 @@ export const queryAccessAuditLogsQuery = z.object({
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {
error: "timeStart must be a valid ISO date string" error: "timeStart must be a valid ISO date string"
}) })
.transform((val) => Math.floor(new Date(val).getTime() / 1000)), .transform((val) => Math.floor(new Date(val).getTime() / 1000))
.prefault(() => getSevenDaysAgo().toISOString()),
timeEnd: z timeEnd: z
.string() .string()
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {

View File

@@ -1,16 +1,12 @@
"use client"; "use client";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { toast } from "@app/hooks/useToast"; 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 { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { import { LogDataTable } from "@app/components/LogDataTable";
getStoredPageSize,
LogDataTable,
setStoredPageSize
} from "@app/components/LogDataTable";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { DateTimeValue } from "@app/components/DateTimePicker"; import { DateTimeValue } from "@app/components/DateTimePicker";
import { ArrowUpRight, Key, User } from "lucide-react"; import { ArrowUpRight, Key, User } from "lucide-react";
@@ -21,21 +17,22 @@ import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusCo
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { build } from "@server/build"; import { build } from "@server/build";
import { Alert, AlertDescription } from "@app/components/ui/alert"; 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() { export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext();
const { orgId } = useParams(); const { orgId } = useParams();
const subscription = useSubscriptionStatusContext(); const subscription = useSubscriptionStatusContext();
const { isUnlocked } = useLicenseStatusContext(); const { isUnlocked } = useLicenseStatusContext();
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();
const [filterAttributes, setFilterAttributes] = useState<{ const [filterAttributes, setFilterAttributes] = useState<{
actors: string[]; actors: string[];
resources: { resources: {
@@ -70,9 +67,7 @@ export default function GeneralPage() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// Initialize page size from storage or default // Initialize page size from storage or default
const [pageSize, setPageSize] = useState<number>(() => { const [pageSize, setPageSize] = useStoredPageSize("access-audit-logs", 20);
return getStoredPageSize("access-audit-logs", 20);
});
// Set default date range to last 24 hours // Set default date range to last 24 hours
const getDefaultDateRange = () => { const getDefaultDateRange = () => {
@@ -91,11 +86,11 @@ export default function GeneralPage() {
} }
const now = new Date(); const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); const lastWeek = getSevenDaysAgo();
return { return {
startDate: { startDate: {
date: yesterday date: lastWeek
}, },
endDate: { endDate: {
date: now date: now
@@ -148,7 +143,6 @@ export default function GeneralPage() {
// Handle page size changes // Handle page size changes
const handlePageSizeChange = (newPageSize: number) => { const handlePageSizeChange = (newPageSize: number) => {
setPageSize(newPageSize); setPageSize(newPageSize);
setStoredPageSize(newPageSize, "access-audit-logs");
setCurrentPage(0); // Reset to first page when changing page size setCurrentPage(0); // Reset to first page when changing page size
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize); queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
}; };
@@ -309,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
@@ -339,11 +331,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"
}); });
} }
@@ -631,7 +633,7 @@ export default function GeneralPage() {
title={t("accessLogs")} title={t("accessLogs")}
onRefresh={refreshData} onRefresh={refreshData}
isRefreshing={isRefreshing} isRefreshing={isRefreshing}
onExport={exportData} onExport={() => startTransition(exportData)}
isExporting={isExporting} isExporting={isExporting}
onDateRangeChange={handleDateRangeChange} onDateRangeChange={handleDateRangeChange}
dateRange={{ dateRange={{

View File

@@ -1,32 +1,28 @@
"use client"; "use client";
import { Button } from "@app/components/ui/button";
import { toast } from "@app/hooks/useToast";
import { useState, useRef, useEffect } 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 { ColumnDef } from "@tanstack/react-table";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { Key, User } from "lucide-react";
import { ColumnFilter } from "@app/components/ColumnFilter"; import { ColumnFilter } from "@app/components/ColumnFilter";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { build } from "@server/build";
import { Alert, AlertDescription } from "@app/components/ui/alert"; 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, useTransition } from "react";
export default function GeneralPage() { export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext();
const { orgId } = useParams(); const { orgId } = useParams();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const subscription = useSubscriptionStatusContext(); const subscription = useSubscriptionStatusContext();
@@ -34,7 +30,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();
const [filterAttributes, setFilterAttributes] = useState<{ const [filterAttributes, setFilterAttributes] = useState<{
actors: string[]; actors: string[];
actions: string[]; actions: string[];
@@ -58,9 +54,7 @@ export default function GeneralPage() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// Initialize page size from storage or default // Initialize page size from storage or default
const [pageSize, setPageSize] = useState<number>(() => { const [pageSize, setPageSize] = useStoredPageSize("action-audit-logs", 20);
return getStoredPageSize("action-audit-logs", 20);
});
// Set default date range to last 24 hours // Set default date range to last 24 hours
const getDefaultDateRange = () => { const getDefaultDateRange = () => {
@@ -79,11 +73,11 @@ export default function GeneralPage() {
} }
const now = new Date(); const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); const lastWeek = getSevenDaysAgo();
return { return {
startDate: { startDate: {
date: yesterday date: lastWeek
}, },
endDate: { endDate: {
date: now date: now
@@ -136,7 +130,6 @@ export default function GeneralPage() {
// Handle page size changes // Handle page size changes
const handlePageSizeChange = (newPageSize: number) => { const handlePageSizeChange = (newPageSize: number) => {
setPageSize(newPageSize); setPageSize(newPageSize);
setStoredPageSize(newPageSize, "action-audit-logs");
setCurrentPage(0); // Reset to first page when changing page size setCurrentPage(0); // Reset to first page when changing page size
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize); queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
}; };
@@ -293,8 +286,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
@@ -323,11 +314,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"
}); });
} }
@@ -484,7 +485,7 @@ export default function GeneralPage() {
searchColumn="action" searchColumn="action"
onRefresh={refreshData} onRefresh={refreshData}
isRefreshing={isRefreshing} isRefreshing={isRefreshing}
onExport={exportData} onExport={() => startTransition(exportData)}
isExporting={isExporting} isExporting={isExporting}
onDateRangeChange={handleDateRangeChange} onDateRangeChange={handleDateRangeChange}
dateRange={{ dateRange={{

View File

@@ -1,34 +1,32 @@
"use client"; "use client";
import { Button } from "@app/components/ui/button";
import { toast } from "@app/hooks/useToast";
import { useState, useRef, useEffect } 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 { ColumnDef } from "@tanstack/react-table";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { Key, RouteOff, User, Lock, Unlock, ArrowUpRight } from "lucide-react";
import Link from "next/link";
import { ColumnFilter } from "@app/components/ColumnFilter"; import { ColumnFilter } from "@app/components/ColumnFilter";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { useTranslations } from "next-intl";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
import { ColumnDef } from "@tanstack/react-table";
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 { useStoredPageSize } from "@app/hooks/useStoredPageSize";
export default function GeneralPage() { export default function GeneralPage() {
const router = useRouter(); const router = useRouter();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext();
const { orgId } = useParams(); const { orgId } = useParams();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
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);
@@ -36,9 +34,7 @@ export default function GeneralPage() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// Initialize page size from storage or default // Initialize page size from storage or default
const [pageSize, setPageSize] = useState<number>(() => { const [pageSize, setPageSize] = useStoredPageSize("request-audit-logs", 20);
return getStoredPageSize("request-audit-logs", 20);
});
const [filterAttributes, setFilterAttributes] = useState<{ const [filterAttributes, setFilterAttributes] = useState<{
actors: string[]; actors: string[];
@@ -95,11 +91,11 @@ export default function GeneralPage() {
} }
const now = new Date(); const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); const lastWeek = getSevenDaysAgo();
return { return {
startDate: { startDate: {
date: yesterday date: lastWeek
}, },
endDate: { endDate: {
date: now date: now
@@ -152,7 +148,6 @@ export default function GeneralPage() {
// Handle page size changes // Handle page size changes
const handlePageSizeChange = (newPageSize: number) => { const handlePageSizeChange = (newPageSize: number) => {
setPageSize(newPageSize); setPageSize(newPageSize);
setStoredPageSize(newPageSize, "request-audit-logs");
setCurrentPage(0); // Reset to first page when changing page size setCurrentPage(0); // Reset to first page when changing page size
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize); queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
}; };
@@ -302,8 +297,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
@@ -335,11 +328,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"
}); });
} }
@@ -773,7 +776,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={{

View File

@@ -1,22 +1,27 @@
"use client"; "use client";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { cn } from "@app/lib/cn";
import { createApiClient } from "@app/lib/api";
import { import {
logAnalyticsFiltersSchema, logAnalyticsFiltersSchema,
logQueries, logQueries,
resourceQueries resourceQueries
} from "@app/lib/queries"; } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { Card, CardContent, CardHeader } from "./ui/card";
import { LoaderIcon, RefreshCw, XIcon } from "lucide-react"; import { LoaderIcon, RefreshCw, XIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { DateRangePicker, type DateTimeValue } from "./DateTimePicker"; import { DateRangePicker, type DateTimeValue } from "./DateTimePicker";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { cn } from "@app/lib/cn"; import { Card, CardContent, CardHeader } from "./ui/card";
import { useTranslations } from "next-intl";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
import {
InfoSection,
InfoSectionContent,
InfoSections,
InfoSectionTitle
} from "./InfoSection";
import { Label } from "./ui/label";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -24,23 +29,10 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "./ui/select"; } from "./ui/select";
import { Label } from "./ui/label";
import { Separator } from "./ui/separator"; import { Separator } from "./ui/separator";
import {
InfoSection,
InfoSectionContent,
InfoSections,
InfoSectionTitle
} from "./InfoSection";
import { WorldMap } from "./WorldMap"; import { WorldMap } from "./WorldMap";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
import { import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "./ui/tooltip";
import { import {
ChartContainer, ChartContainer,
ChartLegend, ChartLegend,
@@ -49,7 +41,13 @@ import {
ChartTooltipContent, ChartTooltipContent,
type ChartConfig type ChartConfig
} from "./ui/chart"; } from "./ui/chart";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "./ui/tooltip";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
export type AnalyticsContentProps = { export type AnalyticsContentProps = {
orgId: string; orgId: string;
@@ -67,17 +65,18 @@ export function LogAnalyticsData(props: AnalyticsContentProps) {
const isEmptySearchParams = const isEmptySearchParams =
!filters.resourceId && !filters.timeStart && !filters.timeEnd; !filters.resourceId && !filters.timeStart && !filters.timeEnd;
const env = useEnvContext();
const [api] = useState(() => createApiClient(env));
const router = useRouter(); const router = useRouter();
console.log({ filters });
const dateRange = { const dateRange = {
startDate: filters.timeStart ? new Date(filters.timeStart) : undefined, startDate: filters.timeStart
endDate: filters.timeEnd ? new Date(filters.timeEnd) : undefined ? new Date(filters.timeStart)
: getSevenDaysAgo(),
endDate: filters.timeEnd ? new Date(filters.timeEnd) : new Date()
}; };
const { data: resources = [], isFetching: isFetchingResources } = useQuery( const { data: resources = [], isFetching: isFetchingResources } = useQuery(
resourceQueries.listNamesPerOrg(props.orgId, api) resourceQueries.listNamesPerOrg(props.orgId)
); );
const { const {
@@ -88,7 +87,6 @@ export function LogAnalyticsData(props: AnalyticsContentProps) {
} = useQuery( } = useQuery(
logQueries.requestAnalytics({ logQueries.requestAnalytics({
orgId: props.orgId, orgId: props.orgId,
api,
filters filters
}) })
); );

View File

@@ -1,16 +1,5 @@
"use client"; "use client";
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
SortingState,
getSortedRowModel,
ColumnFiltersState,
getFilteredRowModel
} from "@tanstack/react-table";
import { import {
Table, Table,
TableBody, TableBody,
@@ -19,29 +8,36 @@ import {
TableHeader, TableHeader,
TableRow TableRow
} from "@/components/ui/table"; } 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 { 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 { DateRangePicker, DateTimeValue } from "@app/components/DateTimePicker";
import { Button } from "@app/components/ui/button";
import { Card, CardContent, CardHeader } from "@app/components/ui/card";
import {
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 { useState, useEffect, useMemo } from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "./ui/tooltip";
const STORAGE_KEYS = { const STORAGE_KEYS = {
PAGE_SIZE: "datatable-page-size", PAGE_SIZE: "datatable-page-size",
@@ -400,15 +396,28 @@ export function LogDataTable<TData, TValue>({
</Button> </Button>
)} )}
{onExport && ( {onExport && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
onClick={() => !disabled && onExport()} onClick={() =>
!disabled && onExport()
}
disabled={isExporting || disabled} disabled={isExporting || disabled}
> >
<Download {isExporting ? (
className={`mr-2 h-4 w-4 ${isExporting ? "animate-spin" : ""}`} <Loader className="mr-2 size-4 animate-spin" />
/> ) : (
<Download className="mr-2 size-4" />
)}
{t("exportCsv")} {t("exportCsv")}
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
{t("exportCsvTooltip")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)} )}
</div> </div>
</CardHeader> </CardHeader>

View File

@@ -0,0 +1,7 @@
export function getSevenDaysAgo() {
const today = new Date();
today.setHours(0, 0, 0, 0); // Set to midnight
const sevenDaysAgo = new Date(today);
sevenDaysAgo.setDate(today.getDate() - 7);
return sevenDaysAgo;
}

View File

@@ -168,17 +168,15 @@ export type LogAnalyticsFilters = z.TypeOf<typeof logAnalyticsFiltersSchema>;
export const logQueries = { export const logQueries = {
requestAnalytics: ({ requestAnalytics: ({
orgId, orgId,
filters, filters
api
}: { }: {
orgId: string; orgId: string;
filters: LogAnalyticsFilters; filters: LogAnalyticsFilters;
api: AxiosInstance;
}) => }) =>
queryOptions({ queryOptions({
queryKey: ["REQUEST_LOG_ANALYTICS", orgId, filters] as const, queryKey: ["REQUEST_LOG_ANALYTICS", orgId, filters] as const,
queryFn: async ({ signal }) => { queryFn: async ({ signal, meta }) => {
const res = await api.get< const res = await meta!.api.get<
AxiosResponse<QueryRequestAnalyticsResponse> AxiosResponse<QueryRequestAnalyticsResponse>
>(`/org/${orgId}/logs/analytics`, { >(`/org/${orgId}/logs/analytics`, {
params: filters, params: filters,
@@ -228,11 +226,11 @@ export const resourceQueries = {
return res.data.data.clients; return res.data.data.clients;
} }
}), }),
listNamesPerOrg: (orgId: string, api: AxiosInstance) => listNamesPerOrg: (orgId: string) =>
queryOptions({ queryOptions({
queryKey: ["RESOURCES_NAMES", orgId] as const, queryKey: ["RESOURCES_NAMES", orgId] as const,
queryFn: async ({ signal }) => { queryFn: async ({ signal, meta }) => {
const res = await api.get< const res = await meta!.api.get<
AxiosResponse<ListResourceNamesResponse> AxiosResponse<ListResourceNamesResponse>
>(`/org/${orgId}/resource-names`, { >(`/org/${orgId}/resource-names`, {
signal signal