AuditLogs.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import { keepPreviousData } from '@tanstack/react-query'
  2. import { useDebounce } from '@uidotdev/usehooks'
  3. import dayjs from 'dayjs'
  4. import { ArrowDown, ArrowUp, RefreshCw } from 'lucide-react'
  5. import { useEffect, useMemo, useState } from 'react'
  6. import { Button } from 'ui'
  7. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  8. import { TimestampInfo } from 'ui-patterns/TimestampInfo'
  9. import { LogsDatePicker } from '../Settings/Logs/Logs.DatePickers'
  10. import { filterByProjects, sortAuditLogs } from './AuditLogs.utils'
  11. import { LogDetailsPanel } from '@/components/interfaces/AuditLogs/LogDetailsPanel'
  12. import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
  13. import Table from '@/components/to-be-cleaned/Table'
  14. import AlertError from '@/components/ui/AlertError'
  15. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  16. import { FilterPopover } from '@/components/ui/FilterPopover'
  17. import {
  18. TIMESTAMP_MICROS_PER_MS,
  19. type AuditLog,
  20. } from '@/data/organizations/organization-audit-logs-query'
  21. import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
  22. import { useProfileAuditLogsQuery } from '@/data/profile/profile-audit-logs-query'
  23. import { useProjectsInfiniteQuery } from '@/data/projects/projects-infinite-query'
  24. export const AuditLogs = () => {
  25. const currentTime = dayjs().utc().set('millisecond', 0)
  26. const [search, setSearch] = useState('')
  27. const debouncedSearch = useDebounce(search, 500)
  28. const [dateSortDesc, setDateSortDesc] = useState(true)
  29. const [dateRange, setDateRange] = useState({
  30. from: currentTime.subtract(1, 'day').toISOString(),
  31. to: currentTime.toISOString(),
  32. })
  33. const [selectedLog, setSelectedLog] = useState<AuditLog>()
  34. const [filters, setFilters] = useState<{ projects: string[] }>({
  35. projects: [],
  36. })
  37. const {
  38. data: projectsData,
  39. isLoading: isLoadingProjects,
  40. isFetching,
  41. isFetchingNextPage,
  42. hasNextPage,
  43. fetchNextPage,
  44. } = useProjectsInfiniteQuery(
  45. { search: search.length === 0 ? search : debouncedSearch },
  46. { placeholderData: keepPreviousData }
  47. )
  48. const projects =
  49. useMemo(() => projectsData?.pages.flatMap((page) => page.projects), [projectsData?.pages]) || []
  50. const { data: organizations } = useOrganizationsQuery()
  51. const {
  52. data,
  53. error,
  54. isPending: isLoading,
  55. isSuccess,
  56. isError,
  57. isRefetching,
  58. refetch,
  59. } = useProfileAuditLogsQuery(
  60. {
  61. iso_timestamp_start: dateRange.from,
  62. iso_timestamp_end: dateRange.to,
  63. },
  64. {
  65. retry: false,
  66. }
  67. )
  68. const logs = data?.result ?? []
  69. const sortedLogs = filterByProjects(sortAuditLogs(logs, dateSortDesc), filters.projects)
  70. // This feature depends on the subscription tier of the user. Free user can view logs up to 1 day
  71. // in the past. The API limits the logs to maximum of 1 day and 5 minutes so when the page is
  72. // viewed for more than 5 minutes, the call parameters needs to be updated. This also works with
  73. // higher tiers (7 days of logs).The user will see a loading shimmer.
  74. useEffect(() => {
  75. const duration = dayjs(dateRange.from).diff(dayjs(dateRange.to))
  76. const interval = setInterval(() => {
  77. const currentTime = dayjs().utc().set('millisecond', 0)
  78. setDateRange({
  79. from: currentTime.add(duration).toISOString(),
  80. to: currentTime.toISOString(),
  81. })
  82. }, 5 * 60000)
  83. return () => clearInterval(interval)
  84. }, [dateRange.from, dateRange.to])
  85. return (
  86. <>
  87. <ScaffoldContainer className="px-6 xl:px-10">
  88. <ScaffoldSection isFullWidth>
  89. <div className="space-y-4 flex flex-col">
  90. <div className="flex flex-col md:flex-row md:items-center justify-between">
  91. <div className="flex items-center space-x-2">
  92. <p className="text-xs prose">Filter by</p>
  93. <FilterPopover
  94. name="Projects"
  95. options={projects ?? []}
  96. labelKey="name"
  97. valueKey="ref"
  98. activeOptions={filters.projects}
  99. onSaveFilters={(values) => setFilters({ ...filters, projects: values })}
  100. search={search}
  101. setSearch={setSearch}
  102. hasNextPage={hasNextPage}
  103. isLoading={isLoadingProjects}
  104. isFetching={isFetching}
  105. isFetchingNextPage={isFetchingNextPage}
  106. fetchNextPage={fetchNextPage}
  107. />
  108. <LogsDatePicker
  109. hideWarnings
  110. value={dateRange}
  111. onSubmit={(value) => setDateRange(value)}
  112. helpers={[
  113. {
  114. text: 'Last 1 hour',
  115. calcFrom: () => dayjs().subtract(1, 'hour').toISOString(),
  116. calcTo: () => dayjs().toISOString(),
  117. },
  118. {
  119. text: 'Last 3 hours',
  120. calcFrom: () => dayjs().subtract(3, 'hour').toISOString(),
  121. calcTo: () => dayjs().toISOString(),
  122. },
  123. {
  124. text: 'Last 6 hours',
  125. calcFrom: () => dayjs().subtract(6, 'hour').toISOString(),
  126. calcTo: () => dayjs().toISOString(),
  127. },
  128. {
  129. text: 'Last 12 hours',
  130. calcFrom: () => dayjs().subtract(12, 'hour').toISOString(),
  131. calcTo: () => dayjs().toISOString(),
  132. },
  133. {
  134. text: 'Last 24 hours',
  135. calcFrom: () => dayjs().subtract(1, 'day').toISOString(),
  136. calcTo: () => dayjs().toISOString(),
  137. },
  138. ]}
  139. />
  140. {isSuccess && (
  141. <>
  142. <div className="h-[20px] border-r border-strong !ml-4 !mr-2" />
  143. <p className="prose text-xs">Viewing {sortedLogs.length} logs in total</p>
  144. </>
  145. )}
  146. </div>
  147. <Button
  148. type="default"
  149. disabled={isLoading || isRefetching}
  150. icon={<RefreshCw className={isRefetching ? 'animate-spin' : ''} />}
  151. onClick={() => refetch()}
  152. >
  153. {isRefetching ? 'Refreshing' : 'Refresh'}
  154. </Button>
  155. </div>
  156. {isLoading && (
  157. <div className="space-y-2">
  158. <ShimmeringLoader />
  159. <ShimmeringLoader className="w-3/4" />
  160. <ShimmeringLoader className="w-1/2" />
  161. </div>
  162. )}
  163. {isError && <AlertError error={error} subject="Failed to retrieve audit logs" />}
  164. {isSuccess && (
  165. <>
  166. {logs.length === 0 ? (
  167. <div className="bg-surface-100 border rounded-sm p-4 flex items-center justify-between">
  168. <p className="prose text-sm">You do not have any audit logs available yet</p>
  169. </div>
  170. ) : logs.length > 0 && sortedLogs.length === 0 ? (
  171. <div className="bg-surface-100 border rounded-sm p-4 flex items-center justify-between">
  172. <p className="prose text-sm">
  173. No audit logs found based on the filters applied
  174. </p>
  175. </div>
  176. ) : (
  177. <div className="overflow-hidden md:overflow-auto overflow-x-scroll">
  178. <Table
  179. head={[
  180. <Table.th key="action" className="py-2">
  181. Action
  182. </Table.th>,
  183. <Table.th key="target" className="py-2">
  184. Target
  185. </Table.th>,
  186. <Table.th key="date" className="py-2">
  187. <div className="flex items-center space-x-2">
  188. <p>Date</p>
  189. <ButtonTooltip
  190. type="text"
  191. className="px-1"
  192. icon={
  193. dateSortDesc ? (
  194. <ArrowDown strokeWidth={1.5} size={14} />
  195. ) : (
  196. <ArrowUp strokeWidth={1.5} size={14} />
  197. )
  198. }
  199. onClick={() => setDateSortDesc(!dateSortDesc)}
  200. tooltip={{
  201. content: {
  202. side: 'bottom',
  203. text: dateSortDesc ? 'Sort latest first' : 'Sort earliest first',
  204. },
  205. }}
  206. />
  207. </div>
  208. </Table.th>,
  209. <Table.th key="actions" className="py-2"></Table.th>,
  210. ]}
  211. body={
  212. sortedLogs?.map((log) => {
  213. const project = projects?.find((p) => p.ref === log.project_ref)
  214. const organization = organizations?.find(
  215. (org) => org.slug === log.organization_slug
  216. )
  217. const isoTimestamp = dayjs(
  218. log.timestamp / TIMESTAMP_MICROS_PER_MS
  219. ).toISOString()
  220. return (
  221. <Table.tr
  222. key={log.request_id}
  223. onClick={() => setSelectedLog(log)}
  224. className="cursor-pointer hover:bg-alternative! transition duration-100"
  225. >
  226. <Table.td className="max-w-[250px]">
  227. <div className="flex items-center space-x-2">
  228. <p className="bg-surface-200 rounded-sm px-1 flex items-center justify-center text-xs font-mono border">
  229. {log.action.status}
  230. </p>
  231. <p className="text-foreground-light text-xs font-mono">
  232. {log.action.method}
  233. </p>
  234. <p className="truncate" title={log.action.name}>
  235. {log.action.name}
  236. </p>
  237. </div>
  238. </Table.td>
  239. <Table.td>
  240. {project || organization ? (
  241. <>
  242. <p
  243. className="text-foreground-light max-w-[230px] truncate"
  244. title={project?.name ?? organization?.name}
  245. >
  246. {project?.name
  247. ? 'Project: '
  248. : organization?.name
  249. ? 'Organization: '
  250. : null}
  251. {project?.name ?? organization?.name}
  252. </p>
  253. <p
  254. className="text-foreground-light text-xs mt-0.5 truncate"
  255. title={log.project_ref ?? log.organization_slug ?? ''}
  256. >
  257. {log.project_ref ? 'Ref: ' : 'Slug: '}
  258. {log.project_ref ?? log.organization_slug}
  259. </p>
  260. </>
  261. ) : (
  262. <p className="text-foreground-light text-sm">
  263. {log.project_ref ?? log.organization_slug ?? '-'}
  264. </p>
  265. )}
  266. </Table.td>
  267. <Table.td>
  268. <TimestampInfo className="text-sm" utcTimestamp={isoTimestamp} />
  269. </Table.td>
  270. <Table.td align="right">
  271. <Button type="default">View details</Button>
  272. </Table.td>
  273. </Table.tr>
  274. )
  275. }) ?? []
  276. }
  277. />
  278. </div>
  279. )}
  280. </>
  281. )}
  282. </div>
  283. </ScaffoldSection>
  284. </ScaffoldContainer>
  285. <LogDetailsPanel selectedLog={selectedLog} onClose={() => setSelectedLog(undefined)} />
  286. </>
  287. )
  288. }