@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,572 @@
1
+ import '../setup-dom';
2
+ import '../helpers/mock-sonner';
3
+ import '../helpers/mock-navigation';
4
+
5
+ import { mock } from 'bun:test';
6
+
7
+ // Mock storage before component import
8
+ const mockHistory = [
9
+ {
10
+ id: 'h1',
11
+ query: 'SELECT * FROM users',
12
+ executedAt: new Date('2026-01-15T10:00:00Z'),
13
+ executionTime: 25,
14
+ rowCount: 10,
15
+ status: 'success' as const,
16
+ connectionId: 'c1',
17
+ connectionName: 'TestDB',
18
+ tabName: 'Query 1',
19
+ },
20
+ {
21
+ id: 'h2',
22
+ query: 'DROP TABLE bad',
23
+ executedAt: new Date('2026-01-14T08:00:00Z'),
24
+ executionTime: 5,
25
+ rowCount: 0,
26
+ status: 'error' as const,
27
+ errorMessage: 'permission denied',
28
+ connectionId: 'c2',
29
+ connectionName: 'ProdDB',
30
+ tabName: 'Query 2',
31
+ },
32
+ ];
33
+
34
+ const mockGetHistory = mock(() => [...mockHistory]);
35
+ const mockClearHistory = mock(() => {});
36
+
37
+ mock.module('@/lib/storage', () => ({
38
+ storage: {
39
+ getHistory: mockGetHistory,
40
+ clearHistory: mockClearHistory,
41
+ },
42
+ }));
43
+
44
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
45
+ import { render, fireEvent, within, cleanup } from '@testing-library/react';
46
+ import userEvent from '@testing-library/user-event';
47
+ import React from 'react';
48
+
49
+ import { QueryHistory } from '@/components/QueryHistory';
50
+
51
+ // =============================================================================
52
+ // QueryHistory Tests
53
+ // =============================================================================
54
+
55
+ function createDefaultProps(overrides: Partial<Parameters<typeof QueryHistory>[0]> = {}) {
56
+ return {
57
+ onSelectQuery: mock(() => {}),
58
+ activeConnectionId: undefined,
59
+ refreshTrigger: 0,
60
+ ...overrides,
61
+ };
62
+ }
63
+
64
+ describe('QueryHistory', () => {
65
+ afterEach(() => {
66
+ cleanup();
67
+ });
68
+
69
+ beforeEach(() => {
70
+ mockGetHistory.mockClear();
71
+ mockClearHistory.mockClear();
72
+ mockGetHistory.mockImplementation(() => [...mockHistory]);
73
+ });
74
+
75
+ // ── Renders history items ─────────────────────────────────────────────────
76
+
77
+ test('renders history items from storage', () => {
78
+ const props = createDefaultProps();
79
+ const { container } = render(<QueryHistory {...props} />);
80
+ const view = within(container);
81
+
82
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
83
+ expect(view.queryByText('DROP TABLE bad')).not.toBeNull();
84
+ });
85
+
86
+ // ── Status icons ──────────────────────────────────────────────────────────
87
+
88
+ test('shows success and error status indicators', () => {
89
+ const props = createDefaultProps();
90
+ const { container } = render(<QueryHistory {...props} />);
91
+
92
+ const successIndicators = container.querySelectorAll('.bg-emerald-500\\/10');
93
+ const errorIndicators = container.querySelectorAll('.bg-red-500\\/10');
94
+
95
+ expect(successIndicators.length).toBeGreaterThan(0);
96
+ expect(errorIndicators.length).toBeGreaterThan(0);
97
+ });
98
+
99
+ // ── Search filters ────────────────────────────────────────────────────────
100
+
101
+ test('search filters by query text', async () => {
102
+ const user = userEvent.setup();
103
+ const props = createDefaultProps();
104
+ const { container } = render(<QueryHistory {...props} />);
105
+ const view = within(container);
106
+
107
+ // Both items initially visible
108
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
109
+ expect(view.queryByText('DROP TABLE bad')).not.toBeNull();
110
+
111
+ // Type in search using userEvent (fireEvent.change doesn't trigger React 19 onChange)
112
+ const searchInput = view.getByPlaceholderText('Search by query, connection or tab...');
113
+ await user.type(searchInput, 'SELECT');
114
+
115
+ // Only SELECT query should remain
116
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
117
+ expect(view.queryByText('DROP TABLE bad')).toBeNull();
118
+ });
119
+
120
+ // ── Restore button fires onSelectQuery ────────────────────────────────────
121
+
122
+ test('onSelectQuery fires when restore button clicked', () => {
123
+ const onSelectQuery = mock(() => {});
124
+ const props = createDefaultProps({ onSelectQuery });
125
+ const { container } = render(<QueryHistory {...props} />);
126
+ const view = within(container);
127
+
128
+ const restoreButtons = view.getAllByTitle('Restore Query');
129
+ expect(restoreButtons.length).toBe(2);
130
+
131
+ fireEvent.click(restoreButtons[0]);
132
+
133
+ expect(onSelectQuery).toHaveBeenCalledTimes(1);
134
+ expect(onSelectQuery).toHaveBeenCalledWith('SELECT * FROM users');
135
+ });
136
+
137
+ // ── Clear history ─────────────────────────────────────────────────────────
138
+
139
+ test('clear history clears state after confirm', () => {
140
+ const originalConfirm = globalThis.confirm;
141
+ globalThis.confirm = mock(() => true) as unknown as typeof confirm;
142
+
143
+ const props = createDefaultProps();
144
+ const { container } = render(<QueryHistory {...props} />);
145
+ const view = within(container);
146
+
147
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
148
+
149
+ const clearButton = view.getByText('Clear');
150
+ fireEvent.click(clearButton);
151
+
152
+ expect(mockClearHistory).toHaveBeenCalledTimes(1);
153
+ expect(view.queryByText('SELECT * FROM users')).toBeNull();
154
+
155
+ globalThis.confirm = originalConfirm;
156
+ });
157
+
158
+ // ── Empty state ───────────────────────────────────────────────────────────
159
+
160
+ test('empty state when no items match filter', async () => {
161
+ const user = userEvent.setup();
162
+ const props = createDefaultProps();
163
+ const { container } = render(<QueryHistory {...props} />);
164
+ const view = within(container);
165
+
166
+ const searchInput = view.getByPlaceholderText('Search by query, connection or tab...');
167
+ await user.type(searchInput, 'NONEXISTENT_QUERY_XYZ');
168
+
169
+ expect(view.queryByText('No history items found')).not.toBeNull();
170
+ expect(view.queryByText('Run some queries to see them here')).not.toBeNull();
171
+ });
172
+
173
+ // ── Shows execution time and row count ────────────────────────────────────
174
+
175
+ test('shows execution time and row count', () => {
176
+ const props = createDefaultProps();
177
+ const { container } = render(<QueryHistory {...props} />);
178
+ const view = within(container);
179
+
180
+ expect(view.queryByText('25ms')).not.toBeNull();
181
+ expect(view.queryByText('5ms')).not.toBeNull();
182
+ expect(view.queryByText('10')).not.toBeNull();
183
+ });
184
+
185
+ // ── Shows connection name and tab name ────────────────────────────────────
186
+
187
+ test('shows connection name and tab name', () => {
188
+ const props = createDefaultProps();
189
+ const { container } = render(<QueryHistory {...props} />);
190
+ const view = within(container);
191
+
192
+ expect(view.queryByText('TestDB')).not.toBeNull();
193
+ expect(view.queryByText('ProdDB')).not.toBeNull();
194
+ expect(view.queryByText('Query 1')).not.toBeNull();
195
+ expect(view.queryByText('Query 2')).not.toBeNull();
196
+ });
197
+
198
+ // ── Filter by success status ──────────────────────────────────────────────
199
+
200
+ test('filter by success status shows only successful queries', async () => {
201
+ const user = userEvent.setup();
202
+ const props = createDefaultProps();
203
+ const { container } = render(<QueryHistory {...props} />);
204
+ const view = within(container);
205
+
206
+ // Click the "success" filter button
207
+ const successButton = view.getByText('success');
208
+ await user.click(successButton);
209
+
210
+ // Only the success item should remain
211
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
212
+ expect(view.queryByText('DROP TABLE bad')).toBeNull();
213
+ });
214
+
215
+ // ── Filter by error status ────────────────────────────────────────────────
216
+
217
+ test('filter by error status shows only failed queries', async () => {
218
+ const user = userEvent.setup();
219
+ const props = createDefaultProps();
220
+ const { container } = render(<QueryHistory {...props} />);
221
+ const view = within(container);
222
+
223
+ // Click the "error" filter button
224
+ const errorButton = view.getByText('error');
225
+ await user.click(errorButton);
226
+
227
+ // Only the error item should remain
228
+ expect(view.queryByText('DROP TABLE bad')).not.toBeNull();
229
+ expect(view.queryByText('SELECT * FROM users')).toBeNull();
230
+ });
231
+
232
+ // ── All Connections toggle shows all items ────────────────────────────────
233
+
234
+ test('All Connections toggle shows items from all connections', async () => {
235
+ const user = userEvent.setup();
236
+ const props = createDefaultProps({ activeConnectionId: 'c1' });
237
+ const { container } = render(<QueryHistory {...props} />);
238
+ const view = within(container);
239
+
240
+ // With "Active Conn" (default), only c1 items should show
241
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
242
+ expect(view.queryByText('DROP TABLE bad')).toBeNull();
243
+
244
+ // Click "All Connections" to show all
245
+ const allConnButton = view.getByText('All Connections');
246
+ await user.click(allConnButton);
247
+
248
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
249
+ expect(view.queryByText('DROP TABLE bad')).not.toBeNull();
250
+ });
251
+
252
+ // ── Active Conn toggle filters by activeConnectionId ──────────────────────
253
+
254
+ test('Active Conn toggle filters by activeConnectionId', async () => {
255
+ const user = userEvent.setup();
256
+ const props = createDefaultProps({ activeConnectionId: 'c1' });
257
+ const { container } = render(<QueryHistory {...props} />);
258
+ const view = within(container);
259
+
260
+ // Switch to "All Connections" first
261
+ await user.click(view.getByText('All Connections'));
262
+ expect(view.queryByText('DROP TABLE bad')).not.toBeNull();
263
+
264
+ // Switch back to "Active Conn"
265
+ await user.click(view.getByText('Active Conn'));
266
+
267
+ // Only c1 connection item should show
268
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
269
+ expect(view.queryByText('DROP TABLE bad')).toBeNull();
270
+ });
271
+
272
+ // ── Sort by executionTime ─────────────────────────────────────────────────
273
+
274
+ test('sort by executionTime orders items by duration', async () => {
275
+ const user = userEvent.setup();
276
+ const props = createDefaultProps();
277
+ const { container } = render(<QueryHistory {...props} />);
278
+ const view = within(container);
279
+
280
+ // Click "Duration" header to sort by executionTime (desc by default)
281
+ const durationHeader = view.getByText('Duration');
282
+ await user.click(durationHeader);
283
+
284
+ // Check ordering: 25ms should come before 5ms in desc order
285
+ const rows = container.querySelectorAll('tbody tr');
286
+ expect(rows.length).toBe(2);
287
+
288
+ const firstRowText = rows[0].textContent || '';
289
+ const secondRowText = rows[1].textContent || '';
290
+ expect(firstRowText).toContain('25ms');
291
+ expect(secondRowText).toContain('5ms');
292
+ });
293
+
294
+ // ── Sort direction toggle on second click ─────────────────────────────────
295
+
296
+ test('sort direction toggles on second click of same column', async () => {
297
+ const user = userEvent.setup();
298
+ const props = createDefaultProps();
299
+ const { container } = render(<QueryHistory {...props} />);
300
+ const view = within(container);
301
+
302
+ // Click "Duration" header twice to toggle to asc
303
+ const durationHeader = view.getByText('Duration');
304
+ await user.click(durationHeader); // desc
305
+ await user.click(durationHeader); // asc
306
+
307
+ // In asc order: 5ms should come before 25ms
308
+ const rows = container.querySelectorAll('tbody tr');
309
+ const firstRowText = rows[0].textContent || '';
310
+ const secondRowText = rows[1].textContent || '';
311
+ expect(firstRowText).toContain('5ms');
312
+ expect(secondRowText).toContain('25ms');
313
+ });
314
+
315
+ // ── Search clear button (X) ───────────────────────────────────────────────
316
+
317
+ test('search clear button resets search and shows all items', async () => {
318
+ const user = userEvent.setup();
319
+ const props = createDefaultProps();
320
+ const { container } = render(<QueryHistory {...props} />);
321
+ const view = within(container);
322
+
323
+ // Type a search term
324
+ const searchInput = view.getByPlaceholderText('Search by query, connection or tab...');
325
+ await user.type(searchInput, 'SELECT');
326
+
327
+ // Only one item visible
328
+ expect(view.queryByText('DROP TABLE bad')).toBeNull();
329
+
330
+ // Click the X clear button
331
+ const clearSearchButton = container.querySelector('button .w-3.h-3')?.closest('button');
332
+ expect(clearSearchButton).not.toBeNull();
333
+ await user.click(clearSearchButton!);
334
+
335
+ // Both items should be visible again
336
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
337
+ expect(view.queryByText('DROP TABLE bad')).not.toBeNull();
338
+ });
339
+
340
+ // ── Export CSV creates download link ──────────────────────────────────────
341
+
342
+ test('export CSV creates download link', async () => {
343
+ const user = userEvent.setup();
344
+ const createObjectURLMock = mock(() => 'blob:fake-csv-url');
345
+ const revokeObjectURLMock = mock(() => {});
346
+ const clickMock = mock(() => {});
347
+
348
+ globalThis.URL.createObjectURL = createObjectURLMock;
349
+ globalThis.URL.revokeObjectURL = revokeObjectURLMock;
350
+
351
+ const origCreateElement = document.createElement.bind(document);
352
+ const createElementSpy = mock((tag: string) => {
353
+ const el = origCreateElement(tag);
354
+ if (tag === 'a') {
355
+ el.click = clickMock;
356
+ }
357
+ return el;
358
+ });
359
+ document.createElement = createElementSpy as unknown as typeof document.createElement;
360
+
361
+ const props = createDefaultProps();
362
+ const { container } = render(<QueryHistory {...props} />);
363
+
364
+ // Click Export button to open dropdown
365
+ const exportButton = within(container).getByText('Export');
366
+ await user.click(exportButton);
367
+
368
+ // Find and click "Export as CSV" in the dropdown (rendered in document body)
369
+ const csvOption = within(document.body as HTMLElement).getByText('Export as CSV');
370
+ await user.click(csvOption);
371
+
372
+ expect(createObjectURLMock).toHaveBeenCalled();
373
+ expect(clickMock).toHaveBeenCalled();
374
+ expect(revokeObjectURLMock).toHaveBeenCalled();
375
+
376
+ // Restore
377
+ document.createElement = origCreateElement;
378
+ });
379
+
380
+ // ── Export JSON creates download link ─────────────────────────────────────
381
+
382
+ test('export JSON creates download link', async () => {
383
+ const user = userEvent.setup();
384
+ const createObjectURLMock = mock(() => 'blob:fake-json-url');
385
+ const revokeObjectURLMock = mock(() => {});
386
+ const clickMock = mock(() => {});
387
+
388
+ globalThis.URL.createObjectURL = createObjectURLMock;
389
+ globalThis.URL.revokeObjectURL = revokeObjectURLMock;
390
+
391
+ const origCreateElement = document.createElement.bind(document);
392
+ const createElementSpy = mock((tag: string) => {
393
+ const el = origCreateElement(tag);
394
+ if (tag === 'a') {
395
+ el.click = clickMock;
396
+ }
397
+ return el;
398
+ });
399
+ document.createElement = createElementSpy as unknown as typeof document.createElement;
400
+
401
+ const props = createDefaultProps();
402
+ const { container } = render(<QueryHistory {...props} />);
403
+
404
+ // Click Export button to open dropdown
405
+ const exportButton = within(container).getByText('Export');
406
+ await user.click(exportButton);
407
+
408
+ // Find and click "Export as JSON" in the dropdown (rendered in document body)
409
+ const jsonOption = within(document.body as HTMLElement).getByText('Export as JSON');
410
+ await user.click(jsonOption);
411
+
412
+ expect(createObjectURLMock).toHaveBeenCalled();
413
+ expect(clickMock).toHaveBeenCalled();
414
+ expect(revokeObjectURLMock).toHaveBeenCalled();
415
+
416
+ // Restore
417
+ document.createElement = origCreateElement;
418
+ });
419
+
420
+ // ── Duration > 500ms amber styling ────────────────────────────────────────
421
+
422
+ test('duration greater than 500ms shows amber styling', () => {
423
+ const slowHistory = [
424
+ {
425
+ id: 'h-slow',
426
+ query: 'SELECT * FROM huge_table',
427
+ executedAt: new Date('2026-01-15T10:00:00Z'),
428
+ executionTime: 750,
429
+ rowCount: 5000,
430
+ status: 'success' as const,
431
+ connectionId: 'c1',
432
+ connectionName: 'TestDB',
433
+ tabName: 'Query 1',
434
+ },
435
+ ];
436
+ mockGetHistory.mockImplementation(() => [...slowHistory]);
437
+
438
+ const props = createDefaultProps();
439
+ const { container } = render(<QueryHistory {...props} />);
440
+
441
+ // The 750ms duration cell should have amber styling
442
+ const amberBadge = container.querySelector('.text-amber-400.bg-amber-400\\/10');
443
+ expect(amberBadge).not.toBeNull();
444
+ expect(amberBadge!.textContent).toBe('750ms');
445
+ });
446
+
447
+ // ── Error message display for failed queries ──────────────────────────────
448
+
449
+ test('error message is displayed for failed queries', () => {
450
+ const props = createDefaultProps();
451
+ const { container } = render(<QueryHistory {...props} />);
452
+ const view = within(container);
453
+
454
+ // The error message from h2 should be visible
455
+ expect(view.queryByText('permission denied')).not.toBeNull();
456
+ });
457
+
458
+ // ── Null rowCount shows dash ──────────────────────────────────────────────
459
+
460
+ test('null rowCount shows dash character', () => {
461
+ const historyWithNullRowCount = [
462
+ {
463
+ id: 'h-null',
464
+ query: 'CREATE INDEX idx ON users(name)',
465
+ executedAt: new Date('2026-01-15T10:00:00Z'),
466
+ executionTime: 30,
467
+ rowCount: null as unknown as number,
468
+ status: 'success' as const,
469
+ connectionId: 'c1',
470
+ connectionName: 'TestDB',
471
+ tabName: 'Query 1',
472
+ },
473
+ ];
474
+ mockGetHistory.mockImplementation(() => [...historyWithNullRowCount]);
475
+
476
+ const props = createDefaultProps();
477
+ const { container } = render(<QueryHistory {...props} />);
478
+
479
+ // The row count cell should show "-" for null/undefined rowCount
480
+ const cells = container.querySelectorAll('td');
481
+ const rowCountCell = Array.from(cells).find(cell => {
482
+ const span = cell.querySelector('.font-mono.text-xs');
483
+ return span && span.textContent === '-';
484
+ });
485
+ expect(rowCountCell).not.toBeNull();
486
+ });
487
+
488
+ // ── Clear history cancelled by user ───────────────────────────────────────
489
+
490
+ test('clear history cancelled by user does not clear items', () => {
491
+ const originalConfirm = globalThis.confirm;
492
+ globalThis.confirm = mock(() => false) as unknown as typeof confirm;
493
+
494
+ const props = createDefaultProps();
495
+ const { container } = render(<QueryHistory {...props} />);
496
+ const view = within(container);
497
+
498
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
499
+
500
+ const clearButton = view.getByText('Clear');
501
+ fireEvent.click(clearButton);
502
+
503
+ // Storage should NOT have been called
504
+ expect(mockClearHistory).not.toHaveBeenCalled();
505
+
506
+ // Items should still be visible
507
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
508
+ expect(view.queryByText('DROP TABLE bad')).not.toBeNull();
509
+
510
+ globalThis.confirm = originalConfirm;
511
+ });
512
+
513
+ // ── refreshTrigger change reloads history ─────────────────────────────────
514
+
515
+ test('refreshTrigger change reloads history from storage', () => {
516
+ const props = createDefaultProps({ refreshTrigger: 0 });
517
+ const { container, rerender } = render(<QueryHistory {...props} />);
518
+ const view = within(container);
519
+
520
+ expect(view.queryByText('SELECT * FROM users')).not.toBeNull();
521
+
522
+ // Clear mock call count from initial render
523
+ mockGetHistory.mockClear();
524
+
525
+ // Change the refreshTrigger to simulate a new query execution
526
+ const newHistory = [
527
+ ...mockHistory,
528
+ {
529
+ id: 'h3',
530
+ query: 'INSERT INTO orders VALUES(1)',
531
+ executedAt: new Date('2026-01-16T12:00:00Z'),
532
+ executionTime: 10,
533
+ rowCount: 1,
534
+ status: 'success' as const,
535
+ connectionId: 'c1',
536
+ connectionName: 'TestDB',
537
+ tabName: 'Query 3',
538
+ },
539
+ ];
540
+ mockGetHistory.mockImplementation(() => [...newHistory]);
541
+
542
+ rerender(<QueryHistory {...createDefaultProps({ refreshTrigger: 1 })} />);
543
+
544
+ // getHistory should have been called again
545
+ expect(mockGetHistory).toHaveBeenCalled();
546
+
547
+ // New item should be visible
548
+ expect(view.queryByText('INSERT INTO orders VALUES(1)')).not.toBeNull();
549
+ });
550
+
551
+ // ── Sort by rowCount ──────────────────────────────────────────────────────
552
+
553
+ test('sort by rowCount orders items by row count', async () => {
554
+ const user = userEvent.setup();
555
+ const props = createDefaultProps();
556
+ const { container } = render(<QueryHistory {...props} />);
557
+ const view = within(container);
558
+
559
+ // Click "Rows" header to sort by rowCount (desc by default)
560
+ const rowsHeader = view.getByText('Rows');
561
+ await user.click(rowsHeader);
562
+
563
+ // In desc order: 10 rows (h1) should come before 0 rows (h2)
564
+ const rows = container.querySelectorAll('tbody tr');
565
+ expect(rows.length).toBe(2);
566
+
567
+ const firstRowText = rows[0].textContent || '';
568
+ const secondRowText = rows[1].textContent || '';
569
+ expect(firstRowText).toContain('SELECT * FROM users');
570
+ expect(secondRowText).toContain('DROP TABLE bad');
571
+ });
572
+ });