LogsQueryPanel.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import { useFlag } from 'common'
  2. import { BookOpen, Check, ChevronDown, ChevronsUpDown, Copy, ExternalLink, X } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { ReactNode, useEffect, useState } from 'react'
  5. import { logConstants } from 'shared-data'
  6. import {
  7. Badge,
  8. Button,
  9. cn,
  10. Command,
  11. CommandEmpty,
  12. CommandGroup,
  13. CommandInput,
  14. CommandItem,
  15. CommandList,
  16. copyToClipboard,
  17. DropdownMenu,
  18. DropdownMenuContent,
  19. DropdownMenuItem,
  20. DropdownMenuTrigger,
  21. Label,
  22. Popover,
  23. PopoverContent,
  24. PopoverTrigger,
  25. SidePanel,
  26. Switch,
  27. Tooltip,
  28. TooltipContent,
  29. TooltipTrigger,
  30. } from 'ui'
  31. import {
  32. EXPLORER_DATEPICKER_HELPERS,
  33. LOGS_SOURCE_DESCRIPTION,
  34. LogsTableName,
  35. } from './Logs.constants'
  36. import { DatePickerValue, LogsDatePicker } from './Logs.DatePickers'
  37. import { LogsWarning, LogTemplate } from './Logs.types'
  38. import Table from '@/components/to-be-cleaned/Table'
  39. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  40. import { DOCS_URL } from '@/lib/constants'
  41. export interface LogsQueryPanelProps {
  42. templates?: LogTemplate[]
  43. value: DatePickerValue
  44. warnings: LogsWarning[]
  45. onSelectTemplate: (template: LogTemplate) => void
  46. onSelectSource: (source: string) => void
  47. onDateChange: (value: DatePickerValue) => void
  48. useOtel?: boolean
  49. onUseOtelChange?: (value: boolean) => void
  50. }
  51. function DropdownMenuItemContent({ name, desc }: { name: ReactNode; desc?: string }) {
  52. return (
  53. <div className="grid gap-1">
  54. <div className="font-mono font-bold">{name}</div>
  55. {desc && <div className="text-foreground-light">{desc}</div>}
  56. </div>
  57. )
  58. }
  59. const LogsQueryPanel = ({
  60. templates = [],
  61. value,
  62. warnings,
  63. onSelectTemplate,
  64. onSelectSource,
  65. onDateChange,
  66. useOtel = false,
  67. onUseOtelChange,
  68. }: LogsQueryPanelProps) => {
  69. const [showReference, setShowReference] = useState(false)
  70. const { logsTemplates } = useIsFeatureEnabled(['logs:templates'])
  71. const showChToggleInLogExplorer = useFlag('showChToggleInLogExplorer')
  72. const otelToggleEnabled = !!showChToggleInLogExplorer && !!onUseOtelChange
  73. const {
  74. projectAuthAll: authEnabled,
  75. projectStorageAll: storageEnabled,
  76. projectEdgeFunctionAll: edgeFunctionsEnabled,
  77. } = useIsFeatureEnabled(['project_auth:all', 'project_storage:all', 'project_edge_function:all'])
  78. const logsTableNames = Object.entries(LogsTableName)
  79. .filter(([key]) => {
  80. if (key === 'AUTH') return authEnabled
  81. if (key === 'STORAGE') return storageEnabled
  82. if (key === 'FN_EDGE') return edgeFunctionsEnabled
  83. if (key === 'PG_CRON') return false
  84. return true
  85. })
  86. .map(([, value]) => value)
  87. const [selectedDatePickerValue, setSelectedDatePickerValue] = useState<DatePickerValue>(value)
  88. useEffect(() => {
  89. setSelectedDatePickerValue(value)
  90. }, [value.from, value.to, value.text, value.isHelper])
  91. const [open, setOpen] = useState(false)
  92. const [selectedSchema, setSelectedSchema] = useState(logConstants.schemas[0])
  93. return (
  94. <div className="flex items-center border-b bg-surface-100 h-(--header-height)">
  95. <div className="flex w-full items-center justify-between px-4 md:px-5 py-2 overflow-x-scroll no-scrollbar">
  96. <div className="flex w-full flex-row items-center justify-between gap-x-4">
  97. <div className="flex items-center gap-2">
  98. <DropdownMenu>
  99. <DropdownMenuTrigger asChild>
  100. <Button type="default" iconRight={<ChevronDown />}>
  101. Insert source
  102. </Button>
  103. </DropdownMenuTrigger>
  104. <DropdownMenuContent
  105. side="bottom"
  106. align="start"
  107. className="max-h-[390px] overflow-auto"
  108. >
  109. {logsTableNames
  110. .sort((a, b) => a.localeCompare(b))
  111. .map((source) => (
  112. <DropdownMenuItem key={source} onClick={() => onSelectSource(source)}>
  113. <DropdownMenuItemContent
  114. name={source}
  115. desc={LOGS_SOURCE_DESCRIPTION[source]}
  116. />
  117. </DropdownMenuItem>
  118. ))}
  119. </DropdownMenuContent>
  120. </DropdownMenu>
  121. {logsTemplates && (
  122. <DropdownMenu>
  123. <DropdownMenuTrigger asChild>
  124. <Button type="default" iconRight={<ChevronDown />}>
  125. Templates
  126. </Button>
  127. </DropdownMenuTrigger>
  128. <DropdownMenuContent side="bottom" align="start">
  129. {templates
  130. .sort((a, b) => a.label!.localeCompare(b.label!))
  131. .map((template) => (
  132. <DropdownMenuItem
  133. key={template.label}
  134. onClick={() => onSelectTemplate(template)}
  135. >
  136. <p>{template.label}</p>
  137. </DropdownMenuItem>
  138. ))}
  139. </DropdownMenuContent>
  140. </DropdownMenu>
  141. )}
  142. <LogsDatePicker
  143. value={selectedDatePickerValue}
  144. onSubmit={(value) => {
  145. setSelectedDatePickerValue(value)
  146. onDateChange(value)
  147. }}
  148. helpers={EXPLORER_DATEPICKER_HELPERS}
  149. />
  150. {otelToggleEnabled && (
  151. <Tooltip>
  152. <TooltipTrigger asChild>
  153. <div className="flex items-center gap-2">
  154. <Switch
  155. id="logs-explorer-otel-toggle"
  156. checked={useOtel}
  157. onCheckedChange={(checked) => onUseOtelChange?.(checked)}
  158. />
  159. <Label
  160. htmlFor="logs-explorer-otel-toggle"
  161. className="text-xs text-foreground-light cursor-pointer"
  162. >
  163. OTEL endpoint
  164. </Label>
  165. </div>
  166. </TooltipTrigger>
  167. <TooltipContent side="bottom" className="max-w-xs">
  168. Run this query against the new ClickHouse-backed OTEL endpoint instead of
  169. BigQuery. Use to validate ClickHouse SQL before relying on it.
  170. </TooltipContent>
  171. </Tooltip>
  172. )}
  173. <div
  174. data-testid="log-explorer-warnings"
  175. className={`transition-all duration-300 h-full ${
  176. warnings.length > 0 ? 'opacity-100' : 'invisible h-0 w-0 opacity-0'
  177. }`}
  178. >
  179. <Tooltip>
  180. <TooltipTrigger className="flex items-start">
  181. <Badge variant="warning">
  182. {warnings.length} {warnings.length > 1 ? 'warnings' : 'warning'}
  183. </Badge>
  184. <TooltipContent className="p-0 divide-y max-w-xs" side="bottom">
  185. {warnings.map((warning, index) => (
  186. <p
  187. key={index}
  188. className="px-3 py-1.5 text-xs text-foreground-light text-left"
  189. >
  190. {warning.text}{' '}
  191. {warning.link && (
  192. <Link href={warning.link}>{warning.linkText || 'View'}</Link>
  193. )}
  194. </p>
  195. ))}
  196. </TooltipContent>
  197. </TooltipTrigger>
  198. </Tooltip>
  199. </div>
  200. </div>
  201. <SidePanel
  202. size="large"
  203. header={
  204. <div className="flex flex-row justify-between items-center">
  205. <h3>Field Reference</h3>
  206. <Button
  207. type="text"
  208. className="px-1"
  209. onClick={() => setShowReference(false)}
  210. icon={<X />}
  211. />
  212. </div>
  213. }
  214. visible={showReference}
  215. cancelText="Close"
  216. onCancel={() => setShowReference(false)}
  217. hideFooter
  218. triggerElement={
  219. <Button
  220. type="text"
  221. onClick={() => setShowReference(true)}
  222. icon={<BookOpen />}
  223. className="px-2"
  224. >
  225. <span>Field Reference</span>
  226. </Button>
  227. }
  228. >
  229. <SidePanel.Content>
  230. <div className="pt-4 pb-2 space-y-1">
  231. <p className="text-sm">
  232. The following table shows all the available paths that can be queried from each
  233. respective source. Do note that to access nested keys, you would need to perform
  234. the necessary{' '}
  235. <Link
  236. href={`${DOCS_URL}/guides/platform/logs#unnesting-arrays`}
  237. target="_blank"
  238. rel="noreferrer"
  239. className="text-brand"
  240. >
  241. unnesting joins
  242. <ExternalLink
  243. size="14"
  244. className="ml-1 inline translate-y-[-2px]"
  245. strokeWidth={1.5}
  246. />
  247. </Link>
  248. </p>
  249. </div>
  250. </SidePanel.Content>
  251. <SidePanel.Separator />
  252. <div className="px-4 pb-4 flex flex-col gap-4">
  253. <Popover open={open} onOpenChange={setOpen}>
  254. <PopoverTrigger asChild>
  255. <Button
  256. type="default"
  257. role="combobox"
  258. size={'small'}
  259. aria-expanded={open}
  260. className="w-full justify-between"
  261. iconRight={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
  262. >
  263. {value ? selectedSchema?.name : 'Select source...'}
  264. </Button>
  265. </PopoverTrigger>
  266. <PopoverContent className="p-0" sameWidthAsTrigger>
  267. <Command>
  268. <CommandInput placeholder="Search source..." />
  269. <CommandList>
  270. <CommandEmpty>No source found.</CommandEmpty>
  271. <CommandGroup>
  272. {logConstants.schemas.map((schema) => (
  273. <CommandItem
  274. key={schema.reference}
  275. value={schema.reference}
  276. onSelect={() => {
  277. setSelectedSchema(schema)
  278. setOpen(false)
  279. }}
  280. >
  281. <Check
  282. className={cn(
  283. 'mr-2 h-4 w-4',
  284. selectedSchema === schema ? 'opacity-100' : 'opacity-0'
  285. )}
  286. />
  287. {schema.name}
  288. </CommandItem>
  289. ))}
  290. </CommandGroup>
  291. </CommandList>
  292. </Command>
  293. </PopoverContent>
  294. </Popover>
  295. <Table
  296. head={[
  297. <Table.th className="text-xs p-2!" key="path">
  298. Path
  299. </Table.th>,
  300. <Table.th key="type" className="text-xs p-2!">
  301. Type
  302. </Table.th>,
  303. ]}
  304. body={selectedSchema.fields.map((field) => (
  305. <Field key={field.path} field={field} />
  306. ))}
  307. />
  308. </div>
  309. </SidePanel>
  310. </div>
  311. </div>
  312. </div>
  313. )
  314. }
  315. const Field = ({
  316. field,
  317. }: {
  318. field: {
  319. path: string
  320. type: string
  321. }
  322. }) => {
  323. const [isCopied, setIsCopied] = useState(false)
  324. return (
  325. <Table.tr>
  326. <Table.td
  327. className="font-mono text-xs p-2! cursor-pointer hover:text-foreground transition flex items-center space-x-2"
  328. onClick={() =>
  329. copyToClipboard(field.path, () => {
  330. setIsCopied(true)
  331. setTimeout(() => setIsCopied(false), 3000)
  332. })
  333. }
  334. >
  335. <span>{field.path}</span>
  336. {isCopied ? (
  337. <Tooltip>
  338. <TooltipTrigger>
  339. <Check size={14} strokeWidth={3} className="text-brand" />
  340. </TooltipTrigger>
  341. <TooltipContent side="bottom">Copied</TooltipContent>
  342. </Tooltip>
  343. ) : (
  344. <Tooltip>
  345. <TooltipTrigger>
  346. <Copy size={14} strokeWidth={1.5} />
  347. </TooltipTrigger>
  348. <TooltipContent side="bottom">Copy value</TooltipContent>
  349. </Tooltip>
  350. )}
  351. </Table.td>
  352. <Table.td className="font-mono text-xs p-2!">{field.type}</Table.td>
  353. </Table.tr>
  354. )
  355. }
  356. export default LogsQueryPanel