@libredb/studio 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (572) hide show
  1. package/.claude/settings.local.json +127 -0
  2. package/.cursorrules +426 -0
  3. package/.devin/wiki.json +143 -0
  4. package/.dockerignore +80 -0
  5. package/.env.example +159 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  8. package/.github/PULL_REQUEST_TEMPLATE.md +57 -0
  9. package/.github/workflows/ci.yml +185 -0
  10. package/.github/workflows/codeql.yml +57 -0
  11. package/.github/workflows/docker-build-push.yml +118 -0
  12. package/.github/workflows/helm-release.yml +113 -0
  13. package/CLAUDE.md +265 -0
  14. package/CODE_OF_CONDUCT.md +124 -0
  15. package/CONTRIBUTING.md +154 -0
  16. package/Dockerfile +73 -0
  17. package/LICENSE +21 -0
  18. package/README.md +614 -0
  19. package/SECURITY.md +107 -0
  20. package/artifacthub-repo.yml +4 -0
  21. package/bun.lock +1714 -0
  22. package/bunfig.toml +3 -0
  23. package/charts/libredb-studio/.helmignore +11 -0
  24. package/charts/libredb-studio/Chart.lock +6 -0
  25. package/charts/libredb-studio/Chart.yaml +50 -0
  26. package/charts/libredb-studio/README.md +206 -0
  27. package/charts/libredb-studio/templates/NOTES.txt +59 -0
  28. package/charts/libredb-studio/templates/_helpers.tpl +135 -0
  29. package/charts/libredb-studio/templates/configmap.yaml +37 -0
  30. package/charts/libredb-studio/templates/deployment.yaml +184 -0
  31. package/charts/libredb-studio/templates/hpa.yaml +32 -0
  32. package/charts/libredb-studio/templates/ingress.yaml +41 -0
  33. package/charts/libredb-studio/templates/networkpolicy.yaml +50 -0
  34. package/charts/libredb-studio/templates/pdb.yaml +18 -0
  35. package/charts/libredb-studio/templates/pvc.yaml +23 -0
  36. package/charts/libredb-studio/templates/secret.yaml +30 -0
  37. package/charts/libredb-studio/templates/seed-configmap.yaml +11 -0
  38. package/charts/libredb-studio/templates/service.yaml +22 -0
  39. package/charts/libredb-studio/templates/serviceaccount.yaml +13 -0
  40. package/charts/libredb-studio/values.schema.json +246 -0
  41. package/charts/libredb-studio/values.yaml +286 -0
  42. package/components.json +22 -0
  43. package/conductor/code_styleguides/typescript.md +43 -0
  44. package/conductor/product-guidelines.md +43 -0
  45. package/conductor/product.md +3 -0
  46. package/conductor/setup_state.json +1 -0
  47. package/conductor/tech-stack.md +39 -0
  48. package/conductor/tracks/enhance_postgres_monitoring_20251227/metadata.json +8 -0
  49. package/conductor/tracks/enhance_postgres_monitoring_20251227/plan.md +44 -0
  50. package/conductor/tracks/enhance_postgres_monitoring_20251227/spec.md +31 -0
  51. package/conductor/tracks.md +8 -0
  52. package/conductor/workflow.md +333 -0
  53. package/database-compose.yml +55 -0
  54. package/docker/postgres-init/01-extensions.sql +10 -0
  55. package/docker/postgres-init/02-sample-data.sql +585 -0
  56. package/docker/postgres.yml +68 -0
  57. package/docker-compose.yml +38 -0
  58. package/docs/AI_PLAN.md +74 -0
  59. package/docs/API_DOCS.md +875 -0
  60. package/docs/ARCHITECTURE.md +218 -0
  61. package/docs/DATABASE_PROVIDERS.md +358 -0
  62. package/docs/FEATURES.md +116 -0
  63. package/docs/HELM_CHART.md +252 -0
  64. package/docs/LOGIN_PAGE.md +178 -0
  65. package/docs/MONACO_EDITOR_PERFORMANCE.md +315 -0
  66. package/docs/OIDC_ARCH.md +681 -0
  67. package/docs/OIDC_SETUP.md +322 -0
  68. package/docs/POSTGRES_METRICS.md +516 -0
  69. package/docs/QUERY_OPTIMIZATION.md +370 -0
  70. package/docs/SEED_CONNECTIONS.md +468 -0
  71. package/docs/SQL_ALIAS_COMPLETION.md +190 -0
  72. package/docs/STORAGE_ARCHITECTURE.md +565 -0
  73. package/docs/STORAGE_QUICK_SETUP.md +419 -0
  74. package/docs/TECHNICAL_PLAN.md +36 -0
  75. package/docs/THEMING.md +345 -0
  76. package/docs/adding-a-new-database-provider.md +642 -0
  77. package/docs/backlogs/000-PLATFORM_DATA_SYNC_DATABASE.md +360 -0
  78. package/docs/backlogs/001-INLINE_DATA_EDITING.md +118 -0
  79. package/docs/backlogs/002-DATA_IMPORT.md +215 -0
  80. package/docs/backlogs/003-QUERY_TIME_MACHINE.md +183 -0
  81. package/docs/backlogs/004-AI_DATA_STORYTELLER.md +292 -0
  82. package/docs/backlogs/005-QUERY_PLAYGROUND.md +352 -0
  83. package/docs/backlogs/006-DATA_MASKING.md +418 -0
  84. package/docs/enterprise-features.md +718 -0
  85. package/docs/kubernetes-helm-chart-artifacthub-plan.md +803 -0
  86. package/docs/medium-koyeb-article-en.md +215 -0
  87. package/docs/plans/test-plans.md +445 -0
  88. package/docs/releases/RELEASE.V0.3.0.md +22 -0
  89. package/docs/releases/RELEASE.V0.4.0.md +154 -0
  90. package/docs/releases/RELEASE.V0.5.0.md +252 -0
  91. package/docs/releases/RELEASE_v0.5.6.md +145 -0
  92. package/docs/releases/RELEASE_v0.6.1.md +303 -0
  93. package/docs/releases/RELEASE_v0.6.7.md +292 -0
  94. package/docs/releases/RELEASE_v0.7.0.md +332 -0
  95. package/docs/releases/RELEASE_v0.8.0.md +521 -0
  96. package/docs/sampledb/titanic.sql +1379 -0
  97. package/docs/superpowers/plans/2026-03-25-seed-connections.md +1362 -0
  98. package/docs/superpowers/specs/2026-03-25-seed-connections-design.md +590 -0
  99. package/e2e/admin-dashboard.spec.ts +64 -0
  100. package/e2e/connection-management.spec.ts +58 -0
  101. package/e2e/export.spec.ts +34 -0
  102. package/e2e/login.spec.ts +85 -0
  103. package/e2e/query-execution.spec.ts +35 -0
  104. package/e2e/tab-management.spec.ts +64 -0
  105. package/eslint.config.mjs +28 -0
  106. package/fly.toml +43 -0
  107. package/next.config.ts +32 -0
  108. package/package.json +130 -0
  109. package/playwright.config.ts +34 -0
  110. package/postcss.config.mjs +7 -0
  111. package/public/favicon-32x32.png +0 -0
  112. package/public/favicon.ico +0 -0
  113. package/public/file.svg +1 -0
  114. package/public/globe.svg +1 -0
  115. package/public/logo.svg +32 -0
  116. package/public/next.svg +1 -0
  117. package/public/screenshots/code-generator.png +0 -0
  118. package/public/screenshots/connection-modal.png +0 -0
  119. package/public/screenshots/data-profiler.png +0 -0
  120. package/public/screenshots/erd-diagram.png +0 -0
  121. package/public/screenshots/hero-editor.png +0 -0
  122. package/public/screenshots/nl2sql.png +0 -0
  123. package/public/vercel.svg +1 -0
  124. package/public/window.svg +1 -0
  125. package/render.yaml +58 -0
  126. package/scripts/merge-lcov.mjs +239 -0
  127. package/sonar-project.properties +16 -0
  128. package/src/app/admin/error.tsx +46 -0
  129. package/src/app/admin/page.tsx +10 -0
  130. package/src/app/api/admin/audit/route.ts +52 -0
  131. package/src/app/api/admin/fleet-health/route.ts +81 -0
  132. package/src/app/api/ai/autopilot/route.ts +105 -0
  133. package/src/app/api/ai/chat/route.ts +132 -0
  134. package/src/app/api/ai/describe-schema/route.ts +52 -0
  135. package/src/app/api/ai/explain/route.ts +86 -0
  136. package/src/app/api/ai/impact/route.ts +97 -0
  137. package/src/app/api/ai/index-advisor/route.ts +98 -0
  138. package/src/app/api/ai/nl2sql/route.ts +87 -0
  139. package/src/app/api/ai/query-safety/route.ts +87 -0
  140. package/src/app/api/auth/login/route.ts +62 -0
  141. package/src/app/api/auth/logout/route.ts +25 -0
  142. package/src/app/api/auth/me/route.ts +10 -0
  143. package/src/app/api/auth/oidc/callback/route.ts +82 -0
  144. package/src/app/api/auth/oidc/login/route.ts +43 -0
  145. package/src/app/api/connections/managed/route.ts +35 -0
  146. package/src/app/api/db/cancel/route.ts +42 -0
  147. package/src/app/api/db/disconnect/route.ts +28 -0
  148. package/src/app/api/db/health/route.ts +49 -0
  149. package/src/app/api/db/maintenance/route.ts +72 -0
  150. package/src/app/api/db/monitoring/route.ts +62 -0
  151. package/src/app/api/db/multi-query/route.ts +116 -0
  152. package/src/app/api/db/pool-stats/route.ts +37 -0
  153. package/src/app/api/db/profile/route.ts +144 -0
  154. package/src/app/api/db/provider-meta/route.ts +49 -0
  155. package/src/app/api/db/query/route.ts +50 -0
  156. package/src/app/api/db/schema/route.ts +47 -0
  157. package/src/app/api/db/schema-snapshot/route.ts +42 -0
  158. package/src/app/api/db/test-connection/route.ts +55 -0
  159. package/src/app/api/db/transaction/route.ts +111 -0
  160. package/src/app/api/storage/[collection]/route.ts +67 -0
  161. package/src/app/api/storage/config/route.ts +17 -0
  162. package/src/app/api/storage/migrate/route.ts +45 -0
  163. package/src/app/api/storage/route.ts +32 -0
  164. package/src/app/error.tsx +49 -0
  165. package/src/app/global-error.tsx +55 -0
  166. package/src/app/globals.css +146 -0
  167. package/src/app/icon.svg +42 -0
  168. package/src/app/layout.tsx +34 -0
  169. package/src/app/login/login-form.tsx +301 -0
  170. package/src/app/login/page.tsx +11 -0
  171. package/src/app/monitoring/page.tsx +8 -0
  172. package/src/app/not-found.tsx +29 -0
  173. package/src/app/page.tsx +5 -0
  174. package/src/components/AIAutopilotPanel.tsx +238 -0
  175. package/src/components/CodeGenerator.tsx +271 -0
  176. package/src/components/CommandPalette.tsx +227 -0
  177. package/src/components/ConnectionModal.tsx +759 -0
  178. package/src/components/CreateTableModal.tsx +281 -0
  179. package/src/components/DataCharts.tsx +962 -0
  180. package/src/components/DataImportModal.tsx +582 -0
  181. package/src/components/DataProfiler.tsx +335 -0
  182. package/src/components/DatabaseDocs.tsx +251 -0
  183. package/src/components/MaskingSettings.tsx +414 -0
  184. package/src/components/MobileNav.tsx +50 -0
  185. package/src/components/NL2SQLPanel.tsx +281 -0
  186. package/src/components/PivotTable.tsx +257 -0
  187. package/src/components/QueryEditor.tsx +760 -0
  188. package/src/components/QueryHistory.tsx +344 -0
  189. package/src/components/QuerySafetyDialog.tsx +290 -0
  190. package/src/components/ResultsGrid.tsx +644 -0
  191. package/src/components/SaveQueryModal.tsx +104 -0
  192. package/src/components/SavedQueries.tsx +128 -0
  193. package/src/components/SchemaDiagram.tsx +473 -0
  194. package/src/components/SchemaDiff.tsx +473 -0
  195. package/src/components/SnapshotTimeline.tsx +116 -0
  196. package/src/components/Studio.tsx +639 -0
  197. package/src/components/TestDataGenerator.tsx +261 -0
  198. package/src/components/VisualExplain.tsx +820 -0
  199. package/src/components/admin/AdminDashboard.tsx +163 -0
  200. package/src/components/admin/tabs/AuditTab.tsx +531 -0
  201. package/src/components/admin/tabs/MonitoringEmbed.tsx +11 -0
  202. package/src/components/admin/tabs/OperationsTab.tsx +646 -0
  203. package/src/components/admin/tabs/OverviewTab.tsx +1328 -0
  204. package/src/components/admin/tabs/SecurityTab.tsx +284 -0
  205. package/src/components/community-section.tsx +92 -0
  206. package/src/components/icons/db-icons.tsx +84 -0
  207. package/src/components/libredb-logo.tsx +61 -0
  208. package/src/components/monitoring/MonitoringDashboard.tsx +345 -0
  209. package/src/components/monitoring/tabs/MetricChart.tsx +82 -0
  210. package/src/components/monitoring/tabs/OverviewTab.tsx +263 -0
  211. package/src/components/monitoring/tabs/PerformanceTab.tsx +254 -0
  212. package/src/components/monitoring/tabs/PoolTab.tsx +174 -0
  213. package/src/components/monitoring/tabs/QueriesTab.tsx +287 -0
  214. package/src/components/monitoring/tabs/SessionsTab.tsx +316 -0
  215. package/src/components/monitoring/tabs/StorageTab.tsx +335 -0
  216. package/src/components/monitoring/tabs/TablesTab.tsx +300 -0
  217. package/src/components/results-grid/ResultCard.tsx +111 -0
  218. package/src/components/results-grid/RowDetailSheet.tsx +178 -0
  219. package/src/components/results-grid/StatsBar.tsx +201 -0
  220. package/src/components/results-grid/index.ts +1 -0
  221. package/src/components/results-grid/utils.ts +23 -0
  222. package/src/components/schema-explorer/ColumnList.tsx +53 -0
  223. package/src/components/schema-explorer/SchemaExplorer.tsx +182 -0
  224. package/src/components/schema-explorer/TableItem.tsx +210 -0
  225. package/src/components/schema-explorer/index.ts +1 -0
  226. package/src/components/sidebar/ConnectionItem.tsx +105 -0
  227. package/src/components/sidebar/ConnectionsList.tsx +62 -0
  228. package/src/components/sidebar/Sidebar.tsx +130 -0
  229. package/src/components/sidebar/index.ts +2 -0
  230. package/src/components/studio/BottomPanel.tsx +286 -0
  231. package/src/components/studio/QueryToolbar.tsx +180 -0
  232. package/src/components/studio/StudioDesktopHeader.tsx +114 -0
  233. package/src/components/studio/StudioMobileHeader.tsx +340 -0
  234. package/src/components/studio/StudioTabBar.tsx +82 -0
  235. package/src/components/studio/index.ts +5 -0
  236. package/src/components/ui/accordion.tsx +66 -0
  237. package/src/components/ui/alert-dialog.tsx +157 -0
  238. package/src/components/ui/alert.tsx +66 -0
  239. package/src/components/ui/aspect-ratio.tsx +11 -0
  240. package/src/components/ui/avatar.tsx +53 -0
  241. package/src/components/ui/badge.tsx +46 -0
  242. package/src/components/ui/breadcrumb.tsx +109 -0
  243. package/src/components/ui/button-group.tsx +83 -0
  244. package/src/components/ui/button.tsx +60 -0
  245. package/src/components/ui/calendar.tsx +216 -0
  246. package/src/components/ui/card.tsx +92 -0
  247. package/src/components/ui/carousel.tsx +241 -0
  248. package/src/components/ui/chart.tsx +357 -0
  249. package/src/components/ui/checkbox.tsx +32 -0
  250. package/src/components/ui/collapsible.tsx +33 -0
  251. package/src/components/ui/command.tsx +184 -0
  252. package/src/components/ui/context-menu.tsx +252 -0
  253. package/src/components/ui/dialog.tsx +143 -0
  254. package/src/components/ui/drawer.tsx +135 -0
  255. package/src/components/ui/dropdown-menu.tsx +257 -0
  256. package/src/components/ui/empty.tsx +104 -0
  257. package/src/components/ui/field.tsx +248 -0
  258. package/src/components/ui/form.tsx +167 -0
  259. package/src/components/ui/hover-card.tsx +44 -0
  260. package/src/components/ui/input-group.tsx +170 -0
  261. package/src/components/ui/input-otp.tsx +77 -0
  262. package/src/components/ui/input.tsx +21 -0
  263. package/src/components/ui/item.tsx +193 -0
  264. package/src/components/ui/kbd.tsx +28 -0
  265. package/src/components/ui/label.tsx +24 -0
  266. package/src/components/ui/menubar.tsx +276 -0
  267. package/src/components/ui/navigation-menu.tsx +168 -0
  268. package/src/components/ui/pagination.tsx +127 -0
  269. package/src/components/ui/popover.tsx +48 -0
  270. package/src/components/ui/progress.tsx +31 -0
  271. package/src/components/ui/radio-group.tsx +45 -0
  272. package/src/components/ui/resizable.tsx +56 -0
  273. package/src/components/ui/scroll-area.tsx +58 -0
  274. package/src/components/ui/select.tsx +187 -0
  275. package/src/components/ui/separator.tsx +28 -0
  276. package/src/components/ui/sheet.tsx +139 -0
  277. package/src/components/ui/sidebar.tsx +726 -0
  278. package/src/components/ui/skeleton.tsx +13 -0
  279. package/src/components/ui/slider.tsx +63 -0
  280. package/src/components/ui/sonner.tsx +40 -0
  281. package/src/components/ui/spinner.tsx +16 -0
  282. package/src/components/ui/switch.tsx +31 -0
  283. package/src/components/ui/table.tsx +116 -0
  284. package/src/components/ui/tabs.tsx +66 -0
  285. package/src/components/ui/textarea.tsx +18 -0
  286. package/src/components/ui/toggle-group.tsx +83 -0
  287. package/src/components/ui/toggle.tsx +47 -0
  288. package/src/components/ui/tooltip.tsx +61 -0
  289. package/src/exports/components.ts +15 -0
  290. package/src/exports/index.ts +4 -0
  291. package/src/exports/providers.ts +4 -0
  292. package/src/exports/types.ts +26 -0
  293. package/src/hooks/use-ai-chat.ts +182 -0
  294. package/src/hooks/use-all-connections.ts +66 -0
  295. package/src/hooks/use-api-call.ts +71 -0
  296. package/src/hooks/use-auth.ts +51 -0
  297. package/src/hooks/use-connection-form.ts +349 -0
  298. package/src/hooks/use-connection-manager.ts +169 -0
  299. package/src/hooks/use-connection-payload.ts +15 -0
  300. package/src/hooks/use-inline-editing.ts +109 -0
  301. package/src/hooks/use-mobile.ts +20 -0
  302. package/src/hooks/use-monitoring-data.ts +270 -0
  303. package/src/hooks/use-provider-metadata.ts +62 -0
  304. package/src/hooks/use-query-execution.ts +478 -0
  305. package/src/hooks/use-storage-sync.ts +259 -0
  306. package/src/hooks/use-tab-manager.ts +231 -0
  307. package/src/hooks/use-toast.ts +20 -0
  308. package/src/hooks/use-transaction-control.ts +64 -0
  309. package/src/lib/api/error-codes.ts +30 -0
  310. package/src/lib/api/errors.ts +236 -0
  311. package/src/lib/api/with-error-handler.ts +41 -0
  312. package/src/lib/audit.ts +105 -0
  313. package/src/lib/auth.ts +87 -0
  314. package/src/lib/connection-string-parser.ts +172 -0
  315. package/src/lib/data-masking.ts +385 -0
  316. package/src/lib/db/base-provider.ts +325 -0
  317. package/src/lib/db/errors.ts +317 -0
  318. package/src/lib/db/factory.ts +324 -0
  319. package/src/lib/db/index.ts +123 -0
  320. package/src/lib/db/providers/document/index.ts +6 -0
  321. package/src/lib/db/providers/document/mongodb.ts +992 -0
  322. package/src/lib/db/providers/keyvalue/redis.ts +554 -0
  323. package/src/lib/db/providers/sql/index.ts +11 -0
  324. package/src/lib/db/providers/sql/mssql.ts +1065 -0
  325. package/src/lib/db/providers/sql/mysql.ts +978 -0
  326. package/src/lib/db/providers/sql/oracle.ts +1044 -0
  327. package/src/lib/db/providers/sql/postgres.ts +1179 -0
  328. package/src/lib/db/providers/sql/sql-base.ts +174 -0
  329. package/src/lib/db/providers/sql/sqlite.ts +721 -0
  330. package/src/lib/db/types.ts +437 -0
  331. package/src/lib/db/utils/pool-manager.ts +287 -0
  332. package/src/lib/db/utils/query-limiter.ts +239 -0
  333. package/src/lib/db-ui-config.ts +86 -0
  334. package/src/lib/editor/mongodb-completions.ts +172 -0
  335. package/src/lib/editor/sql-completions.ts +280 -0
  336. package/src/lib/llm/base-provider.ts +117 -0
  337. package/src/lib/llm/factory.ts +102 -0
  338. package/src/lib/llm/index.ts +90 -0
  339. package/src/lib/llm/providers/custom.ts +181 -0
  340. package/src/lib/llm/providers/gemini.ts +126 -0
  341. package/src/lib/llm/providers/ollama.ts +154 -0
  342. package/src/lib/llm/providers/openai.ts +146 -0
  343. package/src/lib/llm/types.ts +173 -0
  344. package/src/lib/llm/utils/config.ts +187 -0
  345. package/src/lib/llm/utils/retry.ts +119 -0
  346. package/src/lib/llm/utils/streaming.ts +202 -0
  347. package/src/lib/logger.ts +127 -0
  348. package/src/lib/monitoring-thresholds.ts +44 -0
  349. package/src/lib/oidc.ts +262 -0
  350. package/src/lib/query-generators.ts +61 -0
  351. package/src/lib/schema-diff/diff-engine.ts +273 -0
  352. package/src/lib/schema-diff/migration-generator.ts +208 -0
  353. package/src/lib/schema-diff/types.ts +55 -0
  354. package/src/lib/seed/config-loader.ts +79 -0
  355. package/src/lib/seed/connection-filter.ts +49 -0
  356. package/src/lib/seed/credential-resolver.ts +62 -0
  357. package/src/lib/seed/index.ts +40 -0
  358. package/src/lib/seed/resolve-connection.ts +57 -0
  359. package/src/lib/seed/types.ts +69 -0
  360. package/src/lib/sql/alias-extractor.ts +267 -0
  361. package/src/lib/sql/index.ts +8 -0
  362. package/src/lib/sql/statement-splitter.ts +167 -0
  363. package/src/lib/sql/types.ts +40 -0
  364. package/src/lib/ssh/tunnel.ts +142 -0
  365. package/src/lib/storage/factory.ts +84 -0
  366. package/src/lib/storage/index.ts +14 -0
  367. package/src/lib/storage/local-storage.ts +99 -0
  368. package/src/lib/storage/providers/postgres.ts +225 -0
  369. package/src/lib/storage/providers/sqlite.ts +153 -0
  370. package/src/lib/storage/storage-facade.ts +272 -0
  371. package/src/lib/storage/types.ts +75 -0
  372. package/src/lib/time-series-buffer.ts +58 -0
  373. package/src/lib/types.ts +173 -0
  374. package/src/lib/utils.ts +6 -0
  375. package/src/proxy.ts +104 -0
  376. package/src/types/db-drivers.d.ts +23 -0
  377. package/src/types/html2canvas.d.ts +9 -0
  378. package/tests/api/admin/audit.test.ts +178 -0
  379. package/tests/api/admin/fleet-health.test.ts +183 -0
  380. package/tests/api/ai/autopilot.test.ts +174 -0
  381. package/tests/api/ai/chat.test.ts +250 -0
  382. package/tests/api/ai/describe-schema.test.ts +266 -0
  383. package/tests/api/ai/explain.test.ts +199 -0
  384. package/tests/api/ai/impact.test.ts +168 -0
  385. package/tests/api/ai/index-advisor.test.ts +171 -0
  386. package/tests/api/ai/nl2sql.test.ts +202 -0
  387. package/tests/api/ai/query-safety.test.ts +196 -0
  388. package/tests/api/auth/login.test.ts +170 -0
  389. package/tests/api/auth/logout.test.ts +140 -0
  390. package/tests/api/auth/me.test.ts +73 -0
  391. package/tests/api/auth/oidc-callback.test.ts +215 -0
  392. package/tests/api/auth/oidc-login.test.ts +127 -0
  393. package/tests/api/db/cancel.test.ts +198 -0
  394. package/tests/api/db/disconnect.test.ts +124 -0
  395. package/tests/api/db/health.test.ts +222 -0
  396. package/tests/api/db/maintenance.test.ts +263 -0
  397. package/tests/api/db/monitoring.test.ts +221 -0
  398. package/tests/api/db/multi-query.test.ts +316 -0
  399. package/tests/api/db/pool-stats.test.ts +135 -0
  400. package/tests/api/db/profile.test.ts +330 -0
  401. package/tests/api/db/provider-meta.test.ts +193 -0
  402. package/tests/api/db/query.test.ts +314 -0
  403. package/tests/api/db/schema-snapshot.test.ts +170 -0
  404. package/tests/api/db/schema.test.ts +191 -0
  405. package/tests/api/db/test-connection.test.ts +185 -0
  406. package/tests/api/db/transaction.test.ts +314 -0
  407. package/tests/api/proxy.test.ts +191 -0
  408. package/tests/api/seed/managed-route.test.ts +113 -0
  409. package/tests/api/storage/config.test.ts +42 -0
  410. package/tests/api/storage/storage-routes.test.ts +309 -0
  411. package/tests/components/AIAutopilotPanel.test.tsx +756 -0
  412. package/tests/components/AdminPage.test.tsx +33 -0
  413. package/tests/components/CodeGenerator.test.tsx +182 -0
  414. package/tests/components/CommandPalette.test.tsx +428 -0
  415. package/tests/components/CommunitySection.test.tsx +91 -0
  416. package/tests/components/ConnectionModal.mobile.test.tsx +284 -0
  417. package/tests/components/ConnectionModal.test.tsx +570 -0
  418. package/tests/components/CreateTableModal.test.tsx +383 -0
  419. package/tests/components/DataCharts.test.tsx +739 -0
  420. package/tests/components/DataImportModal.test.tsx +751 -0
  421. package/tests/components/DataProfiler.test.tsx +589 -0
  422. package/tests/components/DatabaseDocs.test.tsx +353 -0
  423. package/tests/components/LoginPage.test.tsx +163 -0
  424. package/tests/components/LoginPageOIDC.test.tsx +92 -0
  425. package/tests/components/MaskingSettings.test.tsx +498 -0
  426. package/tests/components/MobileNav.test.tsx +30 -0
  427. package/tests/components/MonitoringPage.test.tsx +32 -0
  428. package/tests/components/NL2SQLPanel.test.tsx +621 -0
  429. package/tests/components/Page.test.tsx +33 -0
  430. package/tests/components/PivotTable.test.tsx +350 -0
  431. package/tests/components/QueryEditor.test.tsx +1730 -0
  432. package/tests/components/QueryHistory.test.tsx +572 -0
  433. package/tests/components/QuerySafetyDialog.test.tsx +586 -0
  434. package/tests/components/ResultsGrid.test.tsx +804 -0
  435. package/tests/components/RootLayout.test.tsx +83 -0
  436. package/tests/components/SaveQueryModal.test.tsx +25 -0
  437. package/tests/components/SavedQueries.test.tsx +43 -0
  438. package/tests/components/SchemaDiagram.test.tsx +1034 -0
  439. package/tests/components/SchemaDiff.test.tsx +906 -0
  440. package/tests/components/SnapshotTimeline.test.tsx +174 -0
  441. package/tests/components/Studio.test.tsx +1030 -0
  442. package/tests/components/TestDataGenerator.test.tsx +291 -0
  443. package/tests/components/VisualExplain.test.tsx +704 -0
  444. package/tests/components/admin/AdminDashboard.test.tsx +205 -0
  445. package/tests/components/admin/AuditTab.test.tsx +220 -0
  446. package/tests/components/admin/MonitoringEmbed.test.tsx +58 -0
  447. package/tests/components/admin/OperationsTab.test.tsx +975 -0
  448. package/tests/components/admin/OverviewTab.test.tsx +254 -0
  449. package/tests/components/admin/SecurityTab.test.tsx +467 -0
  450. package/tests/components/monitoring/MetricChart.test.tsx +111 -0
  451. package/tests/components/monitoring/MonitoringDashboard.test.tsx +259 -0
  452. package/tests/components/monitoring/OverviewTab.test.tsx +78 -0
  453. package/tests/components/monitoring/PerformanceTab.test.tsx +87 -0
  454. package/tests/components/monitoring/PoolTab.test.tsx +42 -0
  455. package/tests/components/monitoring/QueriesTab.test.tsx +80 -0
  456. package/tests/components/monitoring/SessionsTab.test.tsx +154 -0
  457. package/tests/components/monitoring/StorageTab.test.tsx +127 -0
  458. package/tests/components/monitoring/TablesTab.test.tsx +153 -0
  459. package/tests/components/results-grid/ResultCard.test.tsx +105 -0
  460. package/tests/components/results-grid/RowDetailSheet.test.tsx +308 -0
  461. package/tests/components/results-grid/StatsBar.test.tsx +162 -0
  462. package/tests/components/schema-explorer/ColumnList.test.tsx +151 -0
  463. package/tests/components/schema-explorer/SchemaExplorer.test.tsx +461 -0
  464. package/tests/components/schema-explorer/TableItem.test.tsx +415 -0
  465. package/tests/components/sidebar/ConnectionItem.test.tsx +201 -0
  466. package/tests/components/sidebar/ConnectionsList.test.tsx +176 -0
  467. package/tests/components/sidebar/Sidebar.test.tsx +187 -0
  468. package/tests/components/studio/BottomPanel.test.tsx +383 -0
  469. package/tests/components/studio/QueryToolbar.test.tsx +321 -0
  470. package/tests/components/studio/StudioDesktopHeader.test.tsx +377 -0
  471. package/tests/components/studio/StudioMobileHeader.test.tsx +198 -0
  472. package/tests/components/studio/StudioTabBar.test.tsx +331 -0
  473. package/tests/fixtures/connections.ts +96 -0
  474. package/tests/fixtures/masking-configs.ts +86 -0
  475. package/tests/fixtures/query-results.ts +71 -0
  476. package/tests/fixtures/schemas.ts +64 -0
  477. package/tests/fixtures/seed-connections/invalid-config.yaml +7 -0
  478. package/tests/fixtures/seed-connections/minimal-config.yaml +8 -0
  479. package/tests/fixtures/seed-connections/mixed-credentials.yaml +23 -0
  480. package/tests/fixtures/seed-connections/multi-role-config.yaml +30 -0
  481. package/tests/fixtures/seed-connections/valid-config.json +15 -0
  482. package/tests/fixtures/seed-connections/valid-config.yaml +51 -0
  483. package/tests/helpers/mock-fetch.ts +59 -0
  484. package/tests/helpers/mock-monaco.ts +112 -0
  485. package/tests/helpers/mock-navigation.ts +28 -0
  486. package/tests/helpers/mock-next.ts +80 -0
  487. package/tests/helpers/mock-provider.ts +133 -0
  488. package/tests/helpers/mock-sonner.ts +29 -0
  489. package/tests/helpers/render-with-providers.tsx +19 -0
  490. package/tests/hooks/use-ai-chat.test.ts +600 -0
  491. package/tests/hooks/use-auth.test.ts +371 -0
  492. package/tests/hooks/use-connection-form.test.ts +743 -0
  493. package/tests/hooks/use-connection-manager.test.ts +466 -0
  494. package/tests/hooks/use-inline-editing.test.ts +321 -0
  495. package/tests/hooks/use-mobile.test.ts +177 -0
  496. package/tests/hooks/use-monitoring-data.test.ts +819 -0
  497. package/tests/hooks/use-provider-metadata.test.ts +228 -0
  498. package/tests/hooks/use-query-execution.test.ts +1212 -0
  499. package/tests/hooks/use-tab-manager.test.ts +756 -0
  500. package/tests/hooks/use-toast.test.ts +74 -0
  501. package/tests/hooks/use-transaction-control.test.ts +211 -0
  502. package/tests/integration/db/mongodb-provider.test.ts +698 -0
  503. package/tests/integration/db/mssql-provider.test.ts +840 -0
  504. package/tests/integration/db/mysql-provider.test.ts +872 -0
  505. package/tests/integration/db/oracle-provider.test.ts +843 -0
  506. package/tests/integration/db/postgres-provider.test.ts +1382 -0
  507. package/tests/integration/db/redis-provider.test.ts +526 -0
  508. package/tests/integration/db/sqlite-provider.test.ts +480 -0
  509. package/tests/integration/seed/seed-pipeline.test.ts +102 -0
  510. package/tests/isolated/factory-singleton.test.ts +150 -0
  511. package/tests/isolated/use-storage-sync.test.ts +389 -0
  512. package/tests/run-components.sh +196 -0
  513. package/tests/setup-dom.ts +58 -0
  514. package/tests/setup.ts +40 -0
  515. package/tests/unit/api-errors.test.ts +210 -0
  516. package/tests/unit/code-generator-functions.test.ts +271 -0
  517. package/tests/unit/components/column-list.test.tsx +190 -0
  518. package/tests/unit/components/data-import-modal.test.tsx +441 -0
  519. package/tests/unit/components/studio-mobile-header.test.tsx +327 -0
  520. package/tests/unit/data-charts-functions.test.ts +496 -0
  521. package/tests/unit/data-import-functions.test.ts +320 -0
  522. package/tests/unit/data-import-utils.test.ts +125 -0
  523. package/tests/unit/db/base-provider.test.ts +517 -0
  524. package/tests/unit/db/errors.test.ts +403 -0
  525. package/tests/unit/db/factory.test.ts +436 -0
  526. package/tests/unit/db/pool-manager.test.ts +440 -0
  527. package/tests/unit/db/query-limiter.test.ts +387 -0
  528. package/tests/unit/db/sql-base.test.ts +438 -0
  529. package/tests/unit/lib/api/error-codes.test.ts +39 -0
  530. package/tests/unit/lib/audit.test.ts +326 -0
  531. package/tests/unit/lib/auth.test.ts +146 -0
  532. package/tests/unit/lib/connection-string-parser.test.ts +424 -0
  533. package/tests/unit/lib/data-masking.test.ts +583 -0
  534. package/tests/unit/lib/db-icons.test.tsx +41 -0
  535. package/tests/unit/lib/monitoring-thresholds.test.ts +133 -0
  536. package/tests/unit/lib/oidc.test.ts +509 -0
  537. package/tests/unit/lib/query-generators.test.ts +127 -0
  538. package/tests/unit/lib/storage/factory.test.ts +71 -0
  539. package/tests/unit/lib/storage/local-storage.test.ts +114 -0
  540. package/tests/unit/lib/storage/providers/postgres.test.ts +312 -0
  541. package/tests/unit/lib/storage/providers/sqlite.test.ts +232 -0
  542. package/tests/unit/lib/storage/storage-facade-extended.test.ts +331 -0
  543. package/tests/unit/lib/storage/storage-facade.test.ts +184 -0
  544. package/tests/unit/lib/storage.test.ts +317 -0
  545. package/tests/unit/lib/time-series-buffer.test.ts +212 -0
  546. package/tests/unit/lib/utils.test.ts +24 -0
  547. package/tests/unit/llm/base-provider.test.ts +238 -0
  548. package/tests/unit/llm/config.test.ts +262 -0
  549. package/tests/unit/llm/custom-provider.test.ts +281 -0
  550. package/tests/unit/llm/gemini-provider.test.ts +248 -0
  551. package/tests/unit/llm/llm-factory.test.ts +155 -0
  552. package/tests/unit/llm/ollama-provider.test.ts +288 -0
  553. package/tests/unit/llm/openai-provider.test.ts +324 -0
  554. package/tests/unit/llm/retry.test.ts +180 -0
  555. package/tests/unit/llm/streaming.test.ts +355 -0
  556. package/tests/unit/logger.test.ts +198 -0
  557. package/tests/unit/mongodb-completions.test.ts +516 -0
  558. package/tests/unit/pivot-table-functions.test.ts +76 -0
  559. package/tests/unit/query-cancelled-error.test.ts +81 -0
  560. package/tests/unit/schema-diff/diff-engine.test.ts +367 -0
  561. package/tests/unit/schema-diff/migration-generator.test.ts +513 -0
  562. package/tests/unit/seed/config-loader.test.ts +73 -0
  563. package/tests/unit/seed/connection-filter.test.ts +91 -0
  564. package/tests/unit/seed/credential-resolver.test.ts +85 -0
  565. package/tests/unit/seed/index.test.ts +72 -0
  566. package/tests/unit/seed/resolve-connection.test.ts +74 -0
  567. package/tests/unit/seed/types.test.ts +129 -0
  568. package/tests/unit/sql/alias-extractor.test.ts +444 -0
  569. package/tests/unit/sql/statement-splitter.test.ts +348 -0
  570. package/tests/unit/sql-completions.test.ts +463 -0
  571. package/tests/unit/ssh-tunnel.test.ts +465 -0
  572. package/tsconfig.json +42 -0
@@ -0,0 +1,962 @@
1
+ "use client";
2
+
3
+ import React, { useState, useMemo, useRef, useCallback } from 'react';
4
+ import {
5
+ BarChart,
6
+ Bar,
7
+ LineChart,
8
+ Line,
9
+ PieChart,
10
+ Pie,
11
+ AreaChart,
12
+ Area,
13
+ ScatterChart,
14
+ Scatter,
15
+ ZAxis,
16
+ XAxis,
17
+ YAxis,
18
+ CartesianGrid,
19
+ Tooltip,
20
+ Legend,
21
+ Cell,
22
+ ResponsiveContainer,
23
+ } from 'recharts';
24
+ import { QueryResult } from '@/lib/types';
25
+ import { cn } from '@/lib/utils';
26
+ import {
27
+ BarChart3,
28
+ LineChart as LineChartIcon,
29
+ PieChart as PieChartIcon,
30
+ AreaChart as AreaChartIcon,
31
+ Download,
32
+ Settings2,
33
+ TrendingUp,
34
+ Hash,
35
+ Calendar,
36
+ Type,
37
+ AlertCircle,
38
+ Circle,
39
+ BarChart2,
40
+ Save,
41
+ FolderOpen,
42
+ X,
43
+ } from 'lucide-react';
44
+ import { Button } from '@/components/ui/button';
45
+ import {
46
+ DropdownMenu,
47
+ DropdownMenuContent,
48
+ DropdownMenuItem,
49
+ DropdownMenuTrigger,
50
+ } from '@/components/ui/dropdown-menu';
51
+ import {
52
+ Select,
53
+ SelectContent,
54
+ SelectItem,
55
+ SelectTrigger,
56
+ SelectValue,
57
+ } from '@/components/ui/select';
58
+ import { storage } from '@/lib/storage';
59
+
60
+ // Chart colors matching CSS variables
61
+ const CHART_COLORS = [
62
+ 'hsl(217, 91%, 60%)', // Blue
63
+ 'hsl(142, 71%, 45%)', // Green
64
+ 'hsl(38, 92%, 50%)', // Amber
65
+ 'hsl(270, 91%, 65%)', // Purple
66
+ 'hsl(330, 81%, 60%)', // Pink
67
+ 'hsl(199, 89%, 48%)', // Cyan
68
+ 'hsl(24, 95%, 53%)', // Orange
69
+ 'hsl(162, 63%, 41%)', // Teal
70
+ ];
71
+
72
+ type ChartType = 'bar' | 'line' | 'pie' | 'area' | 'scatter' | 'histogram' | 'stacked-bar' | 'stacked-area';
73
+
74
+ export type AggregationType = 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max';
75
+ export type DateGrouping = 'hour' | 'day' | 'week' | 'month' | 'year';
76
+
77
+ export interface FieldAnalysis {
78
+ name: string;
79
+ type: 'numeric' | 'categorical' | 'date' | 'unknown';
80
+ uniqueValues: number;
81
+ hasNulls: boolean;
82
+ sample: unknown;
83
+ }
84
+
85
+ export interface DataAnalysis {
86
+ fields: FieldAnalysis[];
87
+ numericFields: string[];
88
+ categoricalFields: string[];
89
+ dateFields: string[];
90
+ suggestedChartType: ChartType;
91
+ isVisualizable: boolean;
92
+ reason?: string;
93
+ }
94
+
95
+ interface DataChartsProps {
96
+ result: QueryResult | null;
97
+ }
98
+
99
+ export function analyzeField(name: string, values: unknown[]): FieldAnalysis {
100
+ const nonNullValues = values.filter(v => v !== null && v !== undefined);
101
+ const uniqueValues = new Set(nonNullValues).size;
102
+ const sample = nonNullValues[0];
103
+
104
+ // Check if numeric
105
+ const numericCount = nonNullValues.filter(v => typeof v === 'number' || (typeof v === 'string' && !isNaN(Number(v)))).length;
106
+ const isNumeric = numericCount > nonNullValues.length * 0.8;
107
+
108
+ // Check if date
109
+ const datePatterns = [
110
+ /^\d{4}-\d{2}-\d{2}/, // ISO date
111
+ /^\d{2}\/\d{2}\/\d{4}/, // US date
112
+ /^\d{2}\.\d{2}\.\d{4}/, // EU date
113
+ ];
114
+ const isDate = nonNullValues.some(v =>
115
+ (typeof v === 'string' && datePatterns.some(p => p.test(v))) ||
116
+ v instanceof Date
117
+ );
118
+
119
+ let type: FieldAnalysis['type'] = 'unknown';
120
+ if (isDate) type = 'date';
121
+ else if (isNumeric) type = 'numeric';
122
+ else if (uniqueValues <= 50) type = 'categorical';
123
+
124
+ return {
125
+ name,
126
+ type,
127
+ uniqueValues,
128
+ hasNulls: nonNullValues.length < values.length,
129
+ sample,
130
+ };
131
+ }
132
+
133
+ export function analyzeData(result: QueryResult | null): DataAnalysis {
134
+ if (!result || !result.rows || result.rows.length === 0) {
135
+ return {
136
+ fields: [],
137
+ numericFields: [],
138
+ categoricalFields: [],
139
+ dateFields: [],
140
+ suggestedChartType: 'bar',
141
+ isVisualizable: false,
142
+ reason: 'No data to visualize',
143
+ };
144
+ }
145
+
146
+ if (result.rows.length < 2) {
147
+ return {
148
+ fields: [],
149
+ numericFields: [],
150
+ categoricalFields: [],
151
+ dateFields: [],
152
+ suggestedChartType: 'bar',
153
+ isVisualizable: false,
154
+ reason: 'Need at least 2 rows for visualization',
155
+ };
156
+ }
157
+
158
+ const fieldNames = result.fields || Object.keys(result.rows[0]);
159
+ const fields = fieldNames.map(name =>
160
+ analyzeField(name, result.rows.map(row => row[name]))
161
+ );
162
+
163
+ const numericFields = fields.filter(f => f.type === 'numeric').map(f => f.name);
164
+ const categoricalFields = fields.filter(f => f.type === 'categorical').map(f => f.name);
165
+ const dateFields = fields.filter(f => f.type === 'date').map(f => f.name);
166
+
167
+ if (numericFields.length === 0) {
168
+ return {
169
+ fields,
170
+ numericFields,
171
+ categoricalFields,
172
+ dateFields,
173
+ suggestedChartType: 'bar',
174
+ isVisualizable: false,
175
+ reason: 'No numeric fields found for Y-axis',
176
+ };
177
+ }
178
+
179
+ // Suggest chart type based on data
180
+ let suggestedChartType: ChartType = 'bar';
181
+
182
+ if (dateFields.length > 0) {
183
+ suggestedChartType = 'line'; // Time series → line chart
184
+ } else if (numericFields.length >= 2 && categoricalFields.length === 0) {
185
+ suggestedChartType = 'scatter'; // 2+ numeric, no categorical → scatter
186
+ } else if (categoricalFields.length > 0 && result.rows.length <= 10) {
187
+ suggestedChartType = 'pie'; // Few categories → pie chart
188
+ } else if (categoricalFields.length > 0) {
189
+ suggestedChartType = 'bar'; // Many categories → bar chart
190
+ }
191
+
192
+ return {
193
+ fields,
194
+ numericFields,
195
+ categoricalFields,
196
+ dateFields,
197
+ suggestedChartType,
198
+ isVisualizable: true,
199
+ };
200
+ }
201
+
202
+ export function formatNumber(value: number): string {
203
+ if (Math.abs(value) >= 1000000) {
204
+ return (value / 1000000).toFixed(1) + 'M';
205
+ }
206
+ if (Math.abs(value) >= 1000) {
207
+ return (value / 1000).toFixed(1) + 'K';
208
+ }
209
+ return value.toLocaleString();
210
+ }
211
+
212
+ interface TooltipProps {
213
+ active?: boolean;
214
+ payload?: Array<{
215
+ name: string;
216
+ value: number;
217
+ color: string;
218
+ }>;
219
+ label?: string;
220
+ }
221
+
222
+ const CustomTooltip = ({ active, payload, label }: TooltipProps) => {
223
+ if (!active || !payload || !payload.length) return null;
224
+
225
+ return (
226
+ <div className="bg-[#111] border border-white/10 rounded-lg px-3 py-2 shadow-xl">
227
+ <p className="text-zinc-400 text-xs mb-1">{label}</p>
228
+ {payload.map((entry, index) => (
229
+ <p key={index} className="text-sm" style={{ color: entry.color }}>
230
+ {entry.name}: <span className="font-mono font-medium">{formatNumber(entry.value)}</span>
231
+ </p>
232
+ ))}
233
+ </div>
234
+ );
235
+ };
236
+
237
+ // Histogram bin calculation
238
+ export function computeHistogramBins(values: number[], buckets: number): { range: string; count: number; min: number; max: number }[] {
239
+ if (values.length === 0) return [];
240
+ const min = Math.min(...values);
241
+ const max = Math.max(...values);
242
+ if (min === max) return [{ range: `${min}`, count: values.length, min, max }];
243
+ const binWidth = (max - min) / buckets;
244
+ const bins = Array.from({ length: buckets }, (_, i) => ({
245
+ range: `${(min + i * binWidth).toFixed(1)}-${(min + (i + 1) * binWidth).toFixed(1)}`,
246
+ count: 0,
247
+ min: min + i * binWidth,
248
+ max: min + (i + 1) * binWidth,
249
+ }));
250
+ values.forEach(v => {
251
+ let idx = Math.floor((v - min) / binWidth);
252
+ if (idx >= buckets) idx = buckets - 1;
253
+ bins[idx].count++;
254
+ });
255
+ return bins;
256
+ }
257
+
258
+ // Data aggregation helper
259
+ export function aggregateData(
260
+ rows: Record<string, unknown>[],
261
+ groupByField: string,
262
+ metrics: { field: string; aggregation: AggregationType }[],
263
+ dateGrouping?: DateGrouping
264
+ ): Record<string, unknown>[] {
265
+ if (metrics.every(m => m.aggregation === 'none')) return rows;
266
+
267
+ const groups = new Map<string, Record<string, unknown>[]>();
268
+ rows.forEach(row => {
269
+ let key = String(row[groupByField] ?? '');
270
+ if (dateGrouping && key) {
271
+ key = groupByDate(key, dateGrouping);
272
+ }
273
+ if (!groups.has(key)) groups.set(key, []);
274
+ groups.get(key)!.push(row);
275
+ });
276
+
277
+ return Array.from(groups.entries()).map(([key, groupRows]) => {
278
+ const result: Record<string, unknown> = { [groupByField]: key };
279
+ metrics.forEach(({ field, aggregation }) => {
280
+ const values = groupRows.map(r => Number(r[field]) || 0);
281
+ switch (aggregation) {
282
+ case 'sum': result[field] = values.reduce((a, b) => a + b, 0); break;
283
+ case 'avg': result[field] = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; break;
284
+ case 'count': result[field] = values.length; break;
285
+ case 'min': result[field] = Math.min(...values); break;
286
+ case 'max': result[field] = Math.max(...values); break;
287
+ default: result[field] = values[0];
288
+ }
289
+ });
290
+ return result;
291
+ });
292
+ }
293
+
294
+ export function groupByDate(dateStr: string, grouping: DateGrouping): string {
295
+ const date = new Date(dateStr);
296
+ if (isNaN(date.getTime())) return dateStr;
297
+ switch (grouping) {
298
+ case 'hour': return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')} ${String(date.getHours()).padStart(2,'0')}:00`;
299
+ case 'day': return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`;
300
+ case 'week': { const d = new Date(date); d.setDate(d.getDate() - d.getDay()); return `W${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; }
301
+ case 'month': return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}`;
302
+ case 'year': return `${date.getFullYear()}`;
303
+ default: return dateStr;
304
+ }
305
+ }
306
+
307
+ export function DataCharts({ result }: DataChartsProps) {
308
+ const chartRef = useRef<HTMLDivElement>(null);
309
+ const analysis = useMemo(() => analyzeData(result), [result]);
310
+
311
+ const [chartType, setChartType] = useState<ChartType>(analysis.suggestedChartType);
312
+ const [xAxis, setXAxis] = useState<string>('');
313
+ const [yAxis, setYAxis] = useState<string[]>([]);
314
+ const [scatterY, setScatterY] = useState<string>('');
315
+ const [histogramBuckets, setHistogramBuckets] = useState(10);
316
+ const [aggregation, setAggregation] = useState<AggregationType>('none');
317
+ const [dateGrouping, setDateGrouping] = useState<DateGrouping | ''>('');
318
+
319
+ // Saved charts state
320
+ const [savedCharts, setSavedCharts] = useState<{ id: string; name: string; chartType: ChartType; xAxis: string; yAxis: string[]; aggregation: AggregationType; dateGrouping: string }[]>([]);
321
+ const [showSaveDialog, setShowSaveDialog] = useState(false);
322
+ const [saveName, setSaveName] = useState('');
323
+
324
+ // Load saved charts from storage
325
+ React.useEffect(() => {
326
+ const charts = storage.getSavedCharts();
327
+ if (charts.length > 0) {
328
+ setSavedCharts(charts.map(c => ({
329
+ id: c.id,
330
+ name: c.name,
331
+ chartType: c.chartType as ChartType,
332
+ xAxis: c.xAxis,
333
+ yAxis: c.yAxis,
334
+ aggregation: (c.aggregation || 'none') as AggregationType,
335
+ dateGrouping: c.dateGrouping || '',
336
+ })));
337
+ }
338
+ }, []);
339
+
340
+ // Initialize axis selections when analysis changes
341
+ React.useEffect(() => {
342
+ if (analysis.isVisualizable) {
343
+ setChartType(analysis.suggestedChartType);
344
+
345
+ const defaultX = analysis.categoricalFields[0] || analysis.dateFields[0] || analysis.fields[0]?.name || '';
346
+ setXAxis(defaultX);
347
+
348
+ if (analysis.numericFields.length > 0) {
349
+ setYAxis([analysis.numericFields[0]]);
350
+ }
351
+ if (analysis.numericFields.length >= 2) {
352
+ setScatterY(analysis.numericFields[1]);
353
+ }
354
+ }
355
+ }, [analysis]);
356
+
357
+ const chartData = useMemo(() => {
358
+ if (!result?.rows) return [];
359
+
360
+ // Histogram: special data preparation
361
+ if (chartType === 'histogram' && yAxis.length > 0) {
362
+ const values = result.rows.map(r => Number(r[yAxis[0]]) || 0).filter(v => !isNaN(v));
363
+ return computeHistogramBins(values, histogramBuckets);
364
+ }
365
+
366
+ // Scatter: needs both axes as numeric
367
+ if (chartType === 'scatter') {
368
+ if (!xAxis || !scatterY) return [];
369
+ return result.rows.map(row => ({
370
+ [xAxis]: typeof row[xAxis] === 'number' ? row[xAxis] : Number(row[xAxis]) || 0,
371
+ [scatterY]: typeof row[scatterY] === 'number' ? row[scatterY] : Number(row[scatterY]) || 0,
372
+ }));
373
+ }
374
+
375
+ if (!xAxis) return [];
376
+
377
+ const baseData = result.rows.map(row => {
378
+ const dataPoint: Record<string, unknown> = { [xAxis]: row[xAxis] };
379
+ yAxis.forEach(field => {
380
+ const value = row[field];
381
+ dataPoint[field] = typeof value === 'number' ? value : Number(value) || 0;
382
+ });
383
+ return dataPoint;
384
+ });
385
+
386
+ // Apply aggregation if set
387
+ if (aggregation !== 'none' && yAxis.length > 0) {
388
+ return aggregateData(
389
+ baseData,
390
+ xAxis,
391
+ yAxis.map(f => ({ field: f, aggregation })),
392
+ dateGrouping || undefined
393
+ );
394
+ }
395
+
396
+ // Apply date grouping even without aggregation
397
+ if (dateGrouping) {
398
+ return aggregateData(
399
+ baseData,
400
+ xAxis,
401
+ yAxis.map(f => ({ field: f, aggregation: 'sum' })),
402
+ dateGrouping
403
+ );
404
+ }
405
+
406
+ return baseData;
407
+ }, [result, xAxis, yAxis, chartType, scatterY, histogramBuckets, aggregation, dateGrouping]);
408
+
409
+ // Save chart config
410
+ const handleSaveChart = useCallback(() => {
411
+ if (!saveName.trim()) return;
412
+ const newChart = {
413
+ id: Date.now().toString(),
414
+ name: saveName.trim(),
415
+ chartType,
416
+ xAxis,
417
+ yAxis: [...yAxis],
418
+ aggregation,
419
+ dateGrouping: dateGrouping || '',
420
+ };
421
+ const updated = [...savedCharts, newChart];
422
+ setSavedCharts(updated);
423
+ storage.saveChart({
424
+ id: newChart.id,
425
+ name: newChart.name,
426
+ chartType: newChart.chartType,
427
+ xAxis: newChart.xAxis,
428
+ yAxis: newChart.yAxis,
429
+ aggregation: newChart.aggregation,
430
+ dateGrouping: (newChart.dateGrouping || undefined) as DateGrouping | undefined,
431
+ createdAt: new Date(),
432
+ });
433
+ setShowSaveDialog(false);
434
+ setSaveName('');
435
+ }, [saveName, chartType, xAxis, yAxis, aggregation, dateGrouping, savedCharts]);
436
+
437
+ // Load saved chart config
438
+ const loadSavedChart = useCallback((chart: typeof savedCharts[0]) => {
439
+ setChartType(chart.chartType);
440
+ setXAxis(chart.xAxis);
441
+ setYAxis(chart.yAxis);
442
+ setAggregation(chart.aggregation);
443
+ setDateGrouping((chart.dateGrouping || '') as DateGrouping | '');
444
+ }, []);
445
+
446
+ // Delete saved chart
447
+ const deleteSavedChart = useCallback((id: string) => {
448
+ const updated = savedCharts.filter(c => c.id !== id);
449
+ setSavedCharts(updated);
450
+ storage.deleteChart(id);
451
+ }, [savedCharts]);
452
+
453
+ const exportChart = useCallback(async (format: 'png' | 'svg') => {
454
+ if (!chartRef.current) return;
455
+
456
+ if (format === 'png') {
457
+ try {
458
+ // Dynamic import for html2canvas
459
+ const html2canvasModule = await import('html2canvas');
460
+ const html2canvas = html2canvasModule.default as (element: HTMLElement, options?: { backgroundColor?: string; scale?: number }) => Promise<HTMLCanvasElement>;
461
+ const canvas = await html2canvas(chartRef.current, {
462
+ backgroundColor: '#080808',
463
+ scale: 2,
464
+ });
465
+ const link = document.createElement('a');
466
+ link.download = `chart_${Date.now()}.png`;
467
+ link.href = canvas.toDataURL('image/png');
468
+ link.click();
469
+ } catch (error) {
470
+ console.error('Failed to export PNG:', error);
471
+ }
472
+ } else {
473
+ // SVG export - find the SVG element
474
+ const svgElement = chartRef.current.querySelector('svg');
475
+ if (svgElement) {
476
+ const svgData = new XMLSerializer().serializeToString(svgElement);
477
+ const blob = new Blob([svgData], { type: 'image/svg+xml' });
478
+ const url = URL.createObjectURL(blob);
479
+ const link = document.createElement('a');
480
+ link.download = `chart_${Date.now()}.svg`;
481
+ link.href = url;
482
+ link.click();
483
+ URL.revokeObjectURL(url);
484
+ }
485
+ }
486
+ }, []);
487
+
488
+ const toggleYAxis = (field: string) => {
489
+ setYAxis(prev => {
490
+ if (prev.includes(field)) {
491
+ return prev.filter(f => f !== field);
492
+ }
493
+ return [...prev, field];
494
+ });
495
+ };
496
+
497
+ // Empty state
498
+ if (!analysis.isVisualizable) {
499
+ return (
500
+ <div className="h-full flex flex-col items-center justify-center bg-[#080808] text-zinc-500">
501
+ <TrendingUp className="w-12 h-12 mb-4 opacity-30" />
502
+ <p className="text-sm font-medium mb-1">Cannot Visualize Data</p>
503
+ <p className="text-xs text-zinc-600">{analysis.reason}</p>
504
+ </div>
505
+ );
506
+ }
507
+
508
+ const chartTypes: { type: ChartType; icon: React.ReactNode; label: string }[] = [
509
+ { type: 'bar', icon: <BarChart3 className="w-4 h-4" />, label: 'Bar' },
510
+ { type: 'line', icon: <LineChartIcon className="w-4 h-4" />, label: 'Line' },
511
+ { type: 'pie', icon: <PieChartIcon className="w-4 h-4" />, label: 'Pie' },
512
+ { type: 'area', icon: <AreaChartIcon className="w-4 h-4" />, label: 'Area' },
513
+ { type: 'scatter', icon: <Circle className="w-4 h-4" />, label: 'Scatter' },
514
+ { type: 'histogram', icon: <BarChart2 className="w-4 h-4" />, label: 'Histogram' },
515
+ { type: 'stacked-bar', icon: <BarChart3 className="w-4 h-4" />, label: 'Stacked' },
516
+ { type: 'stacked-area', icon: <AreaChartIcon className="w-4 h-4" />, label: 'Stack Area' },
517
+ ];
518
+
519
+ const getFieldIcon = (type: FieldAnalysis['type']) => {
520
+ switch (type) {
521
+ case 'numeric': return <Hash className="w-3 h-3" />;
522
+ case 'date': return <Calendar className="w-3 h-3" />;
523
+ case 'categorical': return <Type className="w-3 h-3" />;
524
+ default: return <AlertCircle className="w-3 h-3" />;
525
+ }
526
+ };
527
+
528
+ return (
529
+ <div className="h-full flex flex-col bg-[#080808]">
530
+ {/* Config Bar */}
531
+ <div className="flex items-center gap-2 px-3 py-2 border-b border-white/5 bg-[#0a0a0a] flex-wrap">
532
+ {/* Chart Type Selector */}
533
+ <div className="flex items-center gap-1 bg-white/5 rounded-lg p-0.5">
534
+ {chartTypes.map(({ type, icon, label }) => (
535
+ <button
536
+ key={type}
537
+ onClick={() => setChartType(type)}
538
+ className={cn(
539
+ "flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all",
540
+ chartType === type
541
+ ? "bg-blue-600 text-white"
542
+ : "text-zinc-500 hover:text-zinc-300 hover:bg-white/5"
543
+ )}
544
+ title={label}
545
+ >
546
+ {icon}
547
+ <span className="hidden sm:inline">{label}</span>
548
+ </button>
549
+ ))}
550
+ </div>
551
+
552
+ <div className="h-4 w-px bg-white/10 hidden sm:block" />
553
+
554
+ {/* X-Axis Selector */}
555
+ {chartType !== 'pie' && (
556
+ <div className="flex items-center gap-2">
557
+ <span className="text-[10px] text-zinc-600 uppercase tracking-wider">X-Axis</span>
558
+ <Select value={xAxis} onValueChange={setXAxis}>
559
+ <SelectTrigger className="h-7 w-[140px] text-xs bg-white/5 border-white/10">
560
+ <SelectValue placeholder="Select field" />
561
+ </SelectTrigger>
562
+ <SelectContent className="bg-[#111] border-white/10">
563
+ {analysis.fields.map(field => (
564
+ <SelectItem key={field.name} value={field.name} className="text-xs">
565
+ <div className="flex items-center gap-2">
566
+ {getFieldIcon(field.type)}
567
+ {field.name}
568
+ </div>
569
+ </SelectItem>
570
+ ))}
571
+ </SelectContent>
572
+ </Select>
573
+ </div>
574
+ )}
575
+
576
+ {/* Y-Axis Selector (for pie, this becomes the value field) */}
577
+ <div className="flex items-center gap-2">
578
+ <span className="text-[10px] text-zinc-600 uppercase tracking-wider">
579
+ {chartType === 'pie' ? 'Value' : 'Y-Axis'}
580
+ </span>
581
+ <DropdownMenu>
582
+ <DropdownMenuTrigger asChild>
583
+ <Button variant="outline" size="sm" className="h-7 text-xs bg-white/5 border-white/10 gap-1">
584
+ {yAxis.length > 0 ? yAxis.join(', ') : 'Select fields'}
585
+ <Settings2 className="w-3 h-3 ml-1" />
586
+ </Button>
587
+ </DropdownMenuTrigger>
588
+ <DropdownMenuContent className="bg-[#111] border-white/10">
589
+ {analysis.numericFields.map(field => (
590
+ <DropdownMenuItem
591
+ key={field}
592
+ onClick={() => chartType === 'pie' ? setYAxis([field]) : toggleYAxis(field)}
593
+ className={cn(
594
+ "text-xs cursor-pointer",
595
+ yAxis.includes(field) && "bg-blue-600/20 text-blue-400"
596
+ )}
597
+ >
598
+ <Hash className="w-3 h-3 mr-2" />
599
+ {field}
600
+ {yAxis.includes(field) && <span className="ml-auto">✓</span>}
601
+ </DropdownMenuItem>
602
+ ))}
603
+ </DropdownMenuContent>
604
+ </DropdownMenu>
605
+ </div>
606
+
607
+ {/* Scatter Y-axis */}
608
+ {chartType === 'scatter' && (
609
+ <div className="flex items-center gap-2">
610
+ <span className="text-[10px] text-zinc-600 uppercase tracking-wider">Y</span>
611
+ <Select value={scatterY} onValueChange={setScatterY}>
612
+ <SelectTrigger className="h-7 w-[120px] text-xs bg-white/5 border-white/10">
613
+ <SelectValue placeholder="Y field" />
614
+ </SelectTrigger>
615
+ <SelectContent className="bg-[#111] border-white/10">
616
+ {analysis.numericFields.filter(f => f !== xAxis).map(field => (
617
+ <SelectItem key={field} value={field} className="text-xs">{field}</SelectItem>
618
+ ))}
619
+ </SelectContent>
620
+ </Select>
621
+ </div>
622
+ )}
623
+
624
+ {/* Histogram buckets */}
625
+ {chartType === 'histogram' && (
626
+ <div className="flex items-center gap-2">
627
+ <span className="text-[10px] text-zinc-600 uppercase tracking-wider">Buckets</span>
628
+ <Select value={String(histogramBuckets)} onValueChange={(v) => setHistogramBuckets(Number(v))}>
629
+ <SelectTrigger className="h-7 w-[70px] text-xs bg-white/5 border-white/10">
630
+ <SelectValue />
631
+ </SelectTrigger>
632
+ <SelectContent className="bg-[#111] border-white/10">
633
+ {[5, 10, 20, 50].map(n => (
634
+ <SelectItem key={n} value={String(n)} className="text-xs">{n}</SelectItem>
635
+ ))}
636
+ </SelectContent>
637
+ </Select>
638
+ </div>
639
+ )}
640
+
641
+ {/* Aggregation */}
642
+ {chartType !== 'scatter' && chartType !== 'histogram' && chartType !== 'pie' && (
643
+ <div className="flex items-center gap-2">
644
+ <span className="text-[10px] text-zinc-600 uppercase tracking-wider">Agg</span>
645
+ <Select value={aggregation} onValueChange={(v) => setAggregation(v as AggregationType)}>
646
+ <SelectTrigger className="h-7 w-[80px] text-xs bg-white/5 border-white/10">
647
+ <SelectValue />
648
+ </SelectTrigger>
649
+ <SelectContent className="bg-[#111] border-white/10">
650
+ {(['none', 'sum', 'avg', 'count', 'min', 'max'] as const).map(a => (
651
+ <SelectItem key={a} value={a} className="text-xs uppercase">{a}</SelectItem>
652
+ ))}
653
+ </SelectContent>
654
+ </Select>
655
+ </div>
656
+ )}
657
+
658
+ {/* Date Grouping */}
659
+ {analysis.dateFields.length > 0 && chartType !== 'scatter' && chartType !== 'histogram' && (
660
+ <div className="flex items-center gap-2">
661
+ <span className="text-[10px] text-zinc-600 uppercase tracking-wider">Group</span>
662
+ <Select value={dateGrouping || 'none'} onValueChange={(v) => setDateGrouping(v === 'none' ? '' : v as DateGrouping)}>
663
+ <SelectTrigger className="h-7 w-[80px] text-xs bg-white/5 border-white/10">
664
+ <SelectValue />
665
+ </SelectTrigger>
666
+ <SelectContent className="bg-[#111] border-white/10">
667
+ <SelectItem value="none" className="text-xs">None</SelectItem>
668
+ {(['hour', 'day', 'week', 'month', 'year'] as const).map(g => (
669
+ <SelectItem key={g} value={g} className="text-xs capitalize">{g}</SelectItem>
670
+ ))}
671
+ </SelectContent>
672
+ </Select>
673
+ </div>
674
+ )}
675
+
676
+ {/* Spacer */}
677
+ <div className="flex-1" />
678
+
679
+ {/* Save Chart */}
680
+ {showSaveDialog ? (
681
+ <div className="flex items-center gap-1">
682
+ <input
683
+ type="text"
684
+ placeholder="Chart name..."
685
+ value={saveName}
686
+ onChange={(e) => setSaveName(e.target.value)}
687
+ onKeyDown={(e) => e.key === 'Enter' && handleSaveChart()}
688
+ className="h-7 px-2 text-xs bg-white/5 border border-white/10 rounded text-zinc-300 focus:outline-none focus:border-blue-500"
689
+ autoFocus
690
+ />
691
+ <Button variant="ghost" size="sm" className="h-7 text-xs text-blue-400" onClick={handleSaveChart}>Save</Button>
692
+ <Button variant="ghost" size="sm" className="h-7 text-xs text-zinc-500" onClick={() => setShowSaveDialog(false)}>Cancel</Button>
693
+ </div>
694
+ ) : (
695
+ <div className="flex items-center gap-1">
696
+ <Button variant="ghost" size="sm" className="h-7 text-[10px] text-zinc-500 hover:text-white gap-1" onClick={() => setShowSaveDialog(true)}>
697
+ <Save className="w-3 h-3" /> Save
698
+ </Button>
699
+ {savedCharts.length > 0 && (
700
+ <DropdownMenu>
701
+ <DropdownMenuTrigger asChild>
702
+ <Button variant="ghost" size="sm" className="h-7 text-[10px] text-zinc-500 hover:text-white gap-1">
703
+ <FolderOpen className="w-3 h-3" /> Saved ({savedCharts.length})
704
+ </Button>
705
+ </DropdownMenuTrigger>
706
+ <DropdownMenuContent align="end" className="bg-[#111] border-white/10 max-h-48 overflow-auto">
707
+ {savedCharts.map(chart => (
708
+ <DropdownMenuItem key={chart.id} className="text-xs cursor-pointer flex items-center justify-between gap-4">
709
+ <span onClick={() => loadSavedChart(chart)}>{chart.name} <span className="text-zinc-600">({chart.chartType})</span></span>
710
+ <button onClick={(e) => { e.stopPropagation(); deleteSavedChart(chart.id); }} className="text-zinc-600 hover:text-red-400">
711
+ <X className="w-3 h-3" />
712
+ </button>
713
+ </DropdownMenuItem>
714
+ ))}
715
+ </DropdownMenuContent>
716
+ </DropdownMenu>
717
+ )}
718
+ </div>
719
+ )}
720
+
721
+ {/* Export Button */}
722
+ <DropdownMenu>
723
+ <DropdownMenuTrigger asChild>
724
+ <Button variant="ghost" size="sm" className="h-7 text-[10px] font-bold uppercase text-zinc-500 hover:text-white gap-1">
725
+ <Download className="w-3 h-3" /> Export
726
+ </Button>
727
+ </DropdownMenuTrigger>
728
+ <DropdownMenuContent align="end" className="bg-[#111] border-white/10">
729
+ <DropdownMenuItem onClick={() => exportChart('png')} className="text-xs cursor-pointer">
730
+ Export as PNG
731
+ </DropdownMenuItem>
732
+ <DropdownMenuItem onClick={() => exportChart('svg')} className="text-xs cursor-pointer">
733
+ Export as SVG
734
+ </DropdownMenuItem>
735
+ </DropdownMenuContent>
736
+ </DropdownMenu>
737
+ </div>
738
+
739
+ {/* Chart Area */}
740
+ <div ref={chartRef} className="flex-1 p-4 min-h-0">
741
+ {yAxis.length === 0 ? (
742
+ <div className="h-full flex items-center justify-center text-zinc-600 text-sm">
743
+ Select at least one numeric field for the chart
744
+ </div>
745
+ ) : (
746
+ <ResponsiveContainer width="100%" height="100%">
747
+ {chartType === 'bar' ? (
748
+ <BarChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
749
+ <CartesianGrid strokeDasharray="3 3" stroke="#222" />
750
+ <XAxis
751
+ dataKey={xAxis}
752
+ tick={{ fill: '#666', fontSize: 11 }}
753
+ angle={-45}
754
+ textAnchor="end"
755
+ height={60}
756
+ />
757
+ <YAxis
758
+ tick={{ fill: '#666', fontSize: 11 }}
759
+ tickFormatter={formatNumber}
760
+ />
761
+ <Tooltip content={<CustomTooltip />} />
762
+ <Legend wrapperStyle={{ paddingTop: 20 }} />
763
+ {yAxis.map((field, index) => (
764
+ <Bar
765
+ key={field}
766
+ dataKey={field}
767
+ fill={CHART_COLORS[index % CHART_COLORS.length]}
768
+ radius={[4, 4, 0, 0]}
769
+ />
770
+ ))}
771
+ </BarChart>
772
+ ) : chartType === 'line' ? (
773
+ <LineChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
774
+ <CartesianGrid strokeDasharray="3 3" stroke="#222" />
775
+ <XAxis
776
+ dataKey={xAxis}
777
+ tick={{ fill: '#666', fontSize: 11 }}
778
+ angle={-45}
779
+ textAnchor="end"
780
+ height={60}
781
+ />
782
+ <YAxis
783
+ tick={{ fill: '#666', fontSize: 11 }}
784
+ tickFormatter={formatNumber}
785
+ />
786
+ <Tooltip content={<CustomTooltip />} />
787
+ <Legend wrapperStyle={{ paddingTop: 20 }} />
788
+ {yAxis.map((field, index) => (
789
+ <Line
790
+ key={field}
791
+ type="monotone"
792
+ dataKey={field}
793
+ stroke={CHART_COLORS[index % CHART_COLORS.length]}
794
+ strokeWidth={2}
795
+ dot={{ fill: CHART_COLORS[index % CHART_COLORS.length], strokeWidth: 0, r: 4 }}
796
+ activeDot={{ r: 6, strokeWidth: 0 }}
797
+ />
798
+ ))}
799
+ </LineChart>
800
+ ) : chartType === 'area' ? (
801
+ <AreaChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
802
+ <CartesianGrid strokeDasharray="3 3" stroke="#222" />
803
+ <XAxis
804
+ dataKey={xAxis}
805
+ tick={{ fill: '#666', fontSize: 11 }}
806
+ angle={-45}
807
+ textAnchor="end"
808
+ height={60}
809
+ />
810
+ <YAxis
811
+ tick={{ fill: '#666', fontSize: 11 }}
812
+ tickFormatter={formatNumber}
813
+ />
814
+ <Tooltip content={<CustomTooltip />} />
815
+ <Legend wrapperStyle={{ paddingTop: 20 }} />
816
+ {yAxis.map((field, index) => (
817
+ <Area
818
+ key={field}
819
+ type="monotone"
820
+ dataKey={field}
821
+ stroke={CHART_COLORS[index % CHART_COLORS.length]}
822
+ fill={CHART_COLORS[index % CHART_COLORS.length]}
823
+ fillOpacity={0.3}
824
+ strokeWidth={2}
825
+ />
826
+ ))}
827
+ </AreaChart>
828
+ ) : chartType === 'scatter' ? (
829
+ <ScatterChart margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
830
+ <CartesianGrid strokeDasharray="3 3" stroke="#222" />
831
+ <XAxis
832
+ dataKey={xAxis}
833
+ type="number"
834
+ tick={{ fill: '#666', fontSize: 11 }}
835
+ name={xAxis}
836
+ label={{ value: xAxis, position: 'bottom', fill: '#666', fontSize: 11 }}
837
+ />
838
+ <YAxis
839
+ dataKey={scatterY}
840
+ type="number"
841
+ tick={{ fill: '#666', fontSize: 11 }}
842
+ name={scatterY}
843
+ label={{ value: scatterY, angle: -90, position: 'insideLeft', fill: '#666', fontSize: 11 }}
844
+ />
845
+ <ZAxis range={[40, 200]} />
846
+ <Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3' }} />
847
+ <Scatter
848
+ name={`${xAxis} vs ${scatterY}`}
849
+ data={chartData}
850
+ fill={CHART_COLORS[0]}
851
+ shape="circle"
852
+ />
853
+ </ScatterChart>
854
+ ) : chartType === 'histogram' ? (
855
+ <BarChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
856
+ <CartesianGrid strokeDasharray="3 3" stroke="#222" />
857
+ <XAxis
858
+ dataKey="range"
859
+ tick={{ fill: '#666', fontSize: 10 }}
860
+ angle={-45}
861
+ textAnchor="end"
862
+ height={60}
863
+ />
864
+ <YAxis
865
+ tick={{ fill: '#666', fontSize: 11 }}
866
+ label={{ value: 'Count', angle: -90, position: 'insideLeft', fill: '#666', fontSize: 11 }}
867
+ />
868
+ <Tooltip content={<CustomTooltip />} />
869
+ <Bar dataKey="count" fill={CHART_COLORS[0]} radius={[4, 4, 0, 0]} />
870
+ </BarChart>
871
+ ) : chartType === 'stacked-bar' ? (
872
+ <BarChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
873
+ <CartesianGrid strokeDasharray="3 3" stroke="#222" />
874
+ <XAxis
875
+ dataKey={xAxis}
876
+ tick={{ fill: '#666', fontSize: 11 }}
877
+ angle={-45}
878
+ textAnchor="end"
879
+ height={60}
880
+ />
881
+ <YAxis
882
+ tick={{ fill: '#666', fontSize: 11 }}
883
+ tickFormatter={formatNumber}
884
+ />
885
+ <Tooltip content={<CustomTooltip />} />
886
+ <Legend wrapperStyle={{ paddingTop: 20 }} />
887
+ {yAxis.map((field, index) => (
888
+ <Bar
889
+ key={field}
890
+ dataKey={field}
891
+ stackId="stack"
892
+ fill={CHART_COLORS[index % CHART_COLORS.length]}
893
+ />
894
+ ))}
895
+ </BarChart>
896
+ ) : chartType === 'stacked-area' ? (
897
+ <AreaChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
898
+ <CartesianGrid strokeDasharray="3 3" stroke="#222" />
899
+ <XAxis
900
+ dataKey={xAxis}
901
+ tick={{ fill: '#666', fontSize: 11 }}
902
+ angle={-45}
903
+ textAnchor="end"
904
+ height={60}
905
+ />
906
+ <YAxis
907
+ tick={{ fill: '#666', fontSize: 11 }}
908
+ tickFormatter={formatNumber}
909
+ />
910
+ <Tooltip content={<CustomTooltip />} />
911
+ <Legend wrapperStyle={{ paddingTop: 20 }} />
912
+ {yAxis.map((field, index) => (
913
+ <Area
914
+ key={field}
915
+ type="monotone"
916
+ dataKey={field}
917
+ stackId="stack"
918
+ stroke={CHART_COLORS[index % CHART_COLORS.length]}
919
+ fill={CHART_COLORS[index % CHART_COLORS.length]}
920
+ fillOpacity={0.5}
921
+ />
922
+ ))}
923
+ </AreaChart>
924
+ ) : (
925
+ <PieChart margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
926
+ <Pie
927
+ data={chartData.slice(0, 10)}
928
+ dataKey={yAxis[0]}
929
+ nameKey={xAxis}
930
+ cx="50%"
931
+ cy="50%"
932
+ outerRadius="70%"
933
+ label={({ name, percent }) => `${name} (${(percent * 100).toFixed(0)}%)`}
934
+ labelLine={{ stroke: '#444' }}
935
+ >
936
+ {chartData.slice(0, 10).map((_entry, index) => (
937
+ <Cell
938
+ key={`cell-${index}`}
939
+ fill={CHART_COLORS[index % CHART_COLORS.length]}
940
+ />
941
+ ))}
942
+ </Pie>
943
+ <Tooltip content={<CustomTooltip />} />
944
+ <Legend />
945
+ </PieChart>
946
+ )}
947
+ </ResponsiveContainer>
948
+ )}
949
+ </div>
950
+
951
+ {/* Footer Stats */}
952
+ <div className="px-3 py-2 border-t border-white/5 bg-[#0a0a0a] flex items-center gap-4 text-[10px] text-zinc-600">
953
+ <span>Rows: <span className="text-zinc-400 font-mono">{result?.rows.length || 0}</span></span>
954
+ <span>Fields: <span className="text-zinc-400 font-mono">{analysis.fields.length}</span></span>
955
+ <span>Numeric: <span className="text-zinc-400 font-mono">{analysis.numericFields.length}</span></span>
956
+ {chartType === 'pie' && chartData.length > 10 && (
957
+ <span className="text-amber-500">Showing top 10 values</span>
958
+ )}
959
+ </div>
960
+ </div>
961
+ );
962
+ }