| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- 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<AuditLog>()
- 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 (
- <>
- <ScaffoldContainer className="px-6 xl:px-10">
- <ScaffoldSection isFullWidth>
- <div className="space-y-4 flex flex-col">
- <div className="flex flex-col md:flex-row md:items-center justify-between">
- <div className="flex items-center space-x-2">
- <p className="text-xs prose">Filter by</p>
- <FilterPopover
- name="Projects"
- options={projects ?? []}
- labelKey="name"
- valueKey="ref"
- activeOptions={filters.projects}
- onSaveFilters={(values) => setFilters({ ...filters, projects: values })}
- search={search}
- setSearch={setSearch}
- hasNextPage={hasNextPage}
- isLoading={isLoadingProjects}
- isFetching={isFetching}
- isFetchingNextPage={isFetchingNextPage}
- fetchNextPage={fetchNextPage}
- />
- <LogsDatePicker
- hideWarnings
- value={dateRange}
- onSubmit={(value) => 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 && (
- <>
- <div className="h-[20px] border-r border-strong !ml-4 !mr-2" />
- <p className="prose text-xs">Viewing {sortedLogs.length} logs in total</p>
- </>
- )}
- </div>
- <Button
- type="default"
- disabled={isLoading || isRefetching}
- icon={<RefreshCw className={isRefetching ? 'animate-spin' : ''} />}
- onClick={() => refetch()}
- >
- {isRefetching ? 'Refreshing' : 'Refresh'}
- </Button>
- </div>
- {isLoading && (
- <div className="space-y-2">
- <ShimmeringLoader />
- <ShimmeringLoader className="w-3/4" />
- <ShimmeringLoader className="w-1/2" />
- </div>
- )}
- {isError && <AlertError error={error} subject="Failed to retrieve audit logs" />}
- {isSuccess && (
- <>
- {logs.length === 0 ? (
- <div className="bg-surface-100 border rounded-sm p-4 flex items-center justify-between">
- <p className="prose text-sm">You do not have any audit logs available yet</p>
- </div>
- ) : logs.length > 0 && sortedLogs.length === 0 ? (
- <div className="bg-surface-100 border rounded-sm p-4 flex items-center justify-between">
- <p className="prose text-sm">
- No audit logs found based on the filters applied
- </p>
- </div>
- ) : (
- <div className="overflow-hidden md:overflow-auto overflow-x-scroll">
- <Table
- head={[
- <Table.th key="action" className="py-2">
- Action
- </Table.th>,
- <Table.th key="target" className="py-2">
- Target
- </Table.th>,
- <Table.th key="date" className="py-2">
- <div className="flex items-center space-x-2">
- <p>Date</p>
- <ButtonTooltip
- type="text"
- className="px-1"
- icon={
- dateSortDesc ? (
- <ArrowDown strokeWidth={1.5} size={14} />
- ) : (
- <ArrowUp strokeWidth={1.5} size={14} />
- )
- }
- onClick={() => setDateSortDesc(!dateSortDesc)}
- tooltip={{
- content: {
- side: 'bottom',
- text: dateSortDesc ? 'Sort latest first' : 'Sort earliest first',
- },
- }}
- />
- </div>
- </Table.th>,
- <Table.th key="actions" className="py-2"></Table.th>,
- ]}
- 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 (
- <Table.tr
- key={log.request_id}
- onClick={() => setSelectedLog(log)}
- className="cursor-pointer hover:bg-alternative! transition duration-100"
- >
- <Table.td className="max-w-[250px]">
- <div className="flex items-center space-x-2">
- <p className="bg-surface-200 rounded-sm px-1 flex items-center justify-center text-xs font-mono border">
- {log.action.status}
- </p>
- <p className="text-foreground-light text-xs font-mono">
- {log.action.method}
- </p>
- <p className="truncate" title={log.action.name}>
- {log.action.name}
- </p>
- </div>
- </Table.td>
- <Table.td>
- {project || organization ? (
- <>
- <p
- className="text-foreground-light max-w-[230px] truncate"
- title={project?.name ?? organization?.name}
- >
- {project?.name
- ? 'Project: '
- : organization?.name
- ? 'Organization: '
- : null}
- {project?.name ?? organization?.name}
- </p>
- <p
- className="text-foreground-light text-xs mt-0.5 truncate"
- title={log.project_ref ?? log.organization_slug ?? ''}
- >
- {log.project_ref ? 'Ref: ' : 'Slug: '}
- {log.project_ref ?? log.organization_slug}
- </p>
- </>
- ) : (
- <p className="text-foreground-light text-sm">
- {log.project_ref ?? log.organization_slug ?? '-'}
- </p>
- )}
- </Table.td>
- <Table.td>
- <TimestampInfo className="text-sm" utcTimestamp={isoTimestamp} />
- </Table.td>
- <Table.td align="right">
- <Button type="default">View details</Button>
- </Table.td>
- </Table.tr>
- )
- }) ?? []
- }
- />
- </div>
- )}
- </>
- )}
- </div>
- </ScaffoldSection>
- </ScaffoldContainer>
- <LogDetailsPanel selectedLog={selectedLog} onClose={() => setSelectedLog(undefined)} />
- </>
- )
- }
|