QueryBlock.tsx382 lines · main
1import dayjs from 'dayjs'
2import { Code, Play } from 'lucide-react'
3import { DragEvent, ReactNode, useEffect, useMemo, useRef, useState } from 'react'
4import { Bar, BarChart, CartesianGrid, Cell, Tooltip, XAxis, YAxis } from 'recharts'
5import { Badge, Button, ChartContainer, ChartTooltipContent, cn } from 'ui'
6import { CodeBlock } from 'ui-patterns/CodeBlock'
7import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
8
9import { ButtonTooltip } from '../ButtonTooltip'
10import { CHART_COLORS } from '../Charts/Charts.constants'
11import { SqlWarningAdmonition } from '../SqlWarningAdmonition'
12import { BlockViewConfiguration } from './BlockViewConfiguration'
13import { EditQueryButton } from './EditQueryButton'
14import {
15 checkHasNonPositiveValues,
16 computeYAxisWidth,
17 formatLogTick,
18 formatYAxisTick,
19 getCumulativeResults,
20} from './QueryBlock.utils'
21import { ReportBlockContainer } from '@/components/interfaces/Reports/ReportBlock/ReportBlockContainer'
22import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
23import Results from '@/components/interfaces/SQLEditor/UtilityPanel/Results'
24
25export const DEFAULT_CHART_CONFIG: ChartConfig = {
26 type: 'bar',
27 cumulative: false,
28 xKey: '',
29 yKey: '',
30 showLabels: false,
31 showGrid: false,
32 logScale: false,
33 view: 'table',
34}
35
36export interface QueryBlockProps {
37 id?: string
38 label: string
39 sql?: string
40 isWriteQuery?: boolean
41 chartConfig?: ChartConfig
42 actions?: ReactNode
43 results?: any[]
44 errorText?: string
45 isExecuting?: boolean
46 initialHideSql?: boolean
47 draggable?: boolean
48 disabled?: boolean
49 blockWriteQueries?: boolean
50 onExecute?: (queryType: 'select' | 'mutation') => void
51 onRemoveChart?: () => void
52 onUpdateChartConfig?: ({ chartConfig }: { chartConfig: Partial<ChartConfig> }) => void
53 onDragStart?: (e: DragEvent<Element>) => void
54}
55
56// [Joshen ReportsV2] JFYI we may adjust this in subsequent PRs when we implement this into Reports V2
57// First iteration here is just to make this work with the AI Assistant first
58export const QueryBlock = ({
59 id,
60 label,
61 sql,
62 chartConfig = DEFAULT_CHART_CONFIG,
63 actions,
64 results,
65 errorText,
66 isWriteQuery = false,
67 isExecuting = false,
68 initialHideSql = false,
69 draggable = false,
70 disabled = false,
71 blockWriteQueries = false,
72 onExecute,
73 onRemoveChart,
74 onUpdateChartConfig,
75 onDragStart,
76}: QueryBlockProps) => {
77 const [chartSettings, setChartSettings] = useState<ChartConfig>(chartConfig)
78 const { xKey, yKey, view = 'table', logScale = false } = chartSettings
79
80 const [showSql, setShowSql] = useState(!results && !initialHideSql)
81 const [focusDataIndex, setFocusDataIndex] = useState<number>()
82 const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>()
83
84 const prevIsWriteQuery = useRef(isWriteQuery)
85
86 useEffect(() => {
87 if (!prevIsWriteQuery.current && isWriteQuery) {
88 setShowWarning('hasWriteOperation')
89 }
90 if (!isWriteQuery && showWarning === 'hasWriteOperation') {
91 setShowWarning(undefined)
92 }
93 prevIsWriteQuery.current = isWriteQuery
94 }, [isWriteQuery, showWarning])
95
96 useEffect(() => {
97 setChartSettings(chartConfig)
98 }, [chartConfig])
99
100 const formattedQueryResult = useMemo(() => {
101 return results?.map((row) => {
102 return Object.fromEntries(
103 Object.entries(row).map(([key, value]) => {
104 if (key === yKey) return [key, Number(value)]
105 return [key, value]
106 })
107 )
108 })
109 }, [results, yKey])
110
111 const chartData = chartSettings.cumulative
112 ? getCumulativeResults({ rows: formattedQueryResult ?? [] }, chartSettings)
113 : formattedQueryResult
114
115 const hasNonPositiveValues = useMemo(() => {
116 if (!logScale || !yKey || !chartData?.length) return false
117 return checkHasNonPositiveValues(chartData, yKey)
118 }, [logScale, yKey, chartData])
119
120 const effectiveLogScale = logScale && !hasNonPositiveValues
121
122 const yAxisWidth = computeYAxisWidth(chartData ?? [], yKey ?? '', {
123 isLogScale: effectiveLogScale,
124 })
125
126 const getDateFormat = (key: any) => {
127 const value = chartData?.[0]?.[key] || ''
128 if (typeof value === 'number') return 'number'
129 if (dayjs(value).isValid()) return 'date'
130 return 'string'
131 }
132 const xKeyDateFormat = getDateFormat(xKey)
133
134 const hasResults = Array.isArray(results) && results.length > 0
135
136 const runSelect = () => {
137 if (!sql || disabled || isExecuting) return
138 if (isWriteQuery) {
139 setShowWarning('hasWriteOperation')
140 return
141 }
142 onExecute?.('select')
143 }
144
145 const runMutation = () => {
146 if (!sql || disabled || isExecuting) return
147 setShowWarning(undefined)
148 onExecute?.('mutation')
149 }
150
151 return (
152 <ReportBlockContainer
153 draggable={draggable}
154 showDragHandle={draggable}
155 onDragStart={(e: DragEvent<Element>) => onDragStart?.(e)}
156 loading={isExecuting}
157 label={label}
158 badge={isWriteQuery && <Badge variant="warning">Write</Badge>}
159 actions={
160 <>
161 {!disabled && (
162 <>
163 <ButtonTooltip
164 type="text"
165 size="tiny"
166 className="w-7 h-7"
167 icon={<Code size={14} strokeWidth={1.5} />}
168 onClick={() => setShowSql(!showSql)}
169 tooltip={{
170 content: { side: 'bottom', text: showSql ? 'Hide query' : 'Show query' },
171 }}
172 />
173 {hasResults && (
174 <BlockViewConfiguration
175 view={view}
176 isChart={view === 'chart'}
177 lockColumns={false}
178 chartConfig={chartSettings}
179 columns={Object.keys(results?.[0] ?? {})}
180 changeView={(nextView) => {
181 if (onUpdateChartConfig)
182 onUpdateChartConfig({ chartConfig: { view: nextView } })
183 setChartSettings({ ...chartSettings, view: nextView })
184 }}
185 updateChartConfig={(config) => {
186 if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: config })
187 setChartSettings(config)
188 }}
189 />
190 )}
191
192 <EditQueryButton id={id} title={label} sql={sql} />
193 <ButtonTooltip
194 type="text"
195 size="tiny"
196 className="w-7 h-7"
197 icon={<Play size={14} strokeWidth={1.5} />}
198 loading={isExecuting}
199 disabled={isExecuting || disabled || !sql}
200 onClick={runSelect}
201 tooltip={{
202 content: {
203 side: 'bottom',
204 className: 'max-w-56 text-center',
205 text: isExecuting
206 ? 'Query is running. Check the SQL Editor to manage running queries.'
207 : 'Run query',
208 },
209 }}
210 />
211 </>
212 )}
213
214 {actions}
215 </>
216 }
217 >
218 {!!showWarning && !blockWriteQueries && (
219 <SqlWarningAdmonition
220 warningType={showWarning}
221 className="border-b"
222 onCancel={() => setShowWarning(undefined)}
223 onConfirm={runMutation}
224 disabled={!sql}
225 {...(showWarning !== 'hasWriteOperation'
226 ? {
227 message: 'Run this query now and send the results to the Assistant? ',
228 subMessage:
229 'We will execute the query and provide the result rows back to the Assistant to continue the conversation.',
230 cancelLabel: 'Skip',
231 confirmLabel: 'Run & send',
232 }
233 : {})}
234 />
235 )}
236
237 {showSql && (
238 <div
239 className={cn(
240 'shrink-0 grow w-full h-full overflow-y-auto overscroll-contain max-h-[min(300px, 100%)]',
241 {
242 'border-b': results !== undefined,
243 }
244 )}
245 >
246 <CodeBlock
247 hideLineNumbers
248 wrapLines={false}
249 value={sql}
250 language="sql"
251 className={cn(
252 'max-w-none block bg-transparent! py-3! px-3.5! prose dark:prose-dark border-0 text-foreground rounded-none! w-full',
253 '[&>code]:m-0 [&>code>span]:text-foreground'
254 )}
255 />
256 </div>
257 )}
258
259 {isExecuting && !results && (
260 <div className="p-3 w-full border-t">
261 <ShimmeringLoader />
262 </div>
263 )}
264
265 {view === 'chart' && results !== undefined ? (
266 <>
267 {(results ?? []).length === 0 ? (
268 <div className="flex w-full h-full items-center justify-center py-3">
269 <p className="text-foreground-light text-xs">No results returned from query</p>
270 </div>
271 ) : !xKey || !yKey ? (
272 <div className="flex w-full h-full items-center justify-center">
273 <p className="text-foreground-light text-xs">Select columns for the X and Y axes</p>
274 </div>
275 ) : (
276 <div className="flex-1 w-full">
277 {hasNonPositiveValues && (
278 <p className="px-3 pt-1 text-xs text-foreground-light">
279 Log scale is unavailable because the data contains zero or negative values.
280 </p>
281 )}
282 <ChartContainer
283 className="aspect-auto px-3 py-2"
284 style={{ height: '230px', minHeight: '230px' }}
285 >
286 <BarChart
287 accessibilityLayer
288 margin={{ left: 0, right: 0, top: 10 }}
289 data={chartData}
290 onMouseMove={(e: any) => {
291 if (e.activeTooltipIndex !== focusDataIndex) {
292 setFocusDataIndex(e.activeTooltipIndex)
293 }
294 }}
295 onMouseLeave={() => setFocusDataIndex(undefined)}
296 >
297 <CartesianGrid vertical={false} stroke={CHART_COLORS.AXIS} />
298 <XAxis
299 dataKey={xKey}
300 tickLine={{ stroke: CHART_COLORS.AXIS }}
301 axisLine={{ stroke: CHART_COLORS.AXIS }}
302 interval="preserveStartEnd"
303 tickMargin={4}
304 minTickGap={32}
305 tickFormatter={(value) =>
306 xKeyDateFormat === 'date' ? dayjs(value).format('MMM D YYYY HH:mm') : value
307 }
308 />
309 <YAxis
310 tickLine={false}
311 axisLine={false}
312 tickMargin={4}
313 scale={effectiveLogScale ? 'log' : 'auto'}
314 domain={effectiveLogScale ? [1, 'auto'] : undefined}
315 allowDataOverflow={effectiveLogScale}
316 width={yAxisWidth}
317 tickFormatter={effectiveLogScale ? formatLogTick : formatYAxisTick}
318 />
319 <Tooltip
320 content={
321 <ChartTooltipContent
322 className="min-w-[200px]"
323 labelFormatter={(value) =>
324 xKeyDateFormat === 'date'
325 ? dayjs(value).format('MMM D YYYY HH:mm')
326 : String(value)
327 }
328 />
329 }
330 />
331 <Bar radius={1} dataKey={yKey} fill="hsl(var(--chart-1))">
332 {chartData?.map((_: any, index: number) => (
333 <Cell
334 key={`cell-${index}`}
335 className="transition-all duration-100"
336 fill="hsl(var(--chart-1))"
337 opacity={focusDataIndex === undefined || focusDataIndex === index ? 1 : 0.4}
338 enableBackground={12}
339 />
340 ))}
341 </Bar>
342 </BarChart>
343 </ChartContainer>
344 </div>
345 )}
346 </>
347 ) : (
348 <>
349 {isWriteQuery && blockWriteQueries ? (
350 <div className="flex flex-col h-full justify-center items-center text-center">
351 <p className="text-xs text-foreground-light">
352 SQL query is not read-only and cannot be rendered
353 </p>
354 <p className="text-xs text-foreground-lighter text-center">
355 Queries that involve any mutation will not be run in reports
356 </p>
357 {!!onRemoveChart && (
358 <Button type="default" className="mt-2" onClick={() => onRemoveChart()}>
359 Remove chart
360 </Button>
361 )}
362 </div>
363 ) : !isExecuting && !!errorText ? (
364 <div className={cn('flex-1 w-full overflow-auto relative border-t px-3.5 py-2')}>
365 <span className="font-mono text-xs">ERROR: {errorText}</span>
366 </div>
367 ) : (
368 results && (
369 <div
370 className={cn(
371 'flex flex-col flex-1 w-full overflow-auto overscroll-contain relative max-h-64'
372 )}
373 >
374 <Results rows={results} />
375 </div>
376 )
377 )}
378 </>
379 )}
380 </ReportBlockContainer>
381 )
382}