UserLogs.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import { useParams } from 'common'
  2. import { ExternalLink, RefreshCw } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { useQueryState } from 'nuqs'
  5. import { useEffect } from 'react'
  6. import { Button, cn, CriticalIcon, Separator } from 'ui'
  7. import { Admonition, TimestampInfo } from 'ui-patterns'
  8. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  9. import { UserHeader } from './UserHeader'
  10. import { PANEL_PADDING } from './Users.constants'
  11. import { LOGS_TABLES } from '@/components/interfaces/Settings/Logs/Logs.constants'
  12. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  13. import { User } from '@/data/auth/users-infinite-query'
  14. import useLogsPreview from '@/hooks/analytics/useLogsPreview'
  15. import { useLogsUrlState } from '@/hooks/analytics/useLogsUrlState'
  16. interface UserLogsProps {
  17. user: User
  18. }
  19. const API_LOGS_QUERY = (userId: string) =>
  20. `select\n cast(timestamp as datetime) as timestamp,\n event_message, metadata \nfrom edge_logs \nWHERE (\n metadata[SAFE_OFFSET(0)].request[SAFE_OFFSET(0)].sb[SAFE_OFFSET(0)].auth_user\n = '${userId}'\n)\nlimit 100`
  21. export const UserLogs = ({ user }: UserLogsProps) => {
  22. const { ref } = useParams()
  23. const { filters, setFilters } = useLogsUrlState()
  24. const [, setFiltersValue] = useQueryState('f')
  25. const {
  26. logData: authLogs,
  27. isSuccess: isSuccessAuthLogs,
  28. isLoading: isLoadingAuthLogs,
  29. refresh,
  30. } = useLogsPreview({
  31. projectRef: ref as string,
  32. table: LOGS_TABLES.auth,
  33. filterOverride: { search_query: user.id },
  34. limit: 5,
  35. })
  36. useEffect(() => {
  37. if (user.id) setFilters({ ...filters, search_query: user.id })
  38. return () => {
  39. setFiltersValue(null)
  40. }
  41. // eslint-disable-next-line react-hooks/exhaustive-deps
  42. }, [user.id])
  43. return (
  44. <div>
  45. <UserHeader user={user} />
  46. <Separator />
  47. <div className={cn('flex flex-col gap-y-3', PANEL_PADDING)}>
  48. <div>
  49. <p>API logs</p>
  50. <p className="text-sm text-foreground-light">
  51. View edge logs for requests made by this user
  52. </p>
  53. </div>
  54. <Button asChild type="default" className="w-min">
  55. <Link
  56. href={`/project/${ref}/logs/explorer?q=${encodeURIComponent(API_LOGS_QUERY(user.id ?? ''))}`}
  57. >
  58. Open in Log Explorer
  59. </Link>
  60. </Button>
  61. </div>
  62. <Separator />
  63. <div className={cn('flex flex-col gap-y-3', PANEL_PADDING)}>
  64. <div>
  65. <p>Authentication logs</p>
  66. <p className="text-sm text-foreground-light">
  67. Latest logs from authentication for this user in the past hour
  68. </p>
  69. </div>
  70. {/* [Joshen] This whole thing here i reckon we can shift to a component, if in the future we wanna add more user logs */}
  71. <div className="flex items-center justify-between">
  72. <div className="flex items-center">
  73. <Button
  74. type={'status_code' in filters ? 'default' : 'secondary'}
  75. className="rounded-r-none border-r-0"
  76. disabled={isLoadingAuthLogs}
  77. onClick={() => setFilters({ search_query: user.id })}
  78. >
  79. Show all
  80. </Button>
  81. <div className="border-button border border-l-0 py-3" />
  82. <Button
  83. type={'status_code' in filters ? 'secondary' : 'default'}
  84. className="rounded-l-none border-l-0"
  85. disabled={isLoadingAuthLogs}
  86. onClick={() =>
  87. setFilters({
  88. search_query: user.id,
  89. status_code: { client_error: true, server_error: true },
  90. })
  91. }
  92. >
  93. Error only
  94. </Button>
  95. </div>
  96. <Button
  97. type="default"
  98. loading={isLoadingAuthLogs}
  99. disabled={isLoadingAuthLogs}
  100. icon={<RefreshCw />}
  101. onClick={() => refresh()}
  102. >
  103. Refresh
  104. </Button>
  105. </div>
  106. {isLoadingAuthLogs && !isSuccessAuthLogs ? (
  107. <GenericSkeletonLoader />
  108. ) : authLogs.length === 0 ? (
  109. <Admonition
  110. type="note"
  111. title="No authentication logs available for this user"
  112. description="Auth events such as logging in will be shown here"
  113. />
  114. ) : (
  115. <div>
  116. <div className="border border-b-0 rounded-t-md divide-y overflow-hidden">
  117. {authLogs.map((log) => {
  118. const status = ((log.status ?? '-') as any).toString()
  119. const is400 = status.startsWith('4')
  120. const is500 = status.startsWith('5')
  121. return (
  122. <div
  123. key={log.id}
  124. className="flex items-center transition font-mono px-2 py-1.5 bg-surface-100 divide-x"
  125. >
  126. <p className="text-xs text-foreground-light min-w-[125px] w-[125px] px-1">
  127. <TimestampInfo utcTimestamp={log.timestamp / 1000} />
  128. </p>
  129. <div className="flex items-center text-xs text-foreground-light h-[22px] min-w-[70px] w-[70px] px-2">
  130. <div
  131. className={cn(
  132. 'flex items-center justify-center gap-x-1',
  133. !!log.status && 'border px-1 py-0.5 rounded-sm',
  134. is400
  135. ? 'text-warning border-warning bg-warning-300'
  136. : is500
  137. ? 'text-destructive border-destructive bg-destructive-300'
  138. : ''
  139. )}
  140. >
  141. {(is400 || is500) && (
  142. <CriticalIcon hideBackground className={cn(is400 && 'text-warning')} />
  143. )}
  144. {status}
  145. </div>
  146. </div>
  147. <p className="group relative flex items-center py-1.5 text-xs text-foreground-light px-2 truncate w-full">
  148. {`${log.path} | ${log.msg}`}
  149. <ButtonTooltip
  150. type="outline"
  151. asChild
  152. tooltip={{ content: { text: 'Open in logs' } }}
  153. className="px-1.5 absolute right-0 top-0 opacity-0 group-hover:opacity-100 transition bg-background focus-visible:opacity-100"
  154. >
  155. <Link href={`/project/${ref}/logs/auth-logs?log=${log.id}`}>
  156. <ExternalLink size="12" className="text-foreground-light" />
  157. </Link>
  158. </ButtonTooltip>
  159. </p>
  160. </div>
  161. )
  162. })}
  163. </div>
  164. <Button
  165. block
  166. asChild
  167. type="outline"
  168. className="transition rounded-t-none text-foreground-light hover:text-foreground"
  169. >
  170. <Link href={`/project/${ref}/logs/auth-logs?s=${user.id}`}>See more logs</Link>
  171. </Button>
  172. </div>
  173. )}
  174. </div>
  175. </div>
  176. )
  177. }