diff --git a/apps/web/src/app/test/chart-playground/page.tsx b/apps/web/src/app/test/chart-playground/page.tsx new file mode 100644 index 000000000..c6c24d1f1 --- /dev/null +++ b/apps/web/src/app/test/chart-playground/page.tsx @@ -0,0 +1,260 @@ +'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'; +import { AppSplitter } from '@/components/ui/layouts/AppSplitter'; + +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(initfile); + const [dataResponse, setDataResponse] = useState(null); + const [dataSourceId, setDataSourceId] = useState(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(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 + } + ); + + // Define the left panel content (code editor and controls) + const leftPanelContent = ( +
+
+ +
+
+
+ + setDataSourceId(e.target.value)} /> +
+ +
+
+ ); + + // Define the right panel content (chart preview) + const rightPanelContent = ( +
+ {/* Header */} +
+

Chart Preview

+
+ + {/* Content Area */} +
+ {/* Error States */} + {chartConfigError && ( +
+
+
+

Chart Configuration Error

+
+
+              {JSON.stringify(chartConfigError || {}, null, 2)}
+            
+
+ )} + + {runSQLError && ( +
+
+
+

SQL Execution Error

+
+
+              {JSON.stringify(runSQLError || {}, null, 2)}
+            
+
+ )} + + {/* Loading State */} + {isRunningSQL && ( +
+
+
+
+

Executing SQL Query

+

+ Please wait while we process your query... +

+
+
+
+ )} + + {/* Checklist */} + {!(chartConfig && data && hasRunSQL && dataSourceId) && ( +
+
+
+

Setup Checklist

+
+
    +
  • +
    + + Chart configuration is {chartConfig ? 'ready' : 'missing'} + +
  • +
  • +
    0 ? 'bg-green-400' : 'bg-yellow-300'}`}>
    + 0 ? 'text-green-700 line-through' : 'text-amber-700'}`}> + Data is {data.length > 0 ? 'available' : 'not available'} + +
  • +
  • +
    + + SQL has {hasRunSQL ? 'been executed' : 'not been run'} + +
  • +
  • +
    + + Data Source ID is {dataSourceId ? 'set' : 'not set'} + +
  • +
+
+ )} + + {/* Chart Display */} + {chartConfig && data && hasRunSQL && dataSourceId && ( +
+
+
+

Chart Ready

+
+
+ +
+
+ )} +
+
+ ); + + return ( +
+ +
+ ); +} + +const DEFAULT_LAYOUT = ['40%', 'auto']; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1489591d..5be6ac1a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -486,8 +486,8 @@ importers: specifier: ^49.0.15 version: 49.0.15(@types/react@18.3.23)(class-variance-authority@0.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwind-merge@3.3.1) '@uploadthing/react': - specifier: ^7.3.1 - version: 7.3.1(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react@18.3.1)(uploadthing@7.7.2(express@4.21.2)(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(tailwindcss@4.1.11)) + specifier: ^7.3.2 + version: 7.3.2(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react@18.3.1)(uploadthing@7.7.2(express@4.21.2)(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(tailwindcss@4.1.11)) ai: specifier: ^4.3.19 version: 4.3.19(react@18.3.1)(zod@3.25.1) @@ -534,8 +534,8 @@ importers: specifier: ^11.1.0 version: 11.1.0 framer-motion: - specifier: ^12.23.11 - version: 12.23.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^12.23.12 + version: 12.23.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) hono: specifier: 'catalog:' version: 4.8.4 @@ -591,8 +591,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 posthog-js: - specifier: ^1.258.2 - version: 1.258.2 + specifier: ^1.258.3 + version: 1.258.3 react: specifier: ^18.3.1 version: 18.3.1 @@ -6136,8 +6136,8 @@ packages: '@uploadthing/mime-types@0.3.5': resolution: {integrity: sha512-iYOmod80XXOSe4NVvaUG9FsS91YGPUaJMTBj52Nwu0G2aTzEN6Xcl0mG1rWqXJ4NUH8MzjVqg+tQND5TPkJWhg==} - '@uploadthing/react@7.3.1': - resolution: {integrity: sha512-yIAFw46ZO/NPb74zpomwn6Hf2ZX/Ws+vNlR4oKNLJ7YtJ+/bqERclzC3xnRVi/pT47ctISlqXQFGiXUn85wg5Q==} + '@uploadthing/react@7.3.2': + resolution: {integrity: sha512-dssVzrxGBKBHUzJu/CiEGc3hQ49U4sPdnqN5xMmtEdnJP+6OB+a8JUjkwxiJDBQXc3ft9XCmkLCZZoUt1EQSIw==} peerDependencies: next: '*' react: ^17.0.2 || ^18.0.0 || ^19.0.0 @@ -6149,6 +6149,9 @@ packages: '@uploadthing/shared@7.1.8': resolution: {integrity: sha512-OA9ZrTfILOCt1G93wOD7dZmS653z99Nr3isZpIxzBO3y4B2geKFmPjJUZClig2RrAWLKr2VUYToXKfd9D/wP9w==} + '@uploadthing/shared@7.1.9': + resolution: {integrity: sha512-5Gn1wGVSygsBxI6tjOwwEQt/U4m+vbmZCnsuf8pDfZ+MiXe3el03CWMmpbH3KtSu0BwG48wyCKNfHplZsphvOA==} + '@vercel/edge@1.2.2': resolution: {integrity: sha512-1+y+f6rk0Yc9ss9bRDgz/gdpLimwoRteKHhrcgHvEpjbP1nyT3ByqEMWm2BTcpIO5UtDmIFXc8zdq4LR190PDA==} @@ -8261,8 +8264,8 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - framer-motion@12.23.11: - resolution: {integrity: sha512-VzNi+exyI3bn7Pzvz1Fjap1VO9gQu8mxrsSsNamMidsZ8AA8W2kQsR+YQOciEUbMtkKAWIbPHPttfn5e9jqqJQ==} + framer-motion@12.23.12: + resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -9849,8 +9852,8 @@ packages: peerDependencies: monaco-editor: '>=0.36' - motion-dom@12.23.9: - resolution: {integrity: sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A==} + motion-dom@12.23.12: + resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==} motion-utils@12.23.6: resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} @@ -10536,8 +10539,8 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} - posthog-js@1.258.2: - resolution: {integrity: sha512-XBSeiN4HjiYsy3tW5zss8WOJF2JXTQXAYw2wZ+zjqQuzzi7kkLEXjIgsVrBnt5Opwhqn0krZVsb0ZBw34dIiyQ==} + posthog-js@1.258.3: + resolution: {integrity: sha512-bX4Ehzo/yBGY7o23CUAQelX+oUdLn5bKhEjTSiveXsjkZhRURxfKDinYTwI6tjs19FC8BWfHFyO3q3TgT2E7CA==} peerDependencies: '@rrweb/types': 2.0.0-alpha.17 rrweb-snapshot: 2.0.0-alpha.17 @@ -19170,9 +19173,9 @@ snapshots: '@uploadthing/mime-types@0.3.5': {} - '@uploadthing/react@7.3.1(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react@18.3.1)(uploadthing@7.7.2(express@4.21.2)(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(tailwindcss@4.1.11))': + '@uploadthing/react@7.3.2(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react@18.3.1)(uploadthing@7.7.2(express@4.21.2)(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(tailwindcss@4.1.11))': dependencies: - '@uploadthing/shared': 7.1.8 + '@uploadthing/shared': 7.1.9 file-selector: 0.6.0 react: 18.3.1 uploadthing: 7.7.2(express@4.21.2)(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(tailwindcss@4.1.11) @@ -19185,6 +19188,12 @@ snapshots: effect: 3.14.21 sqids: 0.3.0 + '@uploadthing/shared@7.1.9': + dependencies: + '@uploadthing/mime-types': 0.3.5 + effect: 3.16.8 + sqids: 0.3.0 + '@vercel/edge@1.2.2': {} '@vercel/functions@1.6.0(@aws-sdk/credential-provider-web-identity@3.840.0)': @@ -20879,7 +20888,6 @@ snapshots: dependencies: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 - optional: true electron-to-chromium@1.5.179: {} @@ -21701,9 +21709,9 @@ snapshots: forwarded@0.2.0: {} - framer-motion@12.23.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + framer-motion@12.23.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - motion-dom: 12.23.9 + motion-dom: 12.23.12 motion-utils: 12.23.6 tslib: 2.8.1 optionalDependencies: @@ -23615,7 +23623,7 @@ snapshots: vscode-uri: 3.1.0 yaml: 2.8.0 - motion-dom@12.23.9: + motion-dom@12.23.12: dependencies: motion-utils: 12.23.6 @@ -24431,7 +24439,7 @@ snapshots: postgres@3.4.7: {} - posthog-js@1.258.2: + posthog-js@1.258.3: dependencies: core-js: 3.44.0 fflate: 0.4.8