QueryBlock.tsx 13 KB

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