import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import dayjs from 'dayjs' import { ArrowDown, ArrowUp, RefreshCw, User } from 'lucide-react' import Image from 'next/legacy/image' import Link from 'next/link' import { useEffect, useState } from 'react' import { LogDetailsPanel } from 'components/interfaces/AuditLogs' import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { DatePicker } from 'components/ui/DatePicker' import { FilterPopover } from 'components/ui/FilterPopover' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { AuditLog, useOrganizationAuditLogsQuery, } from 'data/organizations/organization-audit-logs-query' import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query' import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProjectsQuery } from 'data/projects/projects-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, WarningIcon, } from 'ui' // [Joshen considerations] // - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible // - We'll need pagination as well if the audit logs get too large, but that needs to be implemented on the API side first if possible // - I've hidden time input in the date picker for now cause the time support in the component is a bit iffy, need to investigate // - Maybe a rule to follow from here is just everytime we call dayjs, use UTC(), one TZ to rule them all const AuditLogs = () => { const { slug } = useParams() const currentTime = dayjs().utc().set('millisecond', 0) 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<{ users: string[]; projects: string[] }>({ users: [], // gotrue_id[] projects: [], // project_ref[] }) const canReadAuditLogs = useCheckPermissions(PermissionAction.READ, 'notifications') const { data: projects } = useProjectsQuery() const { data: organizations } = useOrganizationsQuery() const { data: members } = useOrganizationMembersQuery({ slug }) const { data: rolesData } = useOrganizationRolesQuery({ slug }) const { data, error, isLoading, isSuccess, isError, isRefetching, refetch } = useOrganizationAuditLogsQuery( { slug, iso_timestamp_start: dateRange.from, iso_timestamp_end: dateRange.to, }, { enabled: canReadAuditLogs, retry(_failureCount, error) { if (error.message.endsWith('upgrade to Team or Enterprise Plan to access audit logs.')) { return false } return true }, retryOnMount: false, refetchOnWindowFocus: false, } ) // 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]) const roles = rolesData?.roles ?? [] const retentionPeriod = data?.retention_period ?? 0 const logs = data?.result ?? [] const sortedLogs = logs ?.sort((a, b) => dateSortDesc ? Number(new Date(b.occurred_at)) - Number(new Date(a.occurred_at)) : Number(new Date(a.occurred_at)) - Number(new Date(b.occurred_at)) ) ?.filter((log) => { if (filters.users.length > 0) { return filters.users.includes(log.actor.id) } else { return log } }) ?.filter((log) => { if (filters.projects.length > 0) { return filters.projects.includes(log.target.metadata.project_ref || '') } else { return log } }) const currentOrganization = organizations?.find((o) => o.slug === slug) if (!canReadAuditLogs) { return ( ) } return ( <>
{isLoading && (
)} {isError ? ( error.message.endsWith('upgrade to Team or Enterprise Plan to access audit logs.') ? (
Organization Audit Logs are not available on Free or Pro plans

Upgrade to Team or Enterprise to view up to 28 days of Audit Logs for your organization.

) : ( ) ) : null} {isSuccess && ( <>

Filter by

setFilters({ ...filters, users: values })} /> p.organization_id === currentOrganization?.id) ?? [] } labelKey="name" valueKey="ref" activeOptions={filters.projects} onSaveFilters={(values) => setFilters({ ...filters, projects: values })} /> { if (value.from !== null && value.to !== null) { const current = dayjs().utc() const from = dayjs(value.from) .utc() .hour(current.hour()) .minute(current.minute()) .second(current.second()) .toISOString() const to = dayjs(value.to) .utc() .hour(current.hour()) .minute(current.minute()) .second(current.second()) .toISOString() setDateRange({ from, to }) } }} renderFooter={() => { return ( Your organization has a log retention period of{' '} {retentionPeriod} day {retentionPeriod > 1 ? 's' : ''} . You may only view logs from{' '} {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the earliest date. ) }} /> {isSuccess && ( <>

Viewing {sortedLogs.length} logs in total

)}
{logs.length === 0 ? (

Your organization does not have any audit logs available yet

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

No audit logs found based on the filters applied

) : ( User , Action , Target ,

Date

, , ]} body={ sortedLogs?.map((log) => { const user = (members ?? []).find( (member) => member.gotrue_id === log.actor.id ) const role = roles.find((role) => user?.role_ids?.[0] === role.id) const project = projects?.find( (project) => project.ref === log.target.metadata.project_ref ) const organization = organizations?.find( (org) => org.slug === log.target.metadata.org_slug ) const hasStatusCode = log.action.metadata[0]?.status !== undefined const userIcon = user === undefined ? (

?

) : user?.invited_id || user?.username === user?.primary_email ? (
) : ( ) return ( setSelectedLog(log)} className="cursor-pointer hover:!bg-alternative transition duration-100" >
{userIcon}

{user?.username ?? '-'}

{role && (

{role?.name}

)}
{hasStatusCode && (

{log.action.metadata[0].status}

)}

{log.action.name}

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

{log.target.metadata.project_ref ? 'Ref: ' : log.target.metadata.org_slug ? 'Slug: ' : null} {log.target.metadata.project_ref ?? log.target.metadata.org_slug}

{dayjs(log.occurred_at).format('DD MMM YYYY, HH:mm:ss')}
) }) ?? [] } /> )} )} setSelectedLog(undefined)} /> ) } export default AuditLogs