@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,820 @@
1
+ "use client";
2
+
3
+ import React, { useMemo, useState, useCallback, useRef } from 'react';
4
+ import {
5
+ Zap,
6
+ Search,
7
+ ArrowDown,
8
+ Layers,
9
+ Database,
10
+ Clock,
11
+ LayoutGrid,
12
+ AlertTriangle,
13
+ CheckCircle2,
14
+ TrendingUp,
15
+ HardDrive,
16
+ Target,
17
+ ChevronRight,
18
+ Info,
19
+ FileJson,
20
+ Activity,
21
+ Sparkles,
22
+ Play,
23
+ Loader2,
24
+ } from 'lucide-react';
25
+ import { cn } from '@/lib/utils';
26
+
27
+ type ExplainPlanNode = {
28
+ Plan?: ExplainPlanNode;
29
+ 'Node Type'?: string;
30
+ 'Actual Rows'?: number;
31
+ 'Plan Rows'?: number;
32
+ 'Actual Total Time'?: number;
33
+ 'Total Cost'?: number;
34
+ 'Shared Hit Blocks'?: number;
35
+ 'Shared Read Blocks'?: number;
36
+ 'Relation Name'?: string;
37
+ 'Actual Loops'?: number;
38
+ Filter?: string;
39
+ 'Index Name'?: string;
40
+ Plans?: ExplainPlanNode[];
41
+ };
42
+
43
+ export type ExplainPlanResult = {
44
+ Plan?: ExplainPlanNode;
45
+ 'Execution Time'?: number;
46
+ 'Planning Time'?: number;
47
+ };
48
+
49
+ interface VisualExplainProps {
50
+ plan: ExplainPlanResult[] | null | undefined;
51
+ query?: string;
52
+ schemaContext?: string;
53
+ databaseType?: string;
54
+ onLoadQuery?: (query: string) => void;
55
+ }
56
+
57
+ // ============================================================================
58
+ // Helper Functions
59
+ // ============================================================================
60
+
61
+ function formatNumber(num: number): string {
62
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
63
+ if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
64
+ return num.toFixed(0);
65
+ }
66
+
67
+
68
+ function formatTime(ms: number): string {
69
+ if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
70
+ if (ms >= 1) return `${ms.toFixed(2)}ms`;
71
+ return `${(ms * 1000).toFixed(0)}μs`;
72
+ }
73
+
74
+ // ============================================================================
75
+ // Analysis Functions
76
+ // ============================================================================
77
+
78
+ interface PlanAnalysis {
79
+ totalTime: number;
80
+ planningTime: number;
81
+ executionTime: number;
82
+ totalRows: number;
83
+ totalCost: number;
84
+ bufferHits: number;
85
+ bufferReads: number;
86
+ nodeCount: number;
87
+ warnings: Warning[];
88
+ insights: Insight[];
89
+ }
90
+
91
+ interface Warning {
92
+ type: 'critical' | 'warning' | 'info';
93
+ title: string;
94
+ description: string;
95
+ node?: string;
96
+ }
97
+
98
+ interface Insight {
99
+ label: string;
100
+ value: string;
101
+ status: 'good' | 'warning' | 'critical';
102
+ }
103
+
104
+ function analyzePlan(plan: ExplainPlanResult[]): PlanAnalysis {
105
+ const warnings: Warning[] = [];
106
+ const insights: Insight[] = [];
107
+ let totalRows = 0;
108
+ let nodeCount = 0;
109
+ let bufferHits = 0;
110
+ let bufferReads = 0;
111
+
112
+ const rootPlan = plan?.[0]?.Plan;
113
+ const executionTime = plan?.[0]?.['Execution Time'] || rootPlan?.['Actual Total Time'] || 0;
114
+ const planningTime = plan?.[0]?.['Planning Time'] || 0;
115
+ const totalCost = rootPlan?.['Total Cost'] || 0;
116
+
117
+ // Recursive node analysis
118
+ function analyzeNode(node: ExplainPlanNode, depth: number = 0) {
119
+ if (!node) return;
120
+ nodeCount++;
121
+
122
+ const nodeType = node['Node Type'] || '';
123
+ const actualRows = node['Actual Rows'] || 0;
124
+ const planRows = node['Plan Rows'] || 0;
125
+ const actualTime = node['Actual Total Time'] || 0;
126
+
127
+ totalRows += actualRows;
128
+ bufferHits += node['Shared Hit Blocks'] || 0;
129
+ bufferReads += node['Shared Read Blocks'] || 0;
130
+
131
+ // Check for Sequential Scan on large tables
132
+ if (nodeType.includes('Seq Scan') && actualRows > 10000) {
133
+ warnings.push({
134
+ type: 'warning',
135
+ title: 'Sequential Scan',
136
+ description: `Full table scan on "${node['Relation Name'] || 'table'}" (${formatNumber(actualRows)} rows). Consider adding an index.`,
137
+ node: nodeType,
138
+ });
139
+ }
140
+
141
+ // Check for row estimate mismatch
142
+ if (planRows > 0 && actualRows > 0) {
143
+ const ratio = actualRows / planRows;
144
+ if (ratio > 10 || ratio < 0.1) {
145
+ warnings.push({
146
+ type: 'info',
147
+ title: 'Estimate Mismatch',
148
+ description: `Expected ${formatNumber(planRows)} rows, got ${formatNumber(actualRows)}. Statistics may be outdated.`,
149
+ node: nodeType,
150
+ });
151
+ }
152
+ }
153
+
154
+ // Check for expensive sorts
155
+ if (nodeType.includes('Sort') && actualTime > 100) {
156
+ warnings.push({
157
+ type: 'warning',
158
+ title: 'Expensive Sort',
159
+ description: `Sort operation took ${formatTime(actualTime)}. Consider adding an index for ordered access.`,
160
+ node: nodeType,
161
+ });
162
+ }
163
+
164
+ // Check for nested loops with high iterations
165
+ const actualLoops = node['Actual Loops'] ?? 1;
166
+ if (nodeType.includes('Nested Loop') && actualLoops > 1000) {
167
+ warnings.push({
168
+ type: 'critical',
169
+ title: 'High Loop Count',
170
+ description: `Nested loop executed ${formatNumber(actualLoops)} times. This could indicate an N+1 problem.`,
171
+ node: nodeType,
172
+ });
173
+ }
174
+
175
+ // Recurse into children
176
+ (node['Plans'] || []).forEach((child) => analyzeNode(child, depth + 1));
177
+ }
178
+
179
+ if (rootPlan) {
180
+ analyzeNode(rootPlan);
181
+ }
182
+
183
+ // Build insights
184
+ insights.push({
185
+ label: 'Cache Hit Rate',
186
+ value: bufferHits + bufferReads > 0
187
+ ? `${((bufferHits / (bufferHits + bufferReads)) * 100).toFixed(1)}%`
188
+ : 'N/A',
189
+ status: bufferHits / (bufferHits + bufferReads || 1) > 0.95 ? 'good' : 'warning',
190
+ });
191
+
192
+ insights.push({
193
+ label: 'Operations',
194
+ value: nodeCount.toString(),
195
+ status: nodeCount > 20 ? 'warning' : 'good',
196
+ });
197
+
198
+ insights.push({
199
+ label: 'Execution',
200
+ value: formatTime(executionTime),
201
+ status: executionTime > 1000 ? 'critical' : executionTime > 100 ? 'warning' : 'good',
202
+ });
203
+
204
+ return {
205
+ totalTime: executionTime + planningTime,
206
+ planningTime,
207
+ executionTime,
208
+ totalRows,
209
+ totalCost,
210
+ bufferHits,
211
+ bufferReads,
212
+ nodeCount,
213
+ warnings,
214
+ insights,
215
+ };
216
+ }
217
+
218
+ // ============================================================================
219
+ // Components
220
+ // ============================================================================
221
+
222
+ const NodeIcon = ({ type }: { type: string }) => {
223
+ if (type.includes('Seq Scan')) return <Search className="w-4 h-4 text-amber-400" />;
224
+ if (type.includes('Index Scan') || type.includes('Index Only')) return <Target className="w-4 h-4 text-emerald-400" />;
225
+ if (type.includes('Scan')) return <Search className="w-4 h-4 text-blue-400" />;
226
+ if (type.includes('Join')) return <Layers className="w-4 h-4 text-purple-400" />;
227
+ if (type.includes('Sort')) return <ArrowDown className="w-4 h-4 text-amber-400" />;
228
+ if (type.includes('Limit')) return <LayoutGrid className="w-4 h-4 text-zinc-400" />;
229
+ if (type.includes('Aggregate') || type.includes('Group')) return <Zap className="w-4 h-4 text-pink-400" />;
230
+ if (type.includes('Hash')) return <HardDrive className="w-4 h-4 text-cyan-400" />;
231
+ return <Database className="w-4 h-4 text-zinc-500" />;
232
+ };
233
+
234
+ const StatusBadge = ({ status }: { status: 'good' | 'warning' | 'critical' }) => {
235
+ return <div className={cn('w-2 h-2 rounded-full', status === 'good' ? 'bg-emerald-500' : status === 'warning' ? 'bg-amber-500' : 'bg-red-500')} />;
236
+ };
237
+
238
+ // Compact Plan Node
239
+ const PlanNode = ({ node, depth = 0, maxTime }: { node: ExplainPlanNode; depth?: number; maxTime: number }) => {
240
+ const [expanded, setExpanded] = useState(depth < 2);
241
+ const nodeType = node['Node Type'] || 'Unknown';
242
+ const actualTime = node['Actual Total Time'] || 0;
243
+ const actualRows = node['Actual Rows'] || 0;
244
+ const children = node['Plans'] || [];
245
+ const isIndexScan = nodeType.includes('Index');
246
+ const isSeqScan = nodeType.includes('Seq Scan');
247
+
248
+ const timePercent = maxTime > 0 ? (actualTime / maxTime) * 100 : 0;
249
+
250
+ return (
251
+ <div className="relative">
252
+ {/* Node */}
253
+ <div
254
+ className={cn(
255
+ "group flex items-center gap-2 py-1.5 px-2 rounded-lg transition-all cursor-pointer hover:bg-white/5",
256
+ depth === 0 && "bg-white/[0.02]"
257
+ )}
258
+ onClick={() => setExpanded(!expanded)}
259
+ style={{ marginLeft: depth * 20 }}
260
+ >
261
+ {/* Expand icon */}
262
+ {children.length > 0 && (
263
+ <ChevronRight className={cn("w-3 h-3 text-zinc-600 transition-transform", expanded && "rotate-90")} />
264
+ )}
265
+ {children.length === 0 && <div className="w-3" />}
266
+
267
+ {/* Icon */}
268
+ <div className={cn(
269
+ "p-1 rounded",
270
+ isSeqScan ? "bg-amber-500/10" : isIndexScan ? "bg-emerald-500/10" : "bg-white/5"
271
+ )}>
272
+ <NodeIcon type={nodeType} />
273
+ </div>
274
+
275
+ {/* Type & Table */}
276
+ <div className="flex-1 min-w-0">
277
+ <div className="flex items-center gap-2">
278
+ <span className="text-[11px] font-medium text-zinc-200 truncate">{nodeType}</span>
279
+ {node['Relation Name'] && (
280
+ <span className="text-[10px] text-zinc-500 font-mono truncate">{node['Relation Name']}</span>
281
+ )}
282
+ </div>
283
+ </div>
284
+
285
+ {/* Stats */}
286
+ <div className="flex items-center gap-4 text-[10px] font-mono">
287
+ <span className="text-zinc-500 w-16 text-right">{formatNumber(actualRows)} rows</span>
288
+ <span className={cn(
289
+ "w-16 text-right",
290
+ timePercent > 50 ? "text-red-400" : timePercent > 20 ? "text-amber-400" : "text-zinc-400"
291
+ )}>
292
+ {formatTime(actualTime)}
293
+ </span>
294
+ {/* Time bar */}
295
+ <div className="w-20 h-1.5 bg-white/5 rounded-full overflow-hidden">
296
+ <div
297
+ className={cn(
298
+ "h-full rounded-full transition-all",
299
+ timePercent > 50 ? "bg-red-500" : timePercent > 20 ? "bg-amber-500" : "bg-blue-500"
300
+ )}
301
+ style={{ width: `${Math.min(timePercent, 100)}%` }}
302
+ />
303
+ </div>
304
+ </div>
305
+ </div>
306
+
307
+ {/* Details on expand */}
308
+ {expanded && (
309
+ <div className="ml-8 pl-4 border-l border-white/5" style={{ marginLeft: depth * 20 + 32 }}>
310
+ {/* Filter info */}
311
+ {node['Filter'] && (
312
+ <div className="flex items-start gap-2 py-1 text-[10px]">
313
+ <span className="text-amber-500/70 font-medium shrink-0">Filter:</span>
314
+ <span className="text-zinc-500 font-mono break-all">{node['Filter']}</span>
315
+ </div>
316
+ )}
317
+ {/* Index info */}
318
+ {node['Index Name'] && (
319
+ <div className="flex items-center gap-2 py-1 text-[10px]">
320
+ <span className="text-emerald-500/70 font-medium">Index:</span>
321
+ <span className="text-emerald-400 font-mono">{node['Index Name']}</span>
322
+ </div>
323
+ )}
324
+ {/* Buffer stats */}
325
+ {((node['Shared Hit Blocks'] ?? 0) > 0 || (node['Shared Read Blocks'] ?? 0) > 0) && (
326
+ <div className="flex items-center gap-4 py-1 text-[10px] text-zinc-600">
327
+ {(node['Shared Hit Blocks'] ?? 0) > 0 && <span>Cache hits: {node['Shared Hit Blocks']}</span>}
328
+ {(node['Shared Read Blocks'] ?? 0) > 0 && <span>Disk reads: {node['Shared Read Blocks']}</span>}
329
+ </div>
330
+ )}
331
+
332
+ {/* Children */}
333
+ {children.map((child, idx) => (
334
+ <PlanNode key={idx} node={child} depth={depth + 1} maxTime={maxTime} />
335
+ ))}
336
+ </div>
337
+ )}
338
+ </div>
339
+ );
340
+ };
341
+
342
+ // ============================================================================
343
+ // AI Explain Tab Component
344
+ // ============================================================================
345
+
346
+ function AIExplainTab({
347
+ plan,
348
+ query,
349
+ schemaContext,
350
+ databaseType,
351
+ onLoadQuery,
352
+ }: {
353
+ plan: ExplainPlanResult[];
354
+ query?: string;
355
+ schemaContext?: string;
356
+ databaseType?: string;
357
+ onLoadQuery?: (query: string) => void;
358
+ }) {
359
+ const [aiResponse, setAiResponse] = useState('');
360
+ const [isLoading, setIsLoading] = useState(false);
361
+ const [error, setError] = useState<string | null>(null);
362
+ const [hasRun, setHasRun] = useState(false);
363
+ const abortControllerRef = useRef<AbortController | null>(null);
364
+
365
+ const analyzeWithAI = useCallback(async () => {
366
+ if (!query && !plan) return;
367
+
368
+ setIsLoading(true);
369
+ setAiResponse('');
370
+ setError(null);
371
+ setHasRun(true);
372
+
373
+ // Abort previous request if any
374
+ if (abortControllerRef.current) {
375
+ abortControllerRef.current.abort();
376
+ }
377
+ const abortController = new AbortController();
378
+ abortControllerRef.current = abortController;
379
+
380
+ try {
381
+ const response = await fetch('/api/ai/explain', {
382
+ method: 'POST',
383
+ headers: { 'Content-Type': 'application/json' },
384
+ body: JSON.stringify({
385
+ query: query || 'Unknown query',
386
+ explainPlan: plan,
387
+ schemaContext: schemaContext || '',
388
+ databaseType: databaseType || 'postgres',
389
+ }),
390
+ signal: abortController.signal,
391
+ });
392
+
393
+ if (!response.ok) {
394
+ const errData = await response.json().catch(() => ({}));
395
+ throw new Error(errData.error || 'AI analysis failed');
396
+ }
397
+
398
+ const reader = response.body?.getReader();
399
+ if (!reader) throw new Error('No response body');
400
+
401
+ const decoder = new TextDecoder();
402
+ let accumulated = '';
403
+
404
+ while (true) {
405
+ const { done, value } = await reader.read();
406
+ if (done) break;
407
+
408
+ const chunk = decoder.decode(value, { stream: true });
409
+ accumulated += chunk;
410
+ setAiResponse(accumulated);
411
+ }
412
+ } catch (err) {
413
+ if (err instanceof Error && err.name === 'AbortError') return;
414
+ setError(err instanceof Error ? err.message : 'AI analysis failed');
415
+ } finally {
416
+ setIsLoading(false);
417
+ }
418
+ }, [query, plan, schemaContext, databaseType]);
419
+
420
+
421
+ // Simple markdown renderer for the AI response
422
+ const renderMarkdown = (text: string) => {
423
+ const lines = text.split('\n');
424
+ const elements: React.ReactNode[] = [];
425
+ let inCodeBlock = false;
426
+ let codeBlockLang = '';
427
+ let codeBlockContent = '';
428
+
429
+ lines.forEach((line, idx) => {
430
+ if (line.startsWith('```')) {
431
+ if (inCodeBlock) {
432
+ // End code block
433
+ const content = codeBlockContent;
434
+ const isSql = codeBlockLang === 'sql';
435
+ elements.push(
436
+ <div key={`code-${idx}`} className="my-3 relative group/code">
437
+ <pre className={cn(
438
+ "text-[11px] font-mono p-3 rounded-lg overflow-x-auto border",
439
+ isSql ? "bg-blue-500/5 border-blue-500/10 text-blue-300" : "bg-white/[0.02] border-white/5 text-zinc-400"
440
+ )}>
441
+ {content}
442
+ </pre>
443
+ {isSql && onLoadQuery && (
444
+ <button
445
+ onClick={() => onLoadQuery(content)}
446
+ className="absolute top-2 right-2 opacity-0 group-hover/code:opacity-100 transition-opacity px-2 py-1 rounded bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-bold flex items-center gap-1"
447
+ >
448
+ <Play className="w-3 h-3" /> Try This
449
+ </button>
450
+ )}
451
+ </div>
452
+ );
453
+ codeBlockContent = '';
454
+ inCodeBlock = false;
455
+ } else {
456
+ // Start code block
457
+ inCodeBlock = true;
458
+ codeBlockLang = line.slice(3).trim();
459
+ codeBlockContent = '';
460
+ }
461
+ return;
462
+ }
463
+
464
+ if (inCodeBlock) {
465
+ codeBlockContent += (codeBlockContent ? '\n' : '') + line;
466
+ return;
467
+ }
468
+
469
+ // Headers
470
+ if (line.startsWith('## ')) {
471
+ elements.push(
472
+ <h2 key={idx} className="text-[13px] font-bold text-zinc-200 mt-4 mb-2 flex items-center gap-2">
473
+ {line.slice(3)}
474
+ </h2>
475
+ );
476
+ } else if (line.startsWith('### ')) {
477
+ elements.push(
478
+ <h3 key={idx} className="text-[12px] font-semibold text-zinc-300 mt-3 mb-1">
479
+ {line.slice(4)}
480
+ </h3>
481
+ );
482
+ } else if (line.startsWith('- ')) {
483
+ elements.push(
484
+ <div key={idx} className="flex items-start gap-2 text-[11px] text-zinc-400 leading-relaxed ml-2 my-0.5">
485
+ <span className="text-zinc-600 mt-1 shrink-0">•</span>
486
+ <span>{renderInlineFormatting(line.slice(2))}</span>
487
+ </div>
488
+ );
489
+ } else if (/^\d+\.\s/.test(line)) {
490
+ const num = line.match(/^(\d+)\./)?.[1];
491
+ elements.push(
492
+ <div key={idx} className="flex items-start gap-2 text-[11px] text-zinc-400 leading-relaxed ml-2 my-0.5">
493
+ <span className="text-blue-400 font-bold mt-0 shrink-0 w-4">{num}.</span>
494
+ <span>{renderInlineFormatting(line.replace(/^\d+\.\s*/, ''))}</span>
495
+ </div>
496
+ );
497
+ } else if (line.trim() === '') {
498
+ elements.push(<div key={idx} className="h-1" />);
499
+ } else {
500
+ elements.push(
501
+ <p key={idx} className="text-[11px] text-zinc-400 leading-relaxed my-0.5">
502
+ {renderInlineFormatting(line)}
503
+ </p>
504
+ );
505
+ }
506
+ });
507
+
508
+ return elements;
509
+ };
510
+
511
+ const renderInlineFormatting = (text: string): React.ReactNode => {
512
+ // Bold **text**
513
+ const parts = text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g);
514
+ return parts.map((part, i) => {
515
+ if (part.startsWith('**') && part.endsWith('**')) {
516
+ return <strong key={i} className="text-zinc-200 font-medium">{part.slice(2, -2)}</strong>;
517
+ }
518
+ if (part.startsWith('`') && part.endsWith('`')) {
519
+ return <code key={i} className="text-blue-400 bg-blue-500/10 px-1 rounded text-[10px] font-mono">{part.slice(1, -1)}</code>;
520
+ }
521
+ return part;
522
+ });
523
+ };
524
+
525
+ // Not run yet state
526
+ if (!hasRun) {
527
+ return (
528
+ <div className="h-full flex flex-col items-center justify-center p-8 text-center">
529
+ <div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-500/20 to-blue-500/10 flex items-center justify-center mb-4">
530
+ <Sparkles className="w-7 h-7 text-purple-400" />
531
+ </div>
532
+ <h3 className="text-sm font-semibold text-zinc-200 mb-1">AI Query Analysis</h3>
533
+ <p className="text-[11px] text-zinc-500 max-w-[280px] leading-relaxed mb-4">
534
+ Get a plain-language explanation of your query&apos;s execution plan with concrete optimization suggestions.
535
+ </p>
536
+ <button
537
+ onClick={analyzeWithAI}
538
+ disabled={!query}
539
+ className={cn(
540
+ "flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all",
541
+ query
542
+ ? "bg-purple-600 hover:bg-purple-500 text-white shadow-lg shadow-purple-900/20"
543
+ : "bg-white/5 text-zinc-600 cursor-not-allowed"
544
+ )}
545
+ >
546
+ <Sparkles className="w-3.5 h-3.5" />
547
+ Analyze with AI
548
+ </button>
549
+ {!query && (
550
+ <p className="text-[10px] text-zinc-600 mt-2">Run a query first to enable AI analysis.</p>
551
+ )}
552
+ </div>
553
+ );
554
+ }
555
+
556
+ return (
557
+ <div className="h-full flex flex-col">
558
+ {/* Re-analyze button */}
559
+ <div className="flex items-center justify-between px-4 py-2 border-b border-white/5 bg-[#0a0a0a]">
560
+ <div className="flex items-center gap-2">
561
+ <Sparkles className="w-3.5 h-3.5 text-purple-400" />
562
+ <span className="text-[10px] font-bold text-purple-400 uppercase tracking-wider">AI Analysis</span>
563
+ </div>
564
+ <button
565
+ onClick={analyzeWithAI}
566
+ disabled={isLoading}
567
+ className="flex items-center gap-1.5 px-2 py-1 rounded-md text-[10px] font-bold text-zinc-400 hover:text-white hover:bg-white/5 transition-all"
568
+ >
569
+ {isLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <Sparkles className="w-3 h-3" />}
570
+ {isLoading ? 'Analyzing...' : 'Re-analyze'}
571
+ </button>
572
+ </div>
573
+
574
+ {/* Content */}
575
+ <div className="flex-1 overflow-auto p-4">
576
+ {error && (
577
+ <div className="flex items-center gap-2 p-3 rounded-lg bg-red-500/5 border border-red-500/10 text-red-400 text-xs mb-4">
578
+ <AlertTriangle className="w-4 h-4 shrink-0" />
579
+ {error}
580
+ </div>
581
+ )}
582
+
583
+ {aiResponse && (
584
+ <div className="space-y-0">
585
+ {renderMarkdown(aiResponse)}
586
+ </div>
587
+ )}
588
+
589
+ {isLoading && !aiResponse && (
590
+ <div className="flex items-center gap-3 text-zinc-500 text-sm">
591
+ <Loader2 className="w-4 h-4 animate-spin text-purple-400" />
592
+ <span>Analyzing execution plan...</span>
593
+ </div>
594
+ )}
595
+
596
+ {isLoading && aiResponse && (
597
+ <div className="flex items-center gap-2 mt-2 text-zinc-600 text-[10px]">
598
+ <Loader2 className="w-3 h-3 animate-spin" />
599
+ <span>Still generating...</span>
600
+ </div>
601
+ )}
602
+ </div>
603
+ </div>
604
+ );
605
+ }
606
+
607
+ // ============================================================================
608
+ // Main Component
609
+ // ============================================================================
610
+
611
+ export function VisualExplain({ plan, query, schemaContext, databaseType, onLoadQuery }: VisualExplainProps) {
612
+ const [activeTab, setActiveTab] = useState<'insights' | 'tree' | 'raw' | 'ai'>('insights');
613
+
614
+ const analysis = useMemo(() => {
615
+ if (!plan || !Array.isArray(plan) || plan.length === 0) return null;
616
+ return analyzePlan(plan);
617
+ }, [plan]);
618
+
619
+ // Empty state
620
+ if (!plan || !Array.isArray(plan) || plan.length === 0) {
621
+ return (
622
+ <div className="h-full flex flex-col items-center justify-center text-zinc-500 bg-[#080808] p-12 text-center">
623
+ <div className="w-12 h-12 rounded-xl bg-white/5 flex items-center justify-center mb-4">
624
+ <Activity className="w-6 h-6 text-zinc-600" />
625
+ </div>
626
+ <h3 className="text-sm font-medium text-zinc-300 mb-1">No execution plan</h3>
627
+ <p className="text-xs text-zinc-600 max-w-[240px]">
628
+ Run a SELECT query to see its execution plan and performance insights.
629
+ </p>
630
+ </div>
631
+ );
632
+ }
633
+
634
+ const rootPlan = plan[0]?.Plan;
635
+
636
+ return (
637
+ <div className="h-full flex flex-col bg-[#080808]">
638
+ {/* Header Stats */}
639
+ <div className="px-4 py-3 border-b border-white/5 bg-[#0a0a0a]">
640
+ <div className="flex items-center justify-between">
641
+ {/* Quick stats */}
642
+ <div className="flex items-center gap-6">
643
+ <div className="flex items-center gap-2">
644
+ <Clock className="w-3.5 h-3.5 text-blue-400" />
645
+ <span className="text-[13px] font-medium text-zinc-200">
646
+ {formatTime(analysis?.executionTime || 0)}
647
+ </span>
648
+ <span className="text-[10px] text-zinc-600">execution</span>
649
+ </div>
650
+ <div className="flex items-center gap-2">
651
+ <TrendingUp className="w-3.5 h-3.5 text-zinc-500" />
652
+ <span className="text-[13px] font-medium text-zinc-400">
653
+ {formatNumber(analysis?.totalRows || 0)}
654
+ </span>
655
+ <span className="text-[10px] text-zinc-600">rows</span>
656
+ </div>
657
+ <div className="flex items-center gap-2">
658
+ <HardDrive className="w-3.5 h-3.5 text-zinc-500" />
659
+ <span className="text-[13px] font-medium text-zinc-400">
660
+ {formatNumber(analysis?.totalCost || 0)}
661
+ </span>
662
+ <span className="text-[10px] text-zinc-600">cost</span>
663
+ </div>
664
+ </div>
665
+
666
+ {/* Tabs */}
667
+ <div className="flex items-center gap-1 bg-white/5 rounded-lg p-0.5">
668
+ {(['insights', 'ai', 'tree', 'raw'] as const).map((tab) => (
669
+ <button
670
+ key={tab}
671
+ onClick={() => setActiveTab(tab)}
672
+ className={cn(
673
+ "px-3 py-1 text-[10px] font-medium rounded-md transition-all uppercase tracking-wide",
674
+ activeTab === tab
675
+ ? tab === 'ai' ? "bg-purple-500/20 text-purple-300" : "bg-white/10 text-zinc-200"
676
+ : "text-zinc-500 hover:text-zinc-300"
677
+ )}
678
+ >
679
+ {tab === 'insights' && <Zap className="w-3 h-3 inline mr-1" />}
680
+ {tab === 'ai' && <Sparkles className="w-3 h-3 inline mr-1" />}
681
+ {tab === 'tree' && <Layers className="w-3 h-3 inline mr-1" />}
682
+ {tab === 'raw' && <FileJson className="w-3 h-3 inline mr-1" />}
683
+ {tab === 'ai' ? 'AI Explain' : tab}
684
+ </button>
685
+ ))}
686
+ </div>
687
+ </div>
688
+ </div>
689
+
690
+ {/* Content */}
691
+ <div className="flex-1 overflow-auto">
692
+ {activeTab === 'ai' && (
693
+ <AIExplainTab
694
+ plan={plan}
695
+ query={query}
696
+ schemaContext={schemaContext}
697
+ databaseType={databaseType}
698
+ onLoadQuery={onLoadQuery}
699
+ />
700
+ )}
701
+
702
+ {activeTab === 'insights' && (
703
+ <div className="p-4 space-y-4">
704
+ {/* Warnings */}
705
+ {analysis && analysis.warnings.length > 0 && (
706
+ <div className="space-y-2">
707
+ <h3 className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider mb-2">
708
+ Performance Issues
709
+ </h3>
710
+ {analysis.warnings.map((warning, idx) => (
711
+ <div
712
+ key={idx}
713
+ className={cn(
714
+ "flex items-start gap-3 p-3 rounded-lg border",
715
+ warning.type === 'critical'
716
+ ? "bg-red-500/5 border-red-500/10"
717
+ : warning.type === 'warning'
718
+ ? "bg-amber-500/5 border-amber-500/10"
719
+ : "bg-blue-500/5 border-blue-500/10"
720
+ )}
721
+ >
722
+ <div className={cn(
723
+ "p-1 rounded",
724
+ warning.type === 'critical'
725
+ ? "bg-red-500/10"
726
+ : warning.type === 'warning'
727
+ ? "bg-amber-500/10"
728
+ : "bg-blue-500/10"
729
+ )}>
730
+ {warning.type === 'critical' ? (
731
+ <AlertTriangle className="w-3.5 h-3.5 text-red-400" />
732
+ ) : warning.type === 'warning' ? (
733
+ <AlertTriangle className="w-3.5 h-3.5 text-amber-400" />
734
+ ) : (
735
+ <Info className="w-3.5 h-3.5 text-blue-400" />
736
+ )}
737
+ </div>
738
+ <div className="flex-1 min-w-0">
739
+ <h4 className={cn(
740
+ "text-[11px] font-medium",
741
+ warning.type === 'critical'
742
+ ? "text-red-300"
743
+ : warning.type === 'warning'
744
+ ? "text-amber-300"
745
+ : "text-blue-300"
746
+ )}>
747
+ {warning.title}
748
+ </h4>
749
+ <p className="text-[10px] text-zinc-500 mt-0.5 leading-relaxed">
750
+ {warning.description}
751
+ </p>
752
+ </div>
753
+ </div>
754
+ ))}
755
+ </div>
756
+ )}
757
+
758
+ {/* No warnings */}
759
+ {analysis && analysis.warnings.length === 0 && (
760
+ <div className="flex items-center gap-3 p-3 rounded-lg bg-emerald-500/5 border border-emerald-500/10">
761
+ <div className="p-1 rounded bg-emerald-500/10">
762
+ <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" />
763
+ </div>
764
+ <div>
765
+ <h4 className="text-[11px] font-medium text-emerald-300">Query looks good</h4>
766
+ <p className="text-[10px] text-zinc-500">No obvious performance issues detected.</p>
767
+ </div>
768
+ </div>
769
+ )}
770
+
771
+ {/* Metrics Grid */}
772
+ <div className="grid grid-cols-3 gap-2">
773
+ {analysis?.insights.map((insight, idx) => (
774
+ <div key={idx} className="p-3 rounded-lg bg-white/[0.02] border border-white/5">
775
+ <div className="flex items-center gap-2 mb-1">
776
+ <StatusBadge status={insight.status} />
777
+ <span className="text-[9px] text-zinc-500 uppercase tracking-wider font-medium">
778
+ {insight.label}
779
+ </span>
780
+ </div>
781
+ <span className="text-lg font-medium text-zinc-200">{insight.value}</span>
782
+ </div>
783
+ ))}
784
+ </div>
785
+
786
+ {/* Plan tree preview */}
787
+ <div>
788
+ <h3 className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider mb-2">
789
+ Execution Plan
790
+ </h3>
791
+ <div className="rounded-lg border border-white/5 bg-white/[0.01] p-2">
792
+ {rootPlan && analysis && (
793
+ <PlanNode node={rootPlan} maxTime={analysis.executionTime || 1} />
794
+ )}
795
+ </div>
796
+ </div>
797
+ </div>
798
+ )}
799
+
800
+ {activeTab === 'tree' && (
801
+ <div className="p-4">
802
+ <div className="rounded-lg border border-white/5 bg-white/[0.01] p-2">
803
+ {rootPlan && analysis && (
804
+ <PlanNode node={rootPlan} maxTime={analysis.executionTime || 1} />
805
+ )}
806
+ </div>
807
+ </div>
808
+ )}
809
+
810
+ {activeTab === 'raw' && (
811
+ <div className="p-4">
812
+ <pre className="text-[10px] font-mono text-zinc-400 bg-white/[0.02] rounded-lg p-4 overflow-auto border border-white/5">
813
+ {JSON.stringify(plan, null, 2)}
814
+ </pre>
815
+ </div>
816
+ )}
817
+ </div>
818
+ </div>
819
+ );
820
+ }