import { keepPreviousData } from '@tanstack/react-query' import { useDebounce } from '@uidotdev/usehooks' import dayjs from 'dayjs' import { ArrowDown, ArrowUp, RefreshCw } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { Button } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { TimestampInfo } from 'ui-patterns/TimestampInfo' import { LogsDatePicker } from '../Settings/Logs/Logs.DatePickers' import { filterByProjects, sortAuditLogs } from './AuditLogs.utils' import { LogDetailsPanel } from '@/components/interfaces/AuditLogs/LogDetailsPanel' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' import Table from '@/components/to-be-cleaned/Table' import AlertError from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { FilterPopover } from '@/components/ui/FilterPopover' import { TIMESTAMP_MICROS_PER_MS, 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' export const AuditLogs = () => { const currentTime = dayjs().utc().set('millisecond', 0) const [search, setSearch] = useState('') const debouncedSearch = useDebounce(search, 500) const [dateSortDesc, setDateSortDesc] = useState(true) 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, isLoading: isLoadingProjects, isFetching, 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 } = useOrganizationsQuery() const { data, error, isPending: isLoading, isSuccess, isError, isRefetching, refetch, } = useProfileAuditLogsQuery( { iso_timestamp_start: dateRange.from, iso_timestamp_end: dateRange.to, }, { retry: false, } ) const logs = data?.result ?? [] const sortedLogs = filterByProjects(sortAuditLogs(logs, dateSortDesc), filters.projects) // 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 ( <>

Filter by

setFilters({ ...filters, projects: values })} search={search} setSearch={setSearch} hasNextPage={hasNextPage} isLoading={isLoadingProjects} isFetching={isFetching} 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(), }, ]} /> {isSuccess && ( <>

Viewing {sortedLogs.length} logs in total

)}
{isLoading && (
)} {isError && } {isSuccess && ( <> {logs.length === 0 ? (

You do not have any audit logs available yet

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

No audit logs found based on the filters applied

) : (
Action , Target ,

Date

) : ( ) } onClick={() => setDateSortDesc(!dateSortDesc)} tooltip={{ content: { side: 'bottom', text: dateSortDesc ? 'Sort latest first' : 'Sort earliest first', }, }} />
, , ]} body={ sortedLogs?.map((log) => { const project = projects?.find((p) => p.ref === log.project_ref) const organization = organizations?.find( (org) => org.slug === log.organization_slug ) const isoTimestamp = dayjs( log.timestamp / TIMESTAMP_MICROS_PER_MS ).toISOString() return ( setSelectedLog(log)} className="cursor-pointer hover:bg-alternative! transition duration-100" >

{log.action.status}

{log.action.method}

{log.action.name}

{project || organization ? ( <>

{project?.name ? 'Project: ' : organization?.name ? 'Organization: ' : null} {project?.name ?? organization?.name}

{log.project_ref ? 'Ref: ' : 'Slug: '} {log.project_ref ?? log.organization_slug}

) : (

{log.project_ref ?? log.organization_slug ?? '-'}

)}
) }) ?? [] } /> )} )} setSelectedLog(undefined)} /> ) }