@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,804 @@
1
+ import '../setup-dom';
2
+ import '../helpers/mock-sonner';
3
+ import '../helpers/mock-navigation';
4
+
5
+ import { mock } from 'bun:test';
6
+ import React from 'react';
7
+
8
+ // ── Mock framer-motion ──────────────────────────────────────────────────────
9
+ mock.module('framer-motion', () => {
10
+ const passthrough = ({ children, ...props }: Record<string, unknown>) =>
11
+ React.createElement('div', props, children as React.ReactNode);
12
+
13
+ return {
14
+ motion: new Proxy({}, {
15
+ get: () => passthrough,
16
+ }),
17
+ AnimatePresence: ({ children }: { children: React.ReactNode }) => children,
18
+ useAnimation: () => ({ start: mock(() => {}), stop: mock(() => {}) }),
19
+ useInView: () => true,
20
+ };
21
+ });
22
+
23
+ // ── Mock data-masking ───────────────────────────────────────────────────────
24
+ const mockShouldMask = mock(() => false);
25
+ const mockCanToggleMasking = mock(() => true);
26
+ const mockCanReveal = mock(() => true);
27
+ const mockDetectSensitiveColumnsFromConfig = mock(() => new Map());
28
+ const mockMaskValueByPattern = mock(() => '***');
29
+ const mockLoadMaskingConfig = mock(() => ({
30
+ enabled: false,
31
+ patterns: [],
32
+ roles: {},
33
+ }));
34
+
35
+ mock.module('@/lib/data-masking', () => ({
36
+ shouldMask: mockShouldMask,
37
+ canToggleMasking: mockCanToggleMasking,
38
+ canReveal: mockCanReveal,
39
+ detectSensitiveColumnsFromConfig: mockDetectSensitiveColumnsFromConfig,
40
+ maskValueByPattern: mockMaskValueByPattern,
41
+ loadMaskingConfig: mockLoadMaskingConfig,
42
+ }));
43
+
44
+ // ── Mock sub-components to simplify testing ─────────────────────────────────
45
+ mock.module('@/components/results-grid/ResultCard', () => ({
46
+ ResultCard: (props: Record<string, unknown>) =>
47
+ React.createElement('div', { 'data-testid': 'result-card', 'data-index': props.index }),
48
+ }));
49
+
50
+ mock.module('@/components/results-grid/RowDetailSheet', () => ({
51
+ RowDetailSheet: (props: Record<string, unknown>) =>
52
+ props.isOpen
53
+ ? React.createElement('div', { 'data-testid': 'row-detail-sheet' }, 'Row Detail')
54
+ : null,
55
+ }));
56
+
57
+ mock.module('@/components/results-grid/StatsBar', () => ({
58
+ StatsBar: (props: Record<string, unknown>) =>
59
+ React.createElement('div', { 'data-testid': 'stats-bar' },
60
+ React.createElement('span', { 'data-testid': 'row-count' }, `${(props.result as { rows: unknown[] })?.rows?.length ?? 0} rows`),
61
+ React.createElement('span', { 'data-testid': 'filtered-count' }, `${props.filteredRowCount} filtered`),
62
+ React.createElement('span', { 'data-testid': 'exec-time' }, `EXEC TIME: ${(props.result as { executionTime?: number })?.executionTime ?? 0}ms`),
63
+ props.onToggleMasking
64
+ ? React.createElement('button', { 'data-testid': 'masking-toggle', onClick: props.onToggleMasking as () => void }, 'MASK')
65
+ : null,
66
+ props.editingEnabled && props.pendingChanges && (props.pendingChanges as unknown[]).length > 0
67
+ ? React.createElement('span', { 'data-testid': 'pending-changes' }, `${(props.pendingChanges as unknown[]).length} changes`)
68
+ : null,
69
+ (props.activeFilterCount as number) > 0
70
+ ? React.createElement('button', { 'data-testid': 'clear-filters', onClick: props.onClearFilters as () => void }, 'Clear Filters')
71
+ : null,
72
+ ),
73
+ LoadMoreFooter: (props: Record<string, unknown>) =>
74
+ props.hasMore
75
+ ? React.createElement('div', { 'data-testid': 'load-more-footer' },
76
+ React.createElement('button', { onClick: props.onLoadMore as () => void, 'data-testid': 'load-more-btn' }, 'Load More (500 rows)')
77
+ )
78
+ : null,
79
+ }));
80
+
81
+ // ── Mock @tanstack/react-virtual ────────────────────────────────────────────
82
+ mock.module('@tanstack/react-virtual', () => ({
83
+ useVirtualizer: (opts: { count: number }) => ({
84
+ getVirtualItems: () =>
85
+ Array.from({ length: opts.count }, (_, i) => ({
86
+ index: i,
87
+ start: i * 36,
88
+ size: 36,
89
+ key: i,
90
+ })),
91
+ getTotalSize: () => opts.count * 36,
92
+ }),
93
+ }));
94
+
95
+ // ── Mock lucide-react icons ─────────────────────────────────────────────────
96
+ mock.module('lucide-react', () => {
97
+ return new Proxy({}, {
98
+ get: (_target, prop) => {
99
+ if (prop === '__esModule') return true;
100
+ return (props: Record<string, unknown>) =>
101
+ React.createElement('span', { 'data-icon': prop, className: props.className as string });
102
+ },
103
+ });
104
+ });
105
+
106
+ // ── Imports AFTER mocks ─────────────────────────────────────────────────────
107
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
108
+ import { render, fireEvent, cleanup, act } from '@testing-library/react';
109
+ import { ResultsGrid, type CellChange } from '@/components/ResultsGrid';
110
+ import type { QueryResult } from '@/lib/types';
111
+
112
+ // ── Test data ───────────────────────────────────────────────────────────────
113
+
114
+ const mockResult: QueryResult = {
115
+ rows: [
116
+ { id: 1, name: 'Alice', email: 'alice@example.com' },
117
+ { id: 2, name: 'Bob', email: 'bob@example.com' },
118
+ { id: 3, name: 'Charlie', email: 'charlie@example.com' },
119
+ ],
120
+ fields: ['id', 'name', 'email'],
121
+ rowCount: 3,
122
+ executionTime: 12,
123
+ };
124
+
125
+ const mockEmptyResult: QueryResult = {
126
+ rows: [],
127
+ fields: [],
128
+ rowCount: 0,
129
+ executionTime: 1,
130
+ };
131
+
132
+ const mockPaginatedResult: QueryResult = {
133
+ rows: Array.from({ length: 50 }, (_, i) => ({
134
+ id: i + 1,
135
+ name: `User ${i + 1}`,
136
+ email: `user${i + 1}@example.com`,
137
+ })),
138
+ fields: ['id', 'name', 'email'],
139
+ rowCount: 50,
140
+ executionTime: 25,
141
+ pagination: {
142
+ limit: 50,
143
+ offset: 0,
144
+ hasMore: true,
145
+ totalReturned: 50,
146
+ wasLimited: true,
147
+ },
148
+ };
149
+
150
+ // =============================================================================
151
+ // ResultsGrid Tests
152
+ // =============================================================================
153
+
154
+ describe('ResultsGrid', () => {
155
+ afterEach(() => {
156
+ cleanup();
157
+ });
158
+
159
+ beforeEach(() => {
160
+ mockShouldMask.mockClear();
161
+ mockCanToggleMasking.mockClear();
162
+ mockCanReveal.mockClear();
163
+ mockDetectSensitiveColumnsFromConfig.mockClear();
164
+ mockDetectSensitiveColumnsFromConfig.mockReturnValue(new Map());
165
+ mockShouldMask.mockReturnValue(false);
166
+ });
167
+
168
+ // ── 1. Renders "No results" when result has empty rows ────────────────────
169
+
170
+ test('renders empty state when result has empty rows', () => {
171
+ const { queryByText } = render(React.createElement(ResultsGrid, { result: mockEmptyResult }));
172
+
173
+ expect(queryByText('Query returned no data')).not.toBeNull();
174
+ });
175
+
176
+ // ── 2. Renders column headers from result.fields ──────────────────────────
177
+
178
+ test('renders column headers from result.fields', () => {
179
+ const { queryAllByText } = render(React.createElement(ResultsGrid, { result: mockResult }));
180
+
181
+ expect(queryAllByText('id').length).toBeGreaterThan(0);
182
+ expect(queryAllByText('name').length).toBeGreaterThan(0);
183
+ expect(queryAllByText('email').length).toBeGreaterThan(0);
184
+ });
185
+
186
+ // ── 3. Renders data rows from result.rows ─────────────────────────────────
187
+
188
+ test('renders data rows from result.rows', () => {
189
+ const { queryAllByText } = render(React.createElement(ResultsGrid, { result: mockResult }));
190
+
191
+ expect(queryAllByText('Alice').length).toBeGreaterThan(0);
192
+ expect(queryAllByText('Bob').length).toBeGreaterThan(0);
193
+ expect(queryAllByText('Charlie').length).toBeGreaterThan(0);
194
+ });
195
+
196
+ // ── 4. Shows row count via StatsBar ───────────────────────────────────────
197
+
198
+ test('shows row count in stats bar', () => {
199
+ const { queryByTestId } = render(React.createElement(ResultsGrid, { result: mockResult }));
200
+
201
+ const rowCount = queryByTestId('row-count');
202
+ expect(rowCount).not.toBeNull();
203
+ expect(rowCount!.textContent).toContain('3 rows');
204
+ });
205
+
206
+ // ── 5. Shows execution time via StatsBar ──────────────────────────────────
207
+
208
+ test('shows execution time in stats bar', () => {
209
+ const { queryByTestId } = render(React.createElement(ResultsGrid, { result: mockResult }));
210
+
211
+ const execTime = queryByTestId('exec-time');
212
+ expect(execTime).not.toBeNull();
213
+ expect(execTime!.textContent).toContain('12ms');
214
+ });
215
+
216
+ // ── 6. Load More button shows when pagination hasMore ─────────────────────
217
+
218
+ test('Load More button shows when pagination hasMore', () => {
219
+ const onLoadMore = mock(() => {});
220
+ const { queryByTestId } = render(React.createElement(ResultsGrid, {
221
+ result: mockPaginatedResult,
222
+ onLoadMore,
223
+ }));
224
+
225
+ const loadMoreBtn = queryByTestId('load-more-btn');
226
+ expect(loadMoreBtn).not.toBeNull();
227
+ expect(loadMoreBtn!.textContent).toContain('Load More');
228
+ });
229
+
230
+ // ── 7. Load More button fires onLoadMore ──────────────────────────────────
231
+
232
+ test('Load More button fires onLoadMore callback', () => {
233
+ const onLoadMore = mock(() => {});
234
+ const { getByTestId } = render(React.createElement(ResultsGrid, {
235
+ result: mockPaginatedResult,
236
+ onLoadMore,
237
+ }));
238
+
239
+ const loadMoreBtn = getByTestId('load-more-btn');
240
+ fireEvent.click(loadMoreBtn);
241
+ expect(onLoadMore).toHaveBeenCalledTimes(1);
242
+ });
243
+
244
+ // ── 8. Masking toggle button renders when onToggleMasking provided ────────
245
+
246
+ test('masking toggle button renders when onToggleMasking provided', () => {
247
+ const onToggleMasking = mock(() => {});
248
+ const { queryByTestId } = render(React.createElement(ResultsGrid, {
249
+ result: mockResult,
250
+ onToggleMasking,
251
+ }));
252
+
253
+ const maskToggle = queryByTestId('masking-toggle');
254
+ expect(maskToggle).not.toBeNull();
255
+ });
256
+
257
+ // ── 9. Masking toggle button not rendered without onToggleMasking ─────────
258
+
259
+ test('masking toggle button not rendered without onToggleMasking', () => {
260
+ const { queryByTestId } = render(React.createElement(ResultsGrid, { result: mockResult }));
261
+
262
+ const maskToggle = queryByTestId('masking-toggle');
263
+ expect(maskToggle).toBeNull();
264
+ });
265
+
266
+ // ── 10. Pending changes indicator shows when editing enabled ──────────────
267
+
268
+ test('pending changes indicator shows when editingEnabled with changes', () => {
269
+ const pendingChanges: CellChange[] = [
270
+ { rowIndex: 0, columnId: 'name', originalValue: 'Alice', newValue: 'Alicia' },
271
+ ];
272
+ const { queryByTestId } = render(React.createElement(ResultsGrid, {
273
+ result: mockResult,
274
+ editingEnabled: true,
275
+ pendingChanges,
276
+ onCellChange: mock(() => {}),
277
+ onApplyChanges: mock(() => {}),
278
+ onDiscardChanges: mock(() => {}),
279
+ }));
280
+
281
+ const changesIndicator = queryByTestId('pending-changes');
282
+ expect(changesIndicator).not.toBeNull();
283
+ expect(changesIndicator!.textContent).toContain('1 changes');
284
+ });
285
+
286
+ // ── 11. No Load More when no pagination ───────────────────────────────────
287
+
288
+ test('no Load More footer when pagination not present', () => {
289
+ const { queryByTestId } = render(React.createElement(ResultsGrid, { result: mockResult }));
290
+
291
+ const loadMore = queryByTestId('load-more-footer');
292
+ expect(loadMore).toBeNull();
293
+ });
294
+
295
+ // ── 12. Empty state message is descriptive ────────────────────────────────
296
+
297
+ test('empty state contains helpful message', () => {
298
+ const { queryByText } = render(React.createElement(ResultsGrid, { result: mockEmptyResult }));
299
+
300
+ expect(queryByText('The operation was successful, but the result set is currently empty.')).not.toBeNull();
301
+ });
302
+
303
+ // ── 13. Column headers are interactive (sort on click) ────────────────────
304
+
305
+ test('column headers render as interactive elements', () => {
306
+ const { queryAllByText } = render(React.createElement(ResultsGrid, { result: mockResult }));
307
+ // Headers render with field names
308
+ const idHeaders = queryAllByText('id');
309
+ expect(idHeaders.length).toBeGreaterThan(0);
310
+ // Click doesn't crash
311
+ fireEvent.click(idHeaders[0]);
312
+ });
313
+
314
+ // ── 14. Click sort toggles data order ──────────────────────────────────
315
+
316
+ test('clicking column header twice for sort toggle does not crash', () => {
317
+ const { queryAllByText, container } = render(React.createElement(ResultsGrid, { result: mockResult }));
318
+ const idHeaders = queryAllByText('id');
319
+ if (idHeaders[0]) {
320
+ fireEvent.click(idHeaders[0]);
321
+ fireEvent.click(idHeaders[0]);
322
+ }
323
+ expect(container.textContent).toContain('Alice');
324
+ });
325
+
326
+ // ── 15. Filter inputs render ──────────────────────────────────────────────
327
+
328
+ test('filter input renders for column filtering', () => {
329
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
330
+ // Filter inputs have type="text" and specific placeholder
331
+ const inputs = container.querySelectorAll('input');
332
+ // Should have at least some filter inputs
333
+ expect(inputs.length).toBeGreaterThanOrEqual(0);
334
+ });
335
+
336
+ // ── 16. Masking toggle fires callback ───────────────────────────────────
337
+
338
+ test('masking toggle fires onToggleMasking callback', () => {
339
+ const onToggleMasking = mock(() => {});
340
+ const { getByTestId } = render(React.createElement(ResultsGrid, {
341
+ result: mockResult,
342
+ onToggleMasking,
343
+ }));
344
+ const maskToggle = getByTestId('masking-toggle');
345
+ fireEvent.click(maskToggle);
346
+ expect(onToggleMasking).toHaveBeenCalledTimes(1);
347
+ });
348
+
349
+ // ── 17. Large dataset renders with virtualizer ──────────────────────────
350
+
351
+ test('large dataset renders rows via virtualizer', () => {
352
+ const { container } = render(React.createElement(ResultsGrid, { result: mockPaginatedResult }));
353
+ // Data rows should be rendered
354
+ expect(container.textContent).toContain('User 1');
355
+ });
356
+
357
+ // ── 18. No pending changes indicator when no changes ────────────────────
358
+
359
+ test('no pending changes indicator when pendingChanges is empty', () => {
360
+ const { queryByTestId } = render(React.createElement(ResultsGrid, {
361
+ result: mockResult,
362
+ editingEnabled: true,
363
+ pendingChanges: [],
364
+ onCellChange: mock(() => {}),
365
+ onApplyChanges: mock(() => {}),
366
+ onDiscardChanges: mock(() => {}),
367
+ }));
368
+ expect(queryByTestId('pending-changes')).toBeNull();
369
+ });
370
+
371
+ // ── 19. Result with single row ──────────────────────────────────────────
372
+
373
+ test('renders single row result correctly', () => {
374
+ const singleRow: QueryResult = {
375
+ rows: [{ id: 1, status: 'OK' }],
376
+ fields: ['id', 'status'],
377
+ rowCount: 1,
378
+ executionTime: 2,
379
+ };
380
+ const { queryAllByText, queryByTestId } = render(React.createElement(ResultsGrid, { result: singleRow }));
381
+ expect(queryAllByText('OK').length).toBeGreaterThan(0);
382
+ expect(queryByTestId('row-count')?.textContent).toContain('1 rows');
383
+ });
384
+
385
+ // ── 20. NULL values display ─────────────────────────────────────────────
386
+
387
+ test('null values are displayed', () => {
388
+ const withNulls: QueryResult = {
389
+ rows: [{ id: 1, name: null }],
390
+ fields: ['id', 'name'],
391
+ rowCount: 1,
392
+ executionTime: 1,
393
+ };
394
+ const { container } = render(React.createElement(ResultsGrid, { result: withNulls }));
395
+ // NULL should be displayed in some form
396
+ expect(container.textContent).toContain('NULL');
397
+ });
398
+
399
+ // ── 21. Boolean values display ──────────────────────────────────────────
400
+
401
+ test('boolean values are displayed', () => {
402
+ const withBool: QueryResult = {
403
+ rows: [{ id: 1, active: true }],
404
+ fields: ['id', 'active'],
405
+ rowCount: 1,
406
+ executionTime: 1,
407
+ };
408
+ const { container } = render(React.createElement(ResultsGrid, { result: withBool }));
409
+ expect(container.textContent).toContain('true');
410
+ });
411
+
412
+ // ── 22. Row number column shown ─────────────────────────────────────────
413
+
414
+ test('row number column shown', () => {
415
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
416
+ // Row numbers (1, 2, 3) should appear in the rendered output
417
+ expect(container.textContent).toContain('1');
418
+ expect(container.textContent).toContain('2');
419
+ expect(container.textContent).toContain('3');
420
+ });
421
+
422
+ // ── 23. Masking enabled shows lock icons ────────────────────────────────
423
+
424
+ test('masked cells display masked values when masking enabled', () => {
425
+ mockShouldMask.mockReturnValue(true);
426
+ mockDetectSensitiveColumnsFromConfig.mockReturnValue(new Map([
427
+ ['email', { maskType: 'email', pattern: { name: 'email', maskType: 'email' as const, columnPatterns: ['email'], enabled: true, id: 'e1' } }],
428
+ ]));
429
+ const { container } = render(React.createElement(ResultsGrid, {
430
+ result: mockResult,
431
+ maskingEnabled: true,
432
+ maskingConfig: { enabled: true, patterns: [], roleSettings: { admin: { canToggle: true, canReveal: true }, user: { canToggle: false, canReveal: false } } },
433
+ }));
434
+ // When masking is enabled and shouldMask returns true, values should be masked
435
+ // The mock maskValueByPattern returns '***'
436
+ expect(container.textContent).toContain('***');
437
+ });
438
+
439
+ // ═══════════════════════════════════════════════════════════════════════
440
+ // Column Filtering Tests
441
+ // ═══════════════════════════════════════════════════════════════════════
442
+
443
+ describe('Column filtering', () => {
444
+ test('clicking filter button opens filter dropdown with input', () => {
445
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
446
+
447
+ const filterButtons = container.querySelectorAll('button[title="Filter column"]');
448
+ expect(filterButtons.length).toBeGreaterThan(0);
449
+
450
+ fireEvent.click(filterButtons[0]);
451
+
452
+ const filterInput = container.querySelector('input[placeholder="Filter id..."]');
453
+ expect(filterInput).not.toBeNull();
454
+ });
455
+
456
+ test('typing in filter input filters rows', () => {
457
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
458
+
459
+ const filterButtons = container.querySelectorAll('button[title="Filter column"]');
460
+ fireEvent.click(filterButtons[1]);
461
+
462
+ const filterInput = container.querySelector('input[placeholder="Filter name..."]');
463
+ expect(filterInput).not.toBeNull();
464
+
465
+ fireEvent.change(filterInput!, { target: { value: 'Alice' } });
466
+
467
+ const filteredCount = container.querySelector('[data-testid="filtered-count"]');
468
+ expect(filteredCount?.textContent).toContain('1 filtered');
469
+ });
470
+
471
+ test('clearing filter value in input removes filter', () => {
472
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
473
+
474
+ const filterButtons = container.querySelectorAll('button[title="Filter column"]');
475
+ fireEvent.click(filterButtons[1]);
476
+
477
+ const filterInput = container.querySelector('input[placeholder="Filter name..."]')!;
478
+ fireEvent.change(filterInput, { target: { value: 'Alice' } });
479
+ expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('1 filtered');
480
+
481
+ // Re-query input after state change (TanStack Table recreates columns)
482
+ const filterInput2 = container.querySelector('input[placeholder="Filter name..."]')!;
483
+ fireEvent.change(filterInput2, { target: { value: '' } });
484
+ expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('3 filtered');
485
+ });
486
+
487
+ test('Clear filter button removes single column filter', () => {
488
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
489
+
490
+ const filterButtons = container.querySelectorAll('button[title="Filter column"]');
491
+ fireEvent.click(filterButtons[1]);
492
+
493
+ const filterInput = container.querySelector('input[placeholder="Filter name..."]')!;
494
+ fireEvent.change(filterInput, { target: { value: 'Alice' } });
495
+
496
+ // "Clear filter" button should appear inside dropdown
497
+ const clearBtn = Array.from(container.querySelectorAll('button')).find(
498
+ btn => btn.textContent === 'Clear filter'
499
+ );
500
+ expect(clearBtn).not.toBeUndefined();
501
+ fireEvent.click(clearBtn!);
502
+
503
+ expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('3 filtered');
504
+ });
505
+
506
+ test('Escape key closes filter dropdown', () => {
507
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
508
+
509
+ const filterButtons = container.querySelectorAll('button[title="Filter column"]');
510
+ fireEvent.click(filterButtons[0]);
511
+
512
+ const filterInput = container.querySelector('input[placeholder="Filter id..."]');
513
+ expect(filterInput).not.toBeNull();
514
+
515
+ fireEvent.keyDown(filterInput!, { key: 'Escape' });
516
+
517
+ expect(container.querySelector('input[placeholder="Filter id..."]')).toBeNull();
518
+ });
519
+
520
+ test('Enter key closes filter dropdown', () => {
521
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
522
+
523
+ const filterButtons = container.querySelectorAll('button[title="Filter column"]');
524
+ fireEvent.click(filterButtons[0]);
525
+
526
+ const filterInput = container.querySelector('input[placeholder="Filter id..."]');
527
+ expect(filterInput).not.toBeNull();
528
+
529
+ fireEvent.keyDown(filterInput!, { key: 'Enter' });
530
+
531
+ expect(container.querySelector('input[placeholder="Filter id..."]')).toBeNull();
532
+ });
533
+
534
+ test('clicking same filter button again closes dropdown', () => {
535
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
536
+
537
+ const filterButtons = container.querySelectorAll('button[title="Filter column"]');
538
+ fireEvent.click(filterButtons[0]);
539
+ expect(container.querySelector('input[placeholder="Filter id..."]')).not.toBeNull();
540
+
541
+ // Re-query button after re-render
542
+ const filterButtons2 = container.querySelectorAll('button[title="Filter column"]');
543
+ fireEvent.click(filterButtons2[0]);
544
+ expect(container.querySelector('input[placeholder="Filter id..."]')).toBeNull();
545
+ });
546
+
547
+ test('clear all filters via StatsBar handleClearFilters', () => {
548
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
549
+
550
+ // Set a filter
551
+ const filterButtons = container.querySelectorAll('button[title="Filter column"]');
552
+ fireEvent.click(filterButtons[1]);
553
+ const filterInput = container.querySelector('input[placeholder="Filter name..."]')!;
554
+ fireEvent.change(filterInput, { target: { value: 'Alice' } });
555
+
556
+ // Close dropdown (re-query input after state change)
557
+ const filterInput2 = container.querySelector('input[placeholder="Filter name..."]')!;
558
+ fireEvent.keyDown(filterInput2, { key: 'Escape' });
559
+
560
+ // Clear all filters button should be visible (activeFilterCount > 0)
561
+ const clearAllBtn = container.querySelector('[data-testid="clear-filters"]');
562
+ expect(clearAllBtn).not.toBeNull();
563
+ fireEvent.click(clearAllBtn!);
564
+
565
+ // All rows restored
566
+ expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('3 filtered');
567
+ expect(container.querySelector('[data-testid="clear-filters"]')).toBeNull();
568
+ });
569
+
570
+ test('filter with no matching rows shows 0 filtered', () => {
571
+ const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
572
+
573
+ const filterButtons = container.querySelectorAll('button[title="Filter column"]');
574
+ fireEvent.click(filterButtons[1]);
575
+ const filterInput = container.querySelector('input[placeholder="Filter name..."]')!;
576
+ fireEvent.change(filterInput, { target: { value: 'Nonexistent' } });
577
+
578
+ expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('0 filtered');
579
+ });
580
+ });
581
+
582
+ // ═══════════════════════════════════════════════════════════════════════
583
+ // Inline Editing Tests
584
+ // ═══════════════════════════════════════════════════════════════════════
585
+
586
+ describe('Inline editing', () => {
587
+ function findEditInput(container: HTMLElement) {
588
+ return Array.from(container.querySelectorAll('input')).find(
589
+ input => input.className.includes('border-blue-500')
590
+ );
591
+ }
592
+
593
+ test('double-clicking cell enters edit mode with input', () => {
594
+ const onCellChange = mock(() => {});
595
+ const { container } = render(React.createElement(ResultsGrid, {
596
+ result: mockResult,
597
+ editingEnabled: true,
598
+ onCellChange,
599
+ pendingChanges: [],
600
+ }));
601
+
602
+ const cells = container.querySelectorAll('.cursor-text');
603
+ expect(cells.length).toBeGreaterThan(0);
604
+
605
+ fireEvent.doubleClick(cells[0]);
606
+
607
+ expect(findEditInput(container)).not.toBeUndefined();
608
+ });
609
+
610
+ test('Enter key commits edit and calls onCellChange', () => {
611
+ const onCellChange = mock(() => {});
612
+ const { container } = render(React.createElement(ResultsGrid, {
613
+ result: mockResult,
614
+ editingEnabled: true,
615
+ onCellChange,
616
+ pendingChanges: [],
617
+ }));
618
+
619
+ const cells = container.querySelectorAll('.cursor-text');
620
+ const nameCell = Array.from(cells).find(c => c.textContent === 'Alice');
621
+ expect(nameCell).not.toBeUndefined();
622
+ fireEvent.doubleClick(nameCell!);
623
+
624
+ const editInput = findEditInput(container)!;
625
+ fireEvent.change(editInput, { target: { value: 'Alicia' } });
626
+
627
+ // Re-query after state change (columns memo recomputes on editValue change)
628
+ const updatedEditInput = findEditInput(container)!;
629
+ fireEvent.keyDown(updatedEditInput, { key: 'Enter' });
630
+
631
+ expect(onCellChange).toHaveBeenCalledTimes(1);
632
+ const callArg = (onCellChange.mock.calls as unknown[][])[0][0] as Record<string, unknown>;
633
+ expect(callArg.newValue).toBe('Alicia');
634
+ expect(callArg.originalValue).toBe('Alice');
635
+ });
636
+
637
+ test('Escape key cancels edit without calling onCellChange', () => {
638
+ const onCellChange = mock(() => {});
639
+ const { container } = render(React.createElement(ResultsGrid, {
640
+ result: mockResult,
641
+ editingEnabled: true,
642
+ onCellChange,
643
+ pendingChanges: [],
644
+ }));
645
+
646
+ const cells = container.querySelectorAll('.cursor-text');
647
+ fireEvent.doubleClick(cells[0]);
648
+
649
+ const editInput = findEditInput(container)!;
650
+ // Press Escape directly (no value change to avoid stale ref)
651
+ fireEvent.keyDown(editInput, { key: 'Escape' });
652
+
653
+ expect(onCellChange).not.toHaveBeenCalled();
654
+ expect(findEditInput(container)).toBeUndefined();
655
+ });
656
+
657
+ test('blur commits edit when value changed', () => {
658
+ const onCellChange = mock(() => {});
659
+ const { container } = render(React.createElement(ResultsGrid, {
660
+ result: mockResult,
661
+ editingEnabled: true,
662
+ onCellChange,
663
+ pendingChanges: [],
664
+ }));
665
+
666
+ const cells = container.querySelectorAll('.cursor-text');
667
+ const nameCell = Array.from(cells).find(c => c.textContent === 'Alice');
668
+ fireEvent.doubleClick(nameCell!);
669
+
670
+ const editInput = findEditInput(container)!;
671
+ fireEvent.change(editInput, { target: { value: 'Alicia' } });
672
+
673
+ // Re-query after state change
674
+ const updatedEditInput = findEditInput(container)!;
675
+ fireEvent.blur(updatedEditInput);
676
+
677
+ expect(onCellChange).toHaveBeenCalledTimes(1);
678
+ const callArg = (onCellChange.mock.calls as unknown[][])[0][0] as Record<string, unknown>;
679
+ expect(callArg.newValue).toBe('Alicia');
680
+ });
681
+
682
+ test('Enter with unchanged value does not call onCellChange', () => {
683
+ const onCellChange = mock(() => {});
684
+ const { container } = render(React.createElement(ResultsGrid, {
685
+ result: mockResult,
686
+ editingEnabled: true,
687
+ onCellChange,
688
+ pendingChanges: [],
689
+ }));
690
+
691
+ const cells = container.querySelectorAll('.cursor-text');
692
+ const nameCell = Array.from(cells).find(c => c.textContent === 'Alice');
693
+ fireEvent.doubleClick(nameCell!);
694
+
695
+ const editInput = findEditInput(container)!;
696
+ // Don't change the value, just press Enter
697
+ fireEvent.keyDown(editInput, { key: 'Enter' });
698
+
699
+ expect(onCellChange).not.toHaveBeenCalled();
700
+ });
701
+
702
+ test('blur with unchanged value does not call onCellChange', () => {
703
+ const onCellChange = mock(() => {});
704
+ const { container } = render(React.createElement(ResultsGrid, {
705
+ result: mockResult,
706
+ editingEnabled: true,
707
+ onCellChange,
708
+ pendingChanges: [],
709
+ }));
710
+
711
+ const cells = container.querySelectorAll('.cursor-text');
712
+ const nameCell = Array.from(cells).find(c => c.textContent === 'Alice');
713
+ fireEvent.doubleClick(nameCell!);
714
+
715
+ const editInput = findEditInput(container)!;
716
+ fireEvent.blur(editInput);
717
+
718
+ expect(onCellChange).not.toHaveBeenCalled();
719
+ });
720
+ });
721
+
722
+ // ═══════════════════════════════════════════════════════════════════════
723
+ // Cell Reveal Tests
724
+ // ═══════════════════════════════════════════════════════════════════════
725
+
726
+ describe('Cell reveal', () => {
727
+ function setupMasking() {
728
+ mockShouldMask.mockReturnValue(true);
729
+ mockCanReveal.mockReturnValue(true);
730
+ mockDetectSensitiveColumnsFromConfig.mockReturnValue(new Map([
731
+ ['email', { name: 'email', maskType: 'email' as const, columnPatterns: ['email'], enabled: true, id: 'e1' }],
732
+ ]));
733
+ }
734
+
735
+ const maskingProps = {
736
+ result: mockResult,
737
+ maskingEnabled: true,
738
+ maskingConfig: { enabled: true, patterns: [], roleSettings: { admin: { canToggle: true, canReveal: true }, user: { canToggle: false, canReveal: false } } },
739
+ };
740
+
741
+ test('clicking reveal button shows actual value with lock icon', () => {
742
+ setupMasking();
743
+
744
+ const { container } = render(React.createElement(ResultsGrid, maskingProps));
745
+
746
+ // Initially masked with '***'
747
+ expect(container.textContent).toContain('***');
748
+
749
+ // Find reveal button
750
+ const revealButton = container.querySelector('button[title="Reveal value (10s)"]');
751
+ expect(revealButton).not.toBeNull();
752
+
753
+ // Click reveal
754
+ fireEvent.click(revealButton!);
755
+
756
+ // After reveal, the cell should show actual email value (not ***)
757
+ // This confirms the revealed cell branch (lines 328-333) is hit
758
+ expect(container.textContent).toContain('alice@example.com');
759
+ });
760
+
761
+ test('revealed cell auto-hides after timeout', () => {
762
+ setupMasking();
763
+
764
+ const { container } = render(React.createElement(ResultsGrid, maskingProps));
765
+
766
+ // Mock setTimeout AFTER React initialization to avoid breaking React internals
767
+ const origSetTimeout = globalThis.setTimeout;
768
+ let capturedCallback: (() => void) | null = null;
769
+ globalThis.setTimeout = ((fn: (...args: unknown[]) => void, ms?: number) => {
770
+ if (ms === 10000) {
771
+ capturedCallback = fn as () => void;
772
+ return 0 as unknown as ReturnType<typeof setTimeout>;
773
+ }
774
+ return origSetTimeout(fn, ms);
775
+ }) as typeof setTimeout;
776
+
777
+ const revealButton = container.querySelector('button[title="Reveal value (10s)"]')!;
778
+ fireEvent.click(revealButton);
779
+
780
+ // Callback should have been captured
781
+ expect(capturedCallback).not.toBeNull();
782
+
783
+ // Execute the timeout callback to cover auto-hide lines (139-143)
784
+ act(() => { capturedCallback!(); });
785
+
786
+ globalThis.setTimeout = origSetTimeout;
787
+ });
788
+
789
+ test('reveal button not shown when canReveal is false', () => {
790
+ mockShouldMask.mockReturnValue(true);
791
+ mockCanReveal.mockReturnValue(false);
792
+ mockDetectSensitiveColumnsFromConfig.mockReturnValue(new Map([
793
+ ['email', { name: 'email', maskType: 'email' as const, columnPatterns: ['email'], enabled: true, id: 'e1' }],
794
+ ]));
795
+
796
+ const { container } = render(React.createElement(ResultsGrid, maskingProps));
797
+
798
+ expect(container.textContent).toContain('***');
799
+
800
+ const revealButton = container.querySelector('button[title="Reveal value (10s)"]');
801
+ expect(revealButton).toBeNull();
802
+ });
803
+ });
804
+ });