import { keepPreviousData } from '@tanstack/react-query' import { getCoreRowModel, getSortedRowModel, useReactTable, type RowSelectionState, type SortingState, } from '@tanstack/react-table' import { useDebounce } from '@uidotdev/usehooks' import dayjs from 'dayjs' import { RefreshCw } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import { cn, ResizablePanel, ResizablePanelGroup } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { LogsDatePicker } from '../Settings/Logs/Logs.DatePickers' import { AuditLogDetailsPanel } from '@/components/interfaces/AuditLogs/AuditLogDetailsPanel' import { getAuditLogColumns } from '@/components/interfaces/AuditLogs/AuditLogs.columns' import { filterByProjects } from '@/components/interfaces/AuditLogs/AuditLogs.utils' import { AuditLogsSelectionHeader } from '@/components/interfaces/AuditLogs/AuditLogsSelectionHeader' import { AuditLogsTable } from '@/components/interfaces/AuditLogs/AuditLogsTable' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' import { AlertError } from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { FilterPopover } from '@/components/ui/FilterPopover' import { type AuditLog } from '@/data/organizations/organization-audit-logs-query' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { useProfileAuditLogsQuery } from '@/data/profile/profile-audit-logs-query' import { useProjectsInfiniteQuery } from '@/data/projects/projects-infinite-query' const CONTENT_PADDING = 'w-full px-6 xl:px-10' export const AuditLogs = () => { const currentTime = dayjs().utc().set('millisecond', 0) const [search, setSearch] = useState('') const debouncedSearch = useDebounce(search, 500) const [dateRange, setDateRange] = useState({ from: currentTime.subtract(1, 'day').toISOString(), to: currentTime.toISOString(), }) const [selectedLog, setSelectedLog] = useState() const [filters, setFilters] = useState<{ projects: string[] }>({ projects: [], }) const { data: projectsData, isPending: isLoadingProjects, isFetching: isFetchingProjects, isFetchingNextPage, hasNextPage, fetchNextPage, } = useProjectsInfiniteQuery( { search: search.length === 0 ? search : debouncedSearch }, { placeholderData: keepPreviousData } ) const projects = useMemo( () => projectsData?.pages.flatMap((page) => page.projects) ?? [], [projectsData?.pages] ) const { data: organizations, isPending: isLoadingOrganizations } = useOrganizationsQuery() const { data, error, isPending: isLoading, isSuccess, isError, isRefetching, refetch, } = useProfileAuditLogsQuery( { iso_timestamp_start: dateRange.from, iso_timestamp_end: dateRange.to, }, { retry: false, placeholderData: keepPreviousData, } ) const logs = useMemo(() => data?.result ?? [], [data?.result]) const filteredLogs = useMemo( () => filterByProjects(logs, filters.projects), [logs, filters.projects] ) const lastSelectedRowId = useRef(null) const columns = useMemo( () => getAuditLogColumns({ projects, organizations: organizations ?? [], lastSelectedRowId, isLoadingProjects, isLoadingOrganizations, }), [projects, organizations, isLoadingProjects, isLoadingOrganizations] ) const [sorting, setSorting] = useState([{ id: 'date', desc: true }]) const [rowSelection, setRowSelection] = useState({}) const table = useReactTable({ data: filteredLogs, columns, state: { sorting, rowSelection }, enableMultiRowSelection: true, getRowId: (row) => row.request_id, onSortingChange: setSorting, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), }) // This feature depends on the subscription tier of the user. Free user can view logs up to 1 day // in the past. The API limits the logs to maximum of 1 day and 5 minutes so when the page is // viewed for more than 5 minutes, the call parameters needs to be updated. This also works with // higher tiers (7 days of logs).The user will see a loading shimmer. useEffect(() => { const duration = dayjs(dateRange.from).diff(dayjs(dateRange.to)) const interval = setInterval(() => { const currentTime = dayjs().utc().set('millisecond', 0) setDateRange({ from: currentTime.add(duration).toISOString(), to: currentTime.toISOString(), }) }, 5 * 60000) return () => clearInterval(interval) }, [dateRange.from, dateRange.to]) return (
{/* [Joshen] Can consider replacing this with filter bar */}

Filter by

setFilters({ ...filters, projects: values })} search={search} setSearch={setSearch} hasNextPage={hasNextPage} isLoading={isLoadingProjects} isFetching={isFetchingProjects} isFetchingNextPage={isFetchingNextPage} fetchNextPage={fetchNextPage} /> setDateRange(value)} helpers={[ { text: 'Last 1 hour', calcFrom: () => dayjs().subtract(1, 'hour').toISOString(), calcTo: () => dayjs().toISOString(), }, { text: 'Last 3 hours', calcFrom: () => dayjs().subtract(3, 'hour').toISOString(), calcTo: () => dayjs().toISOString(), }, { text: 'Last 6 hours', calcFrom: () => dayjs().subtract(6, 'hour').toISOString(), calcTo: () => dayjs().toISOString(), }, { text: 'Last 12 hours', calcFrom: () => dayjs().subtract(12, 'hour').toISOString(), calcTo: () => dayjs().toISOString(), }, { text: 'Last 24 hours', calcFrom: () => dayjs().subtract(1, 'day').toISOString(), calcTo: () => dayjs().toISOString(), }, ]} />
} onClick={() => refetch()} tooltip={{ content: { side: 'bottom', text: 'Refresh logs' } }} />
{isLoading && (
)} {isError && (
)} {isSuccess && ( <> {logs.length === 0 ? (

You do not have any audit logs available yet

) : logs.length > 0 && filteredLogs.length === 0 ? (

No audit logs found based on the filters applied

) : (
setSelectedLog((prev) => prev?.request_id === log.request_id ? undefined : log ) } />
{selectedLog && ( setSelectedLog(undefined)} /> )}
)} )}
) }