2025-07-31 01:38:19 +08:00
'use client' ;
import { useMemo , useState } from 'react' ;
import { useHotkeys } from 'react-hotkeys-hook' ;
import { AppCodeEditor } from '@/components/ui/inputs/AppCodeEditor' ;
import { yamlToJson } from '@/lib/yaml-to-json' ;
import { useRunSQL } from '@/api/buster_rest/sql/queryRequests' ;
import { BusterChart } from '@/components/ui/charts/BusterChart' ;
import {
ChartConfigPropsSchema ,
type ChartConfigProps ,
type DataResult
} from '@buster/server-shared/metrics' ;
import type { ZodError } from 'zod' ;
import type { RunSQLResponse } from '@/api/asset_interfaces' ;
import { useMemoizedFn } from '@/hooks' ;
import { Button } from '@/components/ui/buttons' ;
import { Input } from '@/components/ui/inputs' ;
2025-07-31 01:43:33 +08:00
import { AppSplitter } from '@/components/ui/layouts/AppSplitter' ;
2025-07-31 01:38:19 +08:00
type YamlifiedConfig = {
sql : string ;
chartConfig : ChartConfigProps ;
} ;
const initfile = ` name: Top 10 Customers by Lifetime Value \ ndescription: Shows the customers who have generated the highest total revenue over their entire relationship with the company \ ntimeFrame: All time \ nsql: \ "SELECT \\ n CONCAT(p.firstname, ' ', p.lastname) AS customer_name, \\ n clv.metric_clv_all_time::numeric AS lifetime_value \\ nFROM postgres.ont_ont.customer_all_time_clv clv \\ nJOIN postgres.ont_ont.customer c ON clv.customerid = c.customerid \\ nLEFT JOIN postgres.ont_ont.person p ON c.personid = p.businessentityid \\ nWHERE p.firstname IS NOT NULL AND p.lastname IS NOT NULL \\ nORDER BY clv.metric_clv_all_time::numeric DESC \\ nLIMIT 10 \\ n \ " \ nchartConfig: \ n selectedChartType: bar \ n columnLabelFormats: \ n customer_name: \ n columnType: string \ n style: string \ n numberSeparatorStyle: null \ n replaceMissingDataWith: null \ n lifetime_value: \ n columnType: number \ n style: currency \ n numberSeparatorStyle: ',' \ n minimumFractionDigits: 2 \ n maximumFractionDigits: 2 \ n replaceMissingDataWith: 0 \ n currency: USD \ n barAndLineAxis: \ n x: \ n - customer_name \ n y: \ n - lifetime_value \ n barLayout: horizontal \ n ` ;
const initDataSourceId = 'cc3ef3bc-44ec-4a43-8dc4-681cae5c996a' ;
export default function ChartPlayground() {
// State management
const [ config , setConfig ] = useState < string > ( initfile ) ;
const [ dataResponse , setDataResponse ] = useState < RunSQLResponse | null > ( null ) ;
const [ dataSourceId , setDataSourceId ] = useState < string > ( initDataSourceId ) ;
// SQL mutation hook
const {
mutateAsync : runSQLMutation ,
error : runSQLError ,
isPending : isRunningSQL ,
isSuccess : hasRunSQL ,
reset : resetRunSQL
} = useRunSQL ( ) ;
// Parse YAML config
const yamlifiedConfig = useMemo ( ( ) = > {
if ( ! config . trim ( ) ) return null ;
try {
return yamlToJson < YamlifiedConfig > ( config ) ;
} catch ( error ) {
return null ;
}
} , [ config ] ) ;
// Parse and validate chart configuration
const chartConfigParsed = useMemo ( ( ) = > {
if ( ! yamlifiedConfig ? . chartConfig ) {
return { data : null , error : null } ;
}
const parsed = ChartConfigPropsSchema . safeParse ( yamlifiedConfig . chartConfig ) ;
return {
data : parsed.success ? parsed.data : null ,
error : parsed.success ? null : ( parsed . error as ZodError )
} ;
} , [ yamlifiedConfig ] ) ;
// Derived values
const chartConfig = chartConfigParsed . data ;
const chartConfigError = chartConfigParsed . error ;
const data : DataResult = dataResponse ? . data || [ ] ;
const columnMetadata = dataResponse ? . data_metadata ? . column_metadata || [ ] ;
const hasSQL = ! ! yamlifiedConfig ? . sql ;
const hasDataSourceId = ! ! dataSourceId . trim ( ) ;
// SQL execution handler
const runSQL = useMemoizedFn ( async ( ) = > {
if ( ! yamlifiedConfig ? . sql || ! hasDataSourceId ) {
return ;
}
try {
const res = await runSQLMutation ( {
sql : yamlifiedConfig.sql ,
data_source_id : dataSourceId
} ) ;
setDataResponse ( res ) ;
} catch ( error ) {
// Error is handled by the hook
}
} ) ;
// Status checks
const isReadyToRun = hasSQL && hasDataSourceId ;
const isReadyToChart = chartConfig && data . length > 0 && hasRunSQL ;
// Setup hotkey for running SQL (meta+enter)
useHotkeys (
'meta+enter' ,
( event ) = > {
event . preventDefault ( ) ;
if ( isReadyToRun && ! isRunningSQL ) {
runSQL ( ) ;
}
} ,
{
enabled : isReadyToRun && ! isRunningSQL ,
enableOnContentEditable : true ,
enableOnFormTags : true
}
) ;
2025-07-31 01:43:33 +08:00
// Define the left panel content (code editor and controls)
const leftPanelContent = (
< div className = "bg-background flex h-full w-full flex-col space-y-4 overflow-hidden rounded-lg border p-4" >
< div className = "h-full w-full overflow-hidden" >
< AppCodeEditor value = { config } onChange = { setConfig } / >
< / div >
< div className = "flex flex-col space-y-2" >
< div className = "flex items-center space-x-1" >
< label className = "w-fit min-w-28 text-sm font-medium" > Data Source ID < / label >
< Input value = { dataSourceId || '' } onChange = { ( e ) = > setDataSourceId ( e . target . value ) } / >
2025-07-31 01:38:19 +08:00
< / div >
2025-07-31 01:43:33 +08:00
< Button block onClick = { runSQL } loading = { isRunningSQL } disabled = { ! hasSQL } >
< div className = "flex items-center justify-center gap-2" >
< span > Run SQL < / span >
< span className = "rounded bg-black/10 px-1.5 py-0.5 text-xs opacity-70" > ⌘ ↵ < / span >
2025-07-31 01:38:19 +08:00
< / div >
2025-07-31 01:43:33 +08:00
< / Button >
2025-07-31 01:38:19 +08:00
< / div >
2025-07-31 01:43:33 +08:00
< / div >
) ;
2025-07-31 01:38:19 +08:00
2025-07-31 01:43:33 +08:00
// Define the right panel content (chart preview)
const rightPanelContent = (
< div className = "bg-background flex h-full flex-col overflow-hidden rounded-lg border shadow-sm" >
{ /* Header */ }
< div className = "bg-muted/30 border-b px-6 py-4" >
< h3 className = "text-foreground text-lg font-semibold" > Chart Preview < / h3 >
< / div >
2025-07-31 01:38:19 +08:00
2025-07-31 01:43:33 +08:00
{ /* Content Area */ }
< div className = "flex-1 space-y-6 overflow-y-auto p-6" >
{ /* Error States */ }
{ chartConfigError && (
< div className = "rounded-md border border-red-200 bg-red-50 p-4" >
< div className = "mb-3 flex items-center space-x-2" >
< div className = "h-4 w-4 flex-shrink-0 rounded-full bg-red-400" > < / div >
< h4 className = "text-sm font-semibold text-red-800" > Chart Configuration Error < / h4 >
2025-07-31 01:38:19 +08:00
< / div >
2025-07-31 01:43:33 +08:00
< pre className = "overflow-x-auto rounded bg-red-100 p-3 text-xs whitespace-pre-wrap text-red-700" >
{ JSON . stringify ( chartConfigError || { } , null , 2 ) }
< / pre >
< / div >
) }
2025-07-31 01:38:19 +08:00
2025-07-31 01:43:33 +08:00
{ runSQLError && (
< div className = "rounded-md border border-red-200 bg-red-50 p-4" >
< div className = "mb-3 flex items-center space-x-2" >
< div className = "h-4 w-4 flex-shrink-0 rounded-full bg-red-400" > < / div >
< h4 className = "text-sm font-semibold text-red-800" > SQL Execution Error < / h4 >
2025-07-31 01:38:19 +08:00
< / div >
2025-07-31 01:43:33 +08:00
< pre className = "overflow-x-auto rounded bg-red-100 p-3 text-xs whitespace-pre-wrap text-red-700" >
{ JSON . stringify ( runSQLError || { } , null , 2 ) }
< / pre >
< / div >
) }
{ /* Loading State */ }
{ isRunningSQL && (
< div className = "rounded-md border border-blue-200 bg-blue-50 p-4" >
< div className = "flex items-center space-x-3" >
< div className = "h-5 w-5 flex-shrink-0 animate-spin rounded-full border-2 border-blue-400 border-t-transparent" > < / div >
< div >
< h4 className = "text-sm font-semibold text-blue-800" > Executing SQL Query < / h4 >
< p className = "mt-1 text-xs text-blue-600" >
Please wait while we process your query . . .
< / p >
2025-07-31 01:38:19 +08:00
< / div >
< / div >
2025-07-31 01:43:33 +08:00
< / div >
) }
{ /* Checklist */ }
{ ! ( chartConfig && data && hasRunSQL && dataSourceId ) && (
< div className = "rounded-md border border-amber-200 bg-amber-50 p-4" >
< div className = "mb-4 flex items-center space-x-2" >
< div className = "h-4 w-4 flex-shrink-0 rounded-full bg-amber-400" > < / div >
< h4 className = "text-sm font-semibold text-amber-800" > Setup Checklist < / h4 >
2025-07-31 01:38:19 +08:00
< / div >
2025-07-31 01:43:33 +08:00
< ul className = "space-y-2" >
< li className = "flex items-center space-x-3" >
< div
className = { ` h-2 w-2 flex-shrink-0 rounded-full ${ chartConfig ? 'bg-green-400' : 'bg-yellow-300' } ` } > < / div >
< span
className = { ` text-sm ${ chartConfig ? 'text-green-700 line-through' : 'text-amber-700' } ` } >
Chart configuration is { chartConfig ? 'ready' : 'missing' }
< / span >
< / li >
< li className = "flex items-center space-x-3" >
< div
className = { ` h-2 w-2 flex-shrink-0 rounded-full ${ data . length > 0 ? 'bg-green-400' : 'bg-yellow-300' } ` } > < / div >
< span
className = { ` text-sm ${ data . length > 0 ? 'text-green-700 line-through' : 'text-amber-700' } ` } >
Data is { data . length > 0 ? 'available' : 'not available' }
< / span >
< / li >
< li className = "flex items-center space-x-3" >
< div
className = { ` h-2 w-2 flex-shrink-0 rounded-full ${ hasRunSQL ? 'bg-green-400' : 'bg-yellow-300' } ` } > < / div >
< span
className = { ` text-sm ${ hasRunSQL ? 'text-green-700 line-through' : 'text-amber-700' } ` } >
SQL has { hasRunSQL ? 'been executed' : 'not been run' }
< / span >
< / li >
< li className = "flex items-center space-x-3" >
< div
className = { ` h-2 w-2 flex-shrink-0 rounded-full ${ dataSourceId ? 'bg-green-400' : 'bg-yellow-300' } ` } > < / div >
< span
className = { ` text-sm ${ dataSourceId ? 'text-green-700 line-through' : 'text-amber-700' } ` } >
Data Source ID is { dataSourceId ? 'set' : 'not set' }
< / span >
< / li >
< / ul >
< / div >
) }
{ /* Chart Display */ }
{ chartConfig && data && hasRunSQL && dataSourceId && (
< div className = "rounded-md border bg-gradient-to-br from-green-50 to-blue-50 p-6" >
< div className = "mb-4 flex items-center space-x-2" >
< div className = "h-3 w-3 flex-shrink-0 animate-pulse rounded-full bg-green-500" > < / div >
< h4 className = "text-sm font-semibold text-green-800" > Chart Ready < / h4 >
2025-07-31 01:38:19 +08:00
< / div >
2025-07-31 01:43:33 +08:00
< div className = "rounded-lg bg-white p-4 shadow-sm" >
< BusterChart { ...chartConfig } data = { data } columnMetadata = { columnMetadata } / >
< / div >
< / div >
) }
2025-07-31 01:38:19 +08:00
< / div >
< / div >
) ;
2025-07-31 01:43:33 +08:00
return (
< div className = "h-screen w-screen p-4" >
< AppSplitter
autoSaveId = "chart-playground"
splitterClassName = "mx-2"
defaultLayout = { DEFAULT_LAYOUT }
className = "h-full w-full"
leftPanelMinSize = "200px"
rightPanelMinSize = "200px"
leftChildren = { leftPanelContent }
rightChildren = { rightPanelContent }
preserveSide = "left"
/ >
< / div >
) ;
2025-07-31 01:38:19 +08:00
}
2025-07-31 01:43:33 +08:00
const DEFAULT_LAYOUT = [ '40%' , 'auto' ] ;