@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,473 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useMemo, useCallback } from 'react';
4
+ import {
5
+ GitCompare,
6
+ Plus,
7
+ Minus,
8
+ Edit3,
9
+ Camera,
10
+ FileCode,
11
+ ChevronRight,
12
+ ChevronDown,
13
+ Clock,
14
+ Database,
15
+ AlertTriangle,
16
+ } from 'lucide-react';
17
+ import { Button } from '@/components/ui/button';
18
+ import {
19
+ Select,
20
+ SelectContent,
21
+ SelectItem,
22
+ SelectTrigger,
23
+ SelectValue,
24
+ } from '@/components/ui/select';
25
+ import { Badge } from '@/components/ui/badge';
26
+ import { cn } from '@/lib/utils';
27
+ import type { TableSchema, SchemaSnapshot, DatabaseType, DatabaseConnection } from '@/lib/types';
28
+ import { storage } from '@/lib/storage';
29
+ import { useAllConnections } from '@/hooks/use-all-connections';
30
+ import { diffSchemas } from '@/lib/schema-diff/diff-engine';
31
+ import { generateMigrationSQL } from '@/lib/schema-diff/migration-generator';
32
+ import type { SchemaDiff as SchemaDiffType, TableDiff } from '@/lib/schema-diff/types';
33
+ import { SnapshotTimeline } from '@/components/SnapshotTimeline';
34
+
35
+ interface SchemaDiffProps {
36
+ schema: TableSchema[];
37
+ connection: DatabaseConnection | null;
38
+ }
39
+
40
+ export function SchemaDiff({ schema, connection }: SchemaDiffProps) {
41
+ const [snapshots, setSnapshots] = useState<SchemaSnapshot[]>(() =>
42
+ storage.getSchemaSnapshots()
43
+ );
44
+ const [sourceId, setSourceId] = useState<string>('current');
45
+ const [targetId, setTargetId] = useState<string>('');
46
+ const [selectedTable, setSelectedTable] = useState<string | null>(null);
47
+ const [showMigration, setShowMigration] = useState(false);
48
+ const [snapshotLabel, setSnapshotLabel] = useState('');
49
+ const [showLabelInput, setShowLabelInput] = useState(false);
50
+
51
+ // Take snapshot of current schema
52
+ const takeSnapshot = useCallback(() => {
53
+ if (!connection) return;
54
+ const snapshot: SchemaSnapshot = {
55
+ id: Date.now().toString(),
56
+ connectionId: connection.id,
57
+ connectionName: connection.name,
58
+ databaseType: connection.type,
59
+ schema: JSON.parse(JSON.stringify(schema)),
60
+ createdAt: new Date(),
61
+ label: snapshotLabel.trim() || undefined,
62
+ };
63
+ storage.saveSchemaSnapshot(snapshot);
64
+ setSnapshots(storage.getSchemaSnapshots());
65
+ setSnapshotLabel('');
66
+ setShowLabelInput(false);
67
+ }, [schema, connection, snapshotLabel]);
68
+
69
+ // Delete snapshot
70
+ const deleteSnapshot = useCallback((id: string) => {
71
+ storage.deleteSchemaSnapshot(id);
72
+ setSnapshots(storage.getSchemaSnapshots());
73
+ if (sourceId === id) setSourceId('current');
74
+ if (targetId === id) setTargetId('');
75
+ }, [sourceId, targetId]);
76
+
77
+ // Compute diff
78
+ const diff = useMemo<SchemaDiffType | null>(() => {
79
+ if (!targetId) return null;
80
+
81
+ const sourceSchema = sourceId === 'current'
82
+ ? schema
83
+ : snapshots.find(s => s.id === sourceId)?.schema || [];
84
+
85
+ const targetSchema = targetId === 'current'
86
+ ? schema
87
+ : snapshots.find(s => s.id === targetId)?.schema || [];
88
+
89
+ if (sourceId === targetId) return null;
90
+
91
+ return diffSchemas(sourceSchema, targetSchema);
92
+ }, [sourceId, targetId, schema, snapshots]);
93
+
94
+ // Generate migration SQL
95
+ const migrationSQL = useMemo(() => {
96
+ if (!diff || !diff.hasChanges) return '';
97
+ const dialect = connection?.type || 'postgres';
98
+ return generateMigrationSQL(diff, dialect as DatabaseType);
99
+ }, [diff, connection]);
100
+
101
+ // Get all connections for cross-connection comparison
102
+ const { connections: allConnections } = useAllConnections();
103
+ const [fetchingRemote, setFetchingRemote] = useState(false);
104
+
105
+ // Fetch schema from a remote connection
106
+ const fetchRemoteSchema = useCallback(async (connId: string) => {
107
+ const conn = allConnections.find(c => c.id === connId);
108
+ if (!conn) return;
109
+
110
+ setFetchingRemote(true);
111
+ try {
112
+ const res = await fetch('/api/db/schema-snapshot', {
113
+ method: 'POST',
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify(
116
+ conn.managed && conn.seedId
117
+ ? { connectionId: `seed:${conn.seedId}` }
118
+ : { connection: conn }
119
+ ),
120
+ });
121
+ const data = await res.json();
122
+ if (!res.ok) throw new Error(data.error);
123
+
124
+ // Auto-save as snapshot
125
+ const snapshot: SchemaSnapshot = {
126
+ id: `remote-${Date.now()}`,
127
+ connectionId: conn.id,
128
+ connectionName: conn.name,
129
+ databaseType: conn.type,
130
+ schema: data.schema,
131
+ createdAt: new Date(),
132
+ label: `Live: ${conn.name}`,
133
+ };
134
+ storage.saveSchemaSnapshot(snapshot);
135
+ setSnapshots(storage.getSchemaSnapshots());
136
+ setTargetId(snapshot.id);
137
+ } catch (err) {
138
+ console.error('Failed to fetch remote schema:', err);
139
+ } finally {
140
+ setFetchingRemote(false);
141
+ }
142
+ }, [allConnections]);
143
+
144
+ const getActionBadge = (action: string) => {
145
+ switch (action) {
146
+ case 'added': return <Badge className="bg-green-500/20 text-green-400 border-green-500/30 text-[10px]"><Plus className="w-2.5 h-2.5 mr-0.5" />Added</Badge>;
147
+ case 'removed': return <Badge className="bg-red-500/20 text-red-400 border-red-500/30 text-[10px]"><Minus className="w-2.5 h-2.5 mr-0.5" />Removed</Badge>;
148
+ case 'modified': return <Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30 text-[10px]"><Edit3 className="w-2.5 h-2.5 mr-0.5" />Modified</Badge>;
149
+ default: return null;
150
+ }
151
+ };
152
+
153
+ const formatSnapshotLabel = (s: SchemaSnapshot) => {
154
+ const date = new Date(s.createdAt).toLocaleString();
155
+ return `${s.label || s.connectionName} (${date})`;
156
+ };
157
+
158
+ return (
159
+ <div className="h-full flex flex-col bg-[#080808]">
160
+ {/* Header */}
161
+ <div className="flex items-center gap-2 px-3 py-2 border-b border-white/5 bg-[#0a0a0a] flex-wrap">
162
+ <GitCompare className="w-4 h-4 text-rose-400" />
163
+ <span className="text-[10px] font-bold uppercase text-zinc-400 tracking-wider">Schema Diff</span>
164
+
165
+ <div className="h-4 w-px bg-white/10" />
166
+
167
+ {/* Source selector */}
168
+ <div className="flex items-center gap-1">
169
+ <span className="text-[10px] text-zinc-600 uppercase">Source</span>
170
+ <Select value={sourceId} onValueChange={setSourceId}>
171
+ <SelectTrigger className="h-7 w-[180px] text-xs bg-white/5 border-white/10">
172
+ <SelectValue placeholder="Select source" />
173
+ </SelectTrigger>
174
+ <SelectContent className="bg-[#111] border-white/10">
175
+ <SelectItem value="current" className="text-xs">
176
+ <div className="flex items-center gap-1">
177
+ <Database className="w-3 h-3" /> Current Schema
178
+ </div>
179
+ </SelectItem>
180
+ {snapshots.map(s => (
181
+ <SelectItem key={s.id} value={s.id} className="text-xs">
182
+ <div className="flex items-center gap-1">
183
+ <Clock className="w-3 h-3" /> {formatSnapshotLabel(s)}
184
+ </div>
185
+ </SelectItem>
186
+ ))}
187
+ </SelectContent>
188
+ </Select>
189
+ </div>
190
+
191
+ <span className="text-zinc-600 text-xs">vs</span>
192
+
193
+ {/* Target selector */}
194
+ <div className="flex items-center gap-1">
195
+ <span className="text-[10px] text-zinc-600 uppercase">Target</span>
196
+ <Select value={targetId} onValueChange={(v) => {
197
+ if (v.startsWith('conn:')) {
198
+ fetchRemoteSchema(v.replace('conn:', ''));
199
+ } else {
200
+ setTargetId(v);
201
+ }
202
+ }}>
203
+ <SelectTrigger className="h-7 w-[180px] text-xs bg-white/5 border-white/10">
204
+ <SelectValue placeholder="Select target" />
205
+ </SelectTrigger>
206
+ <SelectContent className="bg-[#111] border-white/10">
207
+ <SelectItem value="current" className="text-xs">
208
+ <div className="flex items-center gap-1">
209
+ <Database className="w-3 h-3" /> Current Schema
210
+ </div>
211
+ </SelectItem>
212
+ {snapshots.map(s => (
213
+ <SelectItem key={s.id} value={s.id} className="text-xs">
214
+ <div className="flex items-center gap-1">
215
+ <Clock className="w-3 h-3" /> {formatSnapshotLabel(s)}
216
+ </div>
217
+ </SelectItem>
218
+ ))}
219
+ {allConnections.filter(c => c.id !== connection?.id).length > 0 && (
220
+ <>
221
+ <div className="px-2 py-1 text-[9px] text-zinc-600 uppercase border-t border-white/5 mt-1">
222
+ Fetch from connection
223
+ </div>
224
+ {allConnections.filter(c => c.id !== connection?.id).map(c => (
225
+ <SelectItem key={`conn:${c.id}`} value={`conn:${c.id}`} className="text-xs">
226
+ <div className="flex items-center gap-1">
227
+ <Database className="w-3 h-3 text-blue-400" /> {c.name}
228
+ {c.environment === 'production' && (
229
+ <AlertTriangle className="w-3 h-3 text-red-400" />
230
+ )}
231
+ </div>
232
+ </SelectItem>
233
+ ))}
234
+ </>
235
+ )}
236
+ </SelectContent>
237
+ </Select>
238
+ {fetchingRemote && <span className="text-[10px] text-zinc-500 animate-pulse">Fetching...</span>}
239
+ </div>
240
+
241
+ <div className="flex-1" />
242
+
243
+ {/* Snapshot controls */}
244
+ {showLabelInput ? (
245
+ <div className="flex items-center gap-1">
246
+ <input
247
+ type="text"
248
+ placeholder="Label (optional)..."
249
+ value={snapshotLabel}
250
+ onChange={(e) => setSnapshotLabel(e.target.value)}
251
+ onKeyDown={(e) => e.key === 'Enter' && takeSnapshot()}
252
+ 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 w-32"
253
+ autoFocus
254
+ />
255
+ <Button variant="ghost" size="sm" className="h-7 text-xs text-blue-400" onClick={takeSnapshot}>
256
+ Save
257
+ </Button>
258
+ <Button variant="ghost" size="sm" className="h-7 text-xs text-zinc-500" onClick={() => setShowLabelInput(false)}>
259
+ Cancel
260
+ </Button>
261
+ </div>
262
+ ) : (
263
+ <Button
264
+ variant="ghost"
265
+ size="sm"
266
+ className="h-7 text-[10px] font-bold uppercase text-zinc-500 hover:text-white gap-1"
267
+ onClick={() => setShowLabelInput(true)}
268
+ disabled={!connection}
269
+ >
270
+ <Camera className="w-3 h-3" /> Snapshot
271
+ </Button>
272
+ )}
273
+
274
+ {diff?.hasChanges && (
275
+ <Button
276
+ variant="ghost"
277
+ size="sm"
278
+ className="h-7 text-[10px] font-bold uppercase text-zinc-500 hover:text-white gap-1"
279
+ onClick={() => setShowMigration(!showMigration)}
280
+ >
281
+ <FileCode className="w-3 h-3" /> {showMigration ? 'Diff View' : 'SQL Migration'}
282
+ </Button>
283
+ )}
284
+ </div>
285
+
286
+ {/* Content */}
287
+ <div className="flex-1 overflow-hidden flex">
288
+ {!targetId ? (
289
+ <div className="flex-1 flex flex-col items-center justify-center text-zinc-600 gap-3">
290
+ <GitCompare className="w-10 h-10 opacity-30" />
291
+ <p className="text-sm">Select source and target to compare schemas</p>
292
+ <p className="text-xs text-zinc-700">Take a snapshot first, then compare with the current schema</p>
293
+
294
+ {/* Snapshot Timeline */}
295
+ {snapshots.length > 0 && (
296
+ <div className="mt-4 w-full max-w-2xl px-4">
297
+ <SnapshotTimeline
298
+ snapshots={snapshots}
299
+ onCompare={(sourceId, targetId) => {
300
+ setSourceId(sourceId);
301
+ setTargetId(targetId);
302
+ }}
303
+ onDelete={deleteSnapshot}
304
+ />
305
+ </div>
306
+ )}
307
+ </div>
308
+ ) : showMigration && migrationSQL ? (
309
+ <div className="flex-1 overflow-auto p-4">
310
+ <pre className="text-xs font-mono text-zinc-300 bg-[#0d0d0d] border border-white/10 rounded-lg p-4 overflow-auto whitespace-pre-wrap">
311
+ {migrationSQL}
312
+ </pre>
313
+ </div>
314
+ ) : diff && diff.hasChanges ? (
315
+ <>
316
+ {/* Table List */}
317
+ <div className="w-64 border-r border-white/5 overflow-auto">
318
+ <div className="p-2 border-b border-white/5">
319
+ <div className="text-[10px] text-zinc-500 uppercase px-2 mb-1">
320
+ {diff.summary.added} added, {diff.summary.removed} removed, {diff.summary.modified} modified
321
+ </div>
322
+ </div>
323
+ {diff.tables.map(table => (
324
+ <button
325
+ key={table.tableName}
326
+ onClick={() => setSelectedTable(table.tableName)}
327
+ className={cn(
328
+ "w-full text-left px-3 py-2 text-xs flex items-center gap-2 hover:bg-white/5 transition-colors",
329
+ selectedTable === table.tableName && "bg-white/10"
330
+ )}
331
+ >
332
+ {selectedTable === table.tableName ? (
333
+ <ChevronDown className="w-3 h-3 text-zinc-500" />
334
+ ) : (
335
+ <ChevronRight className="w-3 h-3 text-zinc-500" />
336
+ )}
337
+ <span className="text-zinc-300">{table.tableName}</span>
338
+ <span className="ml-auto">{getActionBadge(table.action)}</span>
339
+ </button>
340
+ ))}
341
+ </div>
342
+
343
+ {/* Table Detail */}
344
+ <div className="flex-1 overflow-auto p-4">
345
+ {selectedTable ? (
346
+ <TableDiffDetail diff={diff.tables.find(t => t.tableName === selectedTable)!} />
347
+ ) : (
348
+ <div className="h-full flex items-center justify-center text-zinc-600 text-sm">
349
+ Select a table to view diff details
350
+ </div>
351
+ )}
352
+ </div>
353
+ </>
354
+ ) : diff && !diff.hasChanges ? (
355
+ <div className="flex-1 flex items-center justify-center text-zinc-600 gap-2">
356
+ <span className="text-sm">No differences found between source and target</span>
357
+ </div>
358
+ ) : (
359
+ <div className="flex-1 flex items-center justify-center text-zinc-600 gap-2">
360
+ <AlertTriangle className="w-4 h-4" />
361
+ <span className="text-sm">Cannot compare same schema with itself</span>
362
+ </div>
363
+ )}
364
+ </div>
365
+ </div>
366
+ );
367
+ }
368
+
369
+ function TableDiffDetail({ diff }: { diff: TableDiff }) {
370
+ return (
371
+ <div className="space-y-4">
372
+ <div className="flex items-center gap-2">
373
+ <Database className="w-4 h-4 text-zinc-400" />
374
+ <h3 className="text-sm font-bold text-zinc-200">{diff.tableName}</h3>
375
+ <Badge className={cn(
376
+ "text-[10px]",
377
+ diff.action === 'added' && "bg-green-500/20 text-green-400",
378
+ diff.action === 'removed' && "bg-red-500/20 text-red-400",
379
+ diff.action === 'modified' && "bg-yellow-500/20 text-yellow-400",
380
+ )}>
381
+ {diff.action}
382
+ </Badge>
383
+ </div>
384
+
385
+ {/* Columns */}
386
+ {diff.columns.length > 0 && (
387
+ <div>
388
+ <h4 className="text-[10px] uppercase text-zinc-500 mb-2 font-bold">Columns</h4>
389
+ <div className="space-y-1">
390
+ {diff.columns.map((col, i) => (
391
+ <div key={i} className={cn(
392
+ "px-3 py-2 rounded text-xs flex items-center gap-2",
393
+ col.action === 'added' && "bg-green-500/5 border border-green-500/10",
394
+ col.action === 'removed' && "bg-red-500/5 border border-red-500/10",
395
+ col.action === 'modified' && "bg-yellow-500/5 border border-yellow-500/10",
396
+ )}>
397
+ <span className="font-mono text-zinc-300 min-w-[120px]">{col.columnName}</span>
398
+ {col.action === 'modified' && (
399
+ <div className="flex flex-col gap-0.5">
400
+ {col.changes.map((change, j) => (
401
+ <span key={j} className="text-[10px] text-zinc-500">{change}</span>
402
+ ))}
403
+ </div>
404
+ )}
405
+ {col.action === 'added' && (
406
+ <span className="text-[10px] text-green-400 font-mono">{col.targetType}</span>
407
+ )}
408
+ {col.action === 'removed' && (
409
+ <span className="text-[10px] text-red-400 font-mono">{col.sourceType}</span>
410
+ )}
411
+ <span className="ml-auto">{getActionIcon(col.action)}</span>
412
+ </div>
413
+ ))}
414
+ </div>
415
+ </div>
416
+ )}
417
+
418
+ {/* Indexes */}
419
+ {diff.indexes.length > 0 && (
420
+ <div>
421
+ <h4 className="text-[10px] uppercase text-zinc-500 mb-2 font-bold">Indexes</h4>
422
+ <div className="space-y-1">
423
+ {diff.indexes.map((idx, i) => (
424
+ <div key={i} className={cn(
425
+ "px-3 py-2 rounded text-xs flex items-center gap-2",
426
+ idx.action === 'added' && "bg-green-500/5 border border-green-500/10",
427
+ idx.action === 'removed' && "bg-red-500/5 border border-red-500/10",
428
+ idx.action === 'modified' && "bg-yellow-500/5 border border-yellow-500/10",
429
+ )}>
430
+ <span className="font-mono text-zinc-300">{idx.indexName}</span>
431
+ {idx.changes.map((change, j) => (
432
+ <span key={j} className="text-[10px] text-zinc-500">{change}</span>
433
+ ))}
434
+ <span className="ml-auto">{getActionIcon(idx.action)}</span>
435
+ </div>
436
+ ))}
437
+ </div>
438
+ </div>
439
+ )}
440
+
441
+ {/* Foreign Keys */}
442
+ {diff.foreignKeys.length > 0 && (
443
+ <div>
444
+ <h4 className="text-[10px] uppercase text-zinc-500 mb-2 font-bold">Foreign Keys</h4>
445
+ <div className="space-y-1">
446
+ {diff.foreignKeys.map((fk, i) => (
447
+ <div key={i} className={cn(
448
+ "px-3 py-2 rounded text-xs flex items-center gap-2",
449
+ fk.action === 'added' && "bg-green-500/5 border border-green-500/10",
450
+ fk.action === 'removed' && "bg-red-500/5 border border-red-500/10",
451
+ )}>
452
+ <span className="font-mono text-zinc-300">{fk.columnName}</span>
453
+ {fk.changes.map((change, j) => (
454
+ <span key={j} className="text-[10px] text-zinc-500">{change}</span>
455
+ ))}
456
+ <span className="ml-auto">{getActionIcon(fk.action)}</span>
457
+ </div>
458
+ ))}
459
+ </div>
460
+ </div>
461
+ )}
462
+ </div>
463
+ );
464
+ }
465
+
466
+ function getActionIcon(action: string) {
467
+ switch (action) {
468
+ case 'added': return <Plus className="w-3 h-3 text-green-400" />;
469
+ case 'removed': return <Minus className="w-3 h-3 text-red-400" />;
470
+ case 'modified': return <Edit3 className="w-3 h-3 text-yellow-400" />;
471
+ default: return null;
472
+ }
473
+ }
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { Trash2 } from 'lucide-react';
5
+ import { Badge } from '@/components/ui/badge';
6
+ import { cn } from '@/lib/utils';
7
+ import type { SchemaSnapshot } from '@/lib/types';
8
+
9
+ interface SnapshotTimelineProps {
10
+ snapshots: SchemaSnapshot[];
11
+ onCompare: (sourceId: string, targetId: string) => void;
12
+ onDelete: (id: string) => void;
13
+ }
14
+
15
+ export function SnapshotTimeline({ snapshots, onCompare, onDelete }: SnapshotTimelineProps) {
16
+ const [selected, setSelected] = useState<string[]>([]);
17
+
18
+ const handleClick = (id: string) => {
19
+ setSelected(prev => {
20
+ if (prev.includes(id)) {
21
+ return prev.filter(s => s !== id);
22
+ }
23
+ if (prev.length >= 2) {
24
+ return [prev[1], id];
25
+ }
26
+ return [...prev, id];
27
+ });
28
+ };
29
+
30
+ const canCompare = selected.length === 2;
31
+
32
+ useEffect(() => {
33
+ if (canCompare) {
34
+ onCompare(selected[0], selected[1]);
35
+ }
36
+ }, [selected, canCompare, onCompare]);
37
+
38
+ if (snapshots.length === 0) {
39
+ return (
40
+ <div className="flex items-center justify-center py-4 text-zinc-600 text-xs">
41
+ No snapshots taken yet. Take a snapshot to start tracking schema changes.
42
+ </div>
43
+ );
44
+ }
45
+
46
+ const sorted = [...snapshots].sort((a, b) =>
47
+ new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
48
+ );
49
+
50
+ return (
51
+ <div className="space-y-2">
52
+ <div className="flex items-center justify-between px-2">
53
+ <span className="text-[10px] uppercase text-zinc-500 font-bold">Timeline</span>
54
+ {canCompare && (
55
+ <span className="text-[10px] text-blue-400">Comparing 2 snapshots</span>
56
+ )}
57
+ </div>
58
+
59
+ {/* Horizontal timeline */}
60
+ <div className="relative flex items-center overflow-x-auto pb-2 px-2 gap-0">
61
+ {/* Timeline line */}
62
+ <div className="absolute top-[18px] left-4 right-4 h-[2px] bg-white/10" />
63
+
64
+ {sorted.map((snapshot, idx) => {
65
+ const isSelected = selected.includes(snapshot.id);
66
+ const date = new Date(snapshot.createdAt);
67
+
68
+ return (
69
+ <div
70
+ key={snapshot.id}
71
+ className="relative flex flex-col items-center min-w-[100px] cursor-pointer group"
72
+ onClick={() => handleClick(snapshot.id)}
73
+ >
74
+ {/* Dot */}
75
+ <div className={cn(
76
+ "w-4 h-4 rounded-full border-2 z-10 transition-all",
77
+ isSelected
78
+ ? "bg-blue-500 border-blue-400 scale-125"
79
+ : "bg-[#0d0d0d] border-white/20 hover:border-white/40"
80
+ )} />
81
+
82
+ {/* Connector */}
83
+ {idx < sorted.length - 1 && (
84
+ <div className="absolute top-[7px] left-[50%] w-full h-[2px] bg-white/10" />
85
+ )}
86
+
87
+ {/* Label */}
88
+ <div className={cn(
89
+ "mt-2 text-center transition-colors",
90
+ isSelected ? "text-blue-400" : "text-zinc-500 group-hover:text-zinc-300"
91
+ )}>
92
+ <div className="text-[10px] font-medium truncate max-w-[90px]">
93
+ {snapshot.label || snapshot.connectionName}
94
+ </div>
95
+ <div className="text-[9px] text-zinc-600">
96
+ {date.toLocaleDateString()} {date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
97
+ </div>
98
+ <Badge variant="secondary" className="text-[9px] mt-1">
99
+ {snapshot.schema.length} tables
100
+ </Badge>
101
+ </div>
102
+
103
+ {/* Delete button */}
104
+ <button
105
+ onClick={(e) => { e.stopPropagation(); onDelete(snapshot.id); }}
106
+ className="absolute -top-2 -right-1 p-0.5 text-zinc-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
107
+ >
108
+ <Trash2 className="w-2.5 h-2.5" />
109
+ </button>
110
+ </div>
111
+ );
112
+ })}
113
+ </div>
114
+ </div>
115
+ );
116
+ }