Files
Gildas Garcia c6fc456910 chore: cleanup duplicate exports studio (#47387)
## Problem

Knip reports many duplicate exports (both named and default). Besides,
we're moving away from default exports and even have an eslint rule to
enforce it on new code.

## Solution

- Cleanup those exports
- Update imports when necessary

No functional changes. If it builds, it's fine
2026-06-29 15:46:16 +02:00

306 lines
13 KiB
TypeScript

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
variant="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
variant="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 variant="default">View details</Button>
</Table.td>
</Table.tr>
)
}) ?? []
}
/>
</div>
)}
</>
)}
</div>
</ScaffoldSection>
</ScaffoldContainer>
<LogDetailsPanel selectedLog={selectedLog} onClose={() => setSelectedLog(undefined)} />
</>
)
}