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