LogSelection.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { Check, Copy, MousePointerClick, X } from 'lucide-react'
  2. import { useEffect, useState } from 'react'
  3. import {
  4. Button,
  5. cn,
  6. copyToClipboard,
  7. Tabs_Shadcn_,
  8. TabsContent_Shadcn_,
  9. TabsList_Shadcn_,
  10. TabsTrigger_Shadcn_,
  11. } from 'ui'
  12. import { CodeBlock } from 'ui-patterns/CodeBlock'
  13. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  14. import type { LogData, QueryType } from './Logs.types'
  15. import { apiKey, role as extractRole, jwtAPIKey } from './Logs.utils'
  16. import DefaultPreviewSelectionRenderer from './LogSelectionRenderers/DefaultPreviewSelectionRenderer'
  17. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  18. export interface LogSelectionProps {
  19. log?: LogData
  20. onClose: () => void
  21. queryType?: QueryType
  22. projectRef: string
  23. isLoading: boolean
  24. error?: string | object
  25. }
  26. const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectionProps) => {
  27. const [showCopied, setShowCopied] = useState(false)
  28. useEffect(() => {
  29. if (!showCopied) return
  30. const timer = setTimeout(() => setShowCopied(false), 2000)
  31. return () => clearTimeout(timer)
  32. }, [showCopied])
  33. const LogDetails = () => {
  34. if (error) return <LogErrorState error={error} />
  35. if (!log) return <LogDetailEmptyState />
  36. switch (queryType) {
  37. case 'api':
  38. const status = log?.metadata?.[0]?.response?.[0]?.status_code
  39. const method = log?.metadata?.[0]?.request?.[0]?.method
  40. const path = log?.metadata?.[0]?.request?.[0]?.path
  41. const search = log?.metadata?.[0]?.request?.[0]?.search
  42. const user_agent = log?.metadata?.[0]?.request?.[0]?.headers[0].user_agent
  43. const error_code = log?.metadata?.[0]?.response?.[0]?.headers?.[0]?.x_sb_error_code
  44. const apikey = jwtAPIKey(log?.metadata) ?? apiKey(log?.metadata)
  45. const role = extractRole(log?.metadata)
  46. const { id, metadata, timestamp, event_message, ...rest } = log
  47. const apiLog = {
  48. id,
  49. status,
  50. method,
  51. path,
  52. search,
  53. user_agent,
  54. timestamp,
  55. event_message,
  56. metadata,
  57. ...(apikey ? { apikey } : null),
  58. ...(error_code ? { error_code } : null),
  59. ...(role ? { role } : null),
  60. ...rest,
  61. }
  62. return <DefaultPreviewSelectionRenderer log={apiLog} />
  63. case 'database':
  64. const hint = log?.metadata?.[0]?.parsed?.[0]?.hint
  65. const detail = log?.metadata?.[0]?.parsed?.[0]?.detail
  66. const query = log?.metadata?.[0]?.parsed?.[0]?.query
  67. const postgresLog = {
  68. ...(hint && { hint }),
  69. ...(detail && { detail }),
  70. ...(query && { query }),
  71. ...log,
  72. }
  73. return <DefaultPreviewSelectionRenderer log={postgresLog} />
  74. default:
  75. return <DefaultPreviewSelectionRenderer log={log} />
  76. }
  77. }
  78. return (
  79. <div className="relative flex h-full grow flex-col overflow-y-scroll bg-surface-100 border-t">
  80. <div className="relative grow flex flex-col h-full">
  81. <Tabs_Shadcn_ defaultValue="details" className="flex flex-col h-full">
  82. <TabsList_Shadcn_ className="px-2 pt-2 relative">
  83. <TabsTrigger_Shadcn_ className="px-3" value="details">
  84. Details
  85. </TabsTrigger_Shadcn_>
  86. <TabsTrigger_Shadcn_ disabled={!log} className="px-3" value="raw">
  87. Raw
  88. </TabsTrigger_Shadcn_>
  89. <div className="*:px-1.5 *:text-foreground-lighter ml-auto flex gap-1 absolute right-2 top-2">
  90. <ButtonTooltip
  91. disabled={!log || isLoading}
  92. type="text"
  93. tooltip={{
  94. content: {
  95. side: 'left',
  96. text: isLoading ? 'Loading log...' : 'Copy as JSON',
  97. },
  98. }}
  99. icon={showCopied ? <Check /> : <Copy />}
  100. onClick={() => {
  101. setShowCopied(true)
  102. copyToClipboard(JSON.stringify(log, null, 2))
  103. }}
  104. />
  105. <Button type="text" onClick={onClose}>
  106. <X size={14} strokeWidth={2} />
  107. </Button>
  108. </div>
  109. </TabsList_Shadcn_>
  110. <div className="flex-1 h-full">
  111. {isLoading ? (
  112. <div className="p-4">
  113. <GenericSkeletonLoader />
  114. </div>
  115. ) : (
  116. <>
  117. <TabsContent_Shadcn_ className="space-y-6 h-full" value="details">
  118. <LogDetails />
  119. </TabsContent_Shadcn_>
  120. <TabsContent_Shadcn_ value="raw">
  121. <CodeBlock
  122. hideLineNumbers
  123. language="json"
  124. className="prose w-full pt-0 max-w-full border-none"
  125. >
  126. {JSON.stringify(log, null, 2)}
  127. </CodeBlock>
  128. </TabsContent_Shadcn_>
  129. </>
  130. )}
  131. </div>
  132. </Tabs_Shadcn_>
  133. </div>
  134. </div>
  135. )
  136. }
  137. export default LogSelection
  138. function LogDetailEmptyState({
  139. title = 'Select an Event',
  140. message = 'Select an Event to view the complete JSON payload',
  141. }: {
  142. title?: string
  143. message?: string
  144. }) {
  145. return (
  146. <div
  147. className={cn(
  148. 'flex h-full w-full flex-col items-center justify-center gap-2 overflow-y-scroll text-center transition-all px-4'
  149. )}
  150. >
  151. <div
  152. className={cn(
  153. 'flex w-full max-w-sm flex-col items-center justify-center gap-6 text-center transition-all delay-300 duration-500'
  154. )}
  155. >
  156. <div className="relative flex h-4 w-32 items-center rounded-sm border border-control px-2">
  157. <div className="h-0.5 w-2/3 rounded-full bg-surface-300"></div>
  158. <div className="absolute right-1 -bottom-4">
  159. <MousePointerClick size="24" strokeWidth={1} />
  160. </div>
  161. </div>
  162. <div className="flex flex-col gap-1">
  163. <h3 className="text-sm text-foreground">{title}</h3>
  164. <p className="text-xs text-foreground-lighter">{message}</p>
  165. </div>
  166. </div>
  167. </div>
  168. )
  169. }
  170. function LogErrorState({ error }: { error?: string | object }) {
  171. return <pre>{JSON.stringify(error, null, 2)}</pre>
  172. }