@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,467 @@
1
+ import '../../setup-dom';
2
+ import { mockToastSuccess } from '../../helpers/mock-sonner';
3
+ import '../../helpers/mock-navigation';
4
+
5
+ import { mock } from 'bun:test';
6
+ import { setupRechartssMock, setupFramerMotionMock } from '../../helpers/mock-monaco';
7
+
8
+ setupRechartssMock();
9
+ setupFramerMotionMock();
10
+
11
+ mock.module('@/components/MaskingSettings', () => ({
12
+ MaskingSettings: () => {
13
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
14
+ const React = require('react');
15
+ return React.createElement('div', { 'data-testid': 'masking-settings' }, 'MaskingSettings');
16
+ },
17
+ }));
18
+
19
+ mock.module('@/lib/monitoring-thresholds', () => ({
20
+ DEFAULT_THRESHOLDS: [
21
+ { metric: 'cacheHitRatio', warning: 90, critical: 80, direction: 'below' as const, label: 'Cache Hit Ratio' },
22
+ { metric: 'connectionPercent', warning: 70, critical: 90, direction: 'above' as const, label: 'Connection Usage' },
23
+ ],
24
+ }));
25
+
26
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
27
+ import { render, fireEvent, act, waitFor, cleanup } from '@testing-library/react';
28
+ import React from 'react';
29
+
30
+ import { SecurityTab } from '@/components/admin/tabs/SecurityTab';
31
+
32
+ // =============================================================================
33
+ // SecurityTab Tests
34
+ // =============================================================================
35
+
36
+ // Helper: Radix Tabs uses onMouseDown (not onClick) to switch tabs.
37
+ // In happy-dom, fireEvent.click does not trigger onMouseDown in the correct
38
+ // event sequence, so we dispatch mouseDown explicitly.
39
+ function clickRadixTab(element: HTMLElement) {
40
+ fireEvent.mouseDown(element, { button: 0 });
41
+ }
42
+
43
+ describe('SecurityTab', () => {
44
+ afterEach(() => {
45
+ cleanup();
46
+ });
47
+
48
+ beforeEach(() => {
49
+ // Clear localStorage between tests
50
+ if (typeof localStorage !== 'undefined') {
51
+ localStorage.removeItem('libredb_threshold_config');
52
+ }
53
+ });
54
+
55
+ test('renders 3 tabs (Data Masking, Access, Thresholds)', async () => {
56
+ let renderResult: ReturnType<typeof render>;
57
+ await act(async () => {
58
+ renderResult = render(<SecurityTab />);
59
+ });
60
+ const { queryByText } = renderResult!;
61
+
62
+ expect(queryByText('Data Masking')).not.toBeNull();
63
+ expect(queryByText('Access')).not.toBeNull();
64
+ expect(queryByText('Thresholds')).not.toBeNull();
65
+ });
66
+
67
+ test('masking tab shows MaskingSettings component', async () => {
68
+ let renderResult: ReturnType<typeof render>;
69
+ await act(async () => {
70
+ renderResult = render(<SecurityTab />);
71
+ });
72
+ const { queryByTestId, queryByText } = renderResult!;
73
+
74
+ // Data Masking is the default tab, so MaskingSettings should be visible
75
+ expect(queryByTestId('masking-settings')).not.toBeNull();
76
+ expect(queryByText('MaskingSettings')).not.toBeNull();
77
+ });
78
+
79
+ test('access tab shows security summary cards', async () => {
80
+ let renderResult: ReturnType<typeof render>;
81
+ await act(async () => {
82
+ renderResult = render(<SecurityTab />);
83
+ });
84
+ const { getByText, queryByText } = renderResult!;
85
+
86
+ // Click the Access tab using mouseDown (Radix listens to onMouseDown)
87
+ await act(async () => {
88
+ clickRadixTab(getByText('Access'));
89
+ });
90
+
91
+ // Access summary shows these labels
92
+ await waitFor(() => {
93
+ expect(queryByText('Security & Access')).not.toBeNull();
94
+ expect(queryByText('Authentication')).not.toBeNull();
95
+ expect(queryByText('API Security')).not.toBeNull();
96
+ expect(queryByText('Connection Security')).not.toBeNull();
97
+ });
98
+ });
99
+
100
+ test('thresholds tab shows metric sliders', async () => {
101
+ let renderResult: ReturnType<typeof render>;
102
+ await act(async () => {
103
+ renderResult = render(<SecurityTab />);
104
+ });
105
+ const { getByText, queryByText } = renderResult!;
106
+
107
+ // Click the Thresholds tab using mouseDown (Radix listens to onMouseDown)
108
+ await act(async () => {
109
+ clickRadixTab(getByText('Thresholds'));
110
+ });
111
+
112
+ // Threshold settings should show metric labels
113
+ await waitFor(() => {
114
+ expect(queryByText('Monitoring Thresholds')).not.toBeNull();
115
+ expect(queryByText('Cache Hit Ratio')).not.toBeNull();
116
+ expect(queryByText('Connection Usage')).not.toBeNull();
117
+ });
118
+ });
119
+
120
+ test('save button present in thresholds', async () => {
121
+ let renderResult: ReturnType<typeof render>;
122
+ await act(async () => {
123
+ renderResult = render(<SecurityTab />);
124
+ });
125
+ const { getByText, queryByText } = renderResult!;
126
+
127
+ // Click the Thresholds tab using mouseDown (Radix listens to onMouseDown)
128
+ await act(async () => {
129
+ clickRadixTab(getByText('Thresholds'));
130
+ });
131
+
132
+ await waitFor(() => {
133
+ expect(queryByText('Save Config')).not.toBeNull();
134
+ });
135
+ });
136
+
137
+ test('reset button present in thresholds', async () => {
138
+ let renderResult: ReturnType<typeof render>;
139
+ await act(async () => {
140
+ renderResult = render(<SecurityTab />);
141
+ });
142
+ const { getByText, queryByText } = renderResult!;
143
+
144
+ // Click the Thresholds tab using mouseDown (Radix listens to onMouseDown)
145
+ await act(async () => {
146
+ clickRadixTab(getByText('Thresholds'));
147
+ });
148
+
149
+ await waitFor(() => {
150
+ expect(queryByText('Reset Defaults')).not.toBeNull();
151
+ });
152
+ });
153
+
154
+ // ===========================================================================
155
+ // NEW TESTS
156
+ // ===========================================================================
157
+
158
+ test('access tab shows security badges (ENABLED, Supported, Configurable)', async () => {
159
+ let renderResult: ReturnType<typeof render>;
160
+ await act(async () => {
161
+ renderResult = render(<SecurityTab />);
162
+ });
163
+ const { getByText, queryAllByText, queryByText } = renderResult!;
164
+
165
+ await act(async () => {
166
+ clickRadixTab(getByText('Access'));
167
+ });
168
+
169
+ await waitFor(() => {
170
+ // Admin Access and User Access both have ENABLED badges
171
+ const enabledBadges = queryAllByText('ENABLED');
172
+ expect(enabledBadges.length).toBe(2);
173
+
174
+ // SSL/TLS and SSH Tunnel have Supported badges
175
+ const supportedBadges = queryAllByText('Supported');
176
+ expect(supportedBadges.length).toBe(2);
177
+
178
+ // Data Masking has Configurable badge
179
+ expect(queryByText('Configurable')).not.toBeNull();
180
+ });
181
+ });
182
+
183
+ test('access tab shows JWT and RBAC info', async () => {
184
+ let renderResult: ReturnType<typeof render>;
185
+ await act(async () => {
186
+ renderResult = render(<SecurityTab />);
187
+ });
188
+ const { getByText, queryByText } = renderResult!;
189
+
190
+ await act(async () => {
191
+ clickRadixTab(getByText('Access'));
192
+ });
193
+
194
+ await waitFor(() => {
195
+ // Authentication method shows RBAC
196
+ expect(queryByText('Environment Variable (RBAC)')).not.toBeNull();
197
+ // API security shows JWT
198
+ expect(queryByText('JWT / HTTP-only Cookie')).not.toBeNull();
199
+ });
200
+ });
201
+
202
+ test('thresholds tab shows direction text (Alert when above/below)', async () => {
203
+ let renderResult: ReturnType<typeof render>;
204
+ await act(async () => {
205
+ renderResult = render(<SecurityTab />);
206
+ });
207
+ const { getByText, queryByText } = renderResult!;
208
+
209
+ await act(async () => {
210
+ clickRadixTab(getByText('Thresholds'));
211
+ });
212
+
213
+ await waitFor(() => {
214
+ // cacheHitRatio has direction 'below'
215
+ expect(queryByText('Alert when below')).not.toBeNull();
216
+ // connectionPercent has direction 'above'
217
+ expect(queryByText('Alert when above')).not.toBeNull();
218
+ });
219
+ });
220
+
221
+ test('save button is disabled when no changes have been made', async () => {
222
+ let renderResult: ReturnType<typeof render>;
223
+ await act(async () => {
224
+ renderResult = render(<SecurityTab />);
225
+ });
226
+ const { getByText } = renderResult!;
227
+
228
+ await act(async () => {
229
+ clickRadixTab(getByText('Thresholds'));
230
+ });
231
+
232
+ await waitFor(() => {
233
+ const saveButton = getByText('Save Config').closest('button');
234
+ expect(saveButton).not.toBeNull();
235
+ expect(saveButton!.disabled).toBe(true);
236
+ });
237
+ });
238
+
239
+ test('save button becomes enabled after slider change', async () => {
240
+ let renderResult: ReturnType<typeof render>;
241
+ await act(async () => {
242
+ renderResult = render(<SecurityTab />);
243
+ });
244
+ const { getByText, container } = renderResult!;
245
+
246
+ await act(async () => {
247
+ clickRadixTab(getByText('Thresholds'));
248
+ });
249
+
250
+ // Find a slider thumb and press ArrowRight to change the value
251
+ await waitFor(() => {
252
+ const sliders = container.querySelectorAll('[role="slider"]');
253
+ expect(sliders.length).toBeGreaterThan(0);
254
+ });
255
+
256
+ const sliders = container.querySelectorAll('[role="slider"]');
257
+ await act(async () => {
258
+ (sliders[0] as HTMLElement).focus();
259
+ fireEvent.keyDown(sliders[0], { key: 'ArrowRight' });
260
+ });
261
+
262
+ await waitFor(() => {
263
+ const saveButton = getByText('Save Config').closest('button');
264
+ expect(saveButton).not.toBeNull();
265
+ expect(saveButton!.disabled).toBe(false);
266
+ });
267
+ });
268
+
269
+ test('save config writes to localStorage and shows success toast', async () => {
270
+ mockToastSuccess.mockClear();
271
+
272
+ let renderResult: ReturnType<typeof render>;
273
+ await act(async () => {
274
+ renderResult = render(<SecurityTab />);
275
+ });
276
+ const { getByText, container } = renderResult!;
277
+
278
+ await act(async () => {
279
+ clickRadixTab(getByText('Thresholds'));
280
+ });
281
+
282
+ // Change a slider value so Save becomes enabled
283
+ await waitFor(() => {
284
+ const sliders = container.querySelectorAll('[role="slider"]');
285
+ expect(sliders.length).toBeGreaterThan(0);
286
+ });
287
+
288
+ const sliders = container.querySelectorAll('[role="slider"]');
289
+ await act(async () => {
290
+ (sliders[0] as HTMLElement).focus();
291
+ fireEvent.keyDown(sliders[0], { key: 'ArrowRight' });
292
+ });
293
+
294
+ // Click Save Config
295
+ await act(async () => {
296
+ fireEvent.click(getByText('Save Config'));
297
+ });
298
+
299
+ // Verify localStorage was written
300
+ const stored = localStorage.getItem('libredb_threshold_config');
301
+ expect(stored).not.toBeNull();
302
+ const parsed = JSON.parse(stored!);
303
+ expect(Array.isArray(parsed)).toBe(true);
304
+ expect(parsed.length).toBe(2);
305
+
306
+ // Verify success toast
307
+ expect(mockToastSuccess).toHaveBeenCalledWith('Threshold configuration saved');
308
+ });
309
+
310
+ test('reset defaults restores values and shows success toast', async () => {
311
+ mockToastSuccess.mockClear();
312
+
313
+ // Pre-seed localStorage with custom thresholds
314
+ const custom = [
315
+ { metric: 'cacheHitRatio', warning: 50, critical: 30, direction: 'below', label: 'Cache Hit Ratio' },
316
+ { metric: 'connectionPercent', warning: 60, critical: 85, direction: 'above', label: 'Connection Usage' },
317
+ ];
318
+ localStorage.setItem('libredb_threshold_config', JSON.stringify(custom));
319
+
320
+ let renderResult: ReturnType<typeof render>;
321
+ await act(async () => {
322
+ renderResult = render(<SecurityTab />);
323
+ });
324
+ const { getByText, queryByText } = renderResult!;
325
+
326
+ await act(async () => {
327
+ clickRadixTab(getByText('Thresholds'));
328
+ });
329
+
330
+ // Click Reset Defaults
331
+ await act(async () => {
332
+ fireEvent.click(getByText('Reset Defaults'));
333
+ });
334
+
335
+ // Verify default values are shown (from the mock DEFAULT_THRESHOLDS)
336
+ await waitFor(() => {
337
+ // Cache Hit Ratio warning=90, critical=80 (defaults)
338
+ expect(queryByText('Cache Hit Ratio')).not.toBeNull();
339
+ expect(queryByText('Connection Usage')).not.toBeNull();
340
+ });
341
+
342
+ // Verify localStorage was overwritten with defaults
343
+ const stored = localStorage.getItem('libredb_threshold_config');
344
+ expect(stored).not.toBeNull();
345
+ const parsed = JSON.parse(stored!);
346
+ expect(parsed[0].warning).toBe(90);
347
+ expect(parsed[0].critical).toBe(80);
348
+
349
+ // Verify success toast
350
+ expect(mockToastSuccess).toHaveBeenCalledWith('Thresholds reset to defaults');
351
+ });
352
+
353
+ test('loads thresholds from localStorage on mount', async () => {
354
+ // Pre-seed localStorage with custom values
355
+ const custom = [
356
+ { metric: 'cacheHitRatio', warning: 55, critical: 40, direction: 'below', label: 'Cache Hit Ratio' },
357
+ { metric: 'connectionPercent', warning: 65, critical: 88, direction: 'above', label: 'Connection Usage' },
358
+ ];
359
+ localStorage.setItem('libredb_threshold_config', JSON.stringify(custom));
360
+
361
+ let renderResult: ReturnType<typeof render>;
362
+ await act(async () => {
363
+ renderResult = render(<SecurityTab />);
364
+ });
365
+ const { getByText, queryByText } = renderResult!;
366
+
367
+ await act(async () => {
368
+ clickRadixTab(getByText('Thresholds'));
369
+ });
370
+
371
+ // The custom warning/critical values should appear in the rendered output
372
+ await waitFor(() => {
373
+ // cacheHitRatio warning=55 should be displayed
374
+ expect(queryByText('55%')).not.toBeNull();
375
+ // cacheHitRatio critical=40 should be displayed
376
+ expect(queryByText('40%')).not.toBeNull();
377
+ // connectionPercent warning=65 should be displayed
378
+ expect(queryByText('65%')).not.toBeNull();
379
+ // connectionPercent critical=88 should be displayed
380
+ expect(queryByText('88%')).not.toBeNull();
381
+ });
382
+ });
383
+
384
+ test('percentage suffix is shown for percent metrics', async () => {
385
+ let renderResult: ReturnType<typeof render>;
386
+ await act(async () => {
387
+ renderResult = render(<SecurityTab />);
388
+ });
389
+ const { getByText, queryAllByText, queryByText } = renderResult!;
390
+
391
+ await act(async () => {
392
+ clickRadixTab(getByText('Thresholds'));
393
+ });
394
+
395
+ // Both mock thresholds are percent metrics (not 'deadlocks')
396
+ // cacheHitRatio: warning=90, critical=80
397
+ // connectionPercent: warning=70, critical=90
398
+ await waitFor(() => {
399
+ // 90% appears twice (cacheHitRatio warning + connectionPercent critical)
400
+ const ninetyPercent = queryAllByText('90%');
401
+ expect(ninetyPercent.length).toBe(2);
402
+
403
+ // 80% appears once (cacheHitRatio critical)
404
+ expect(queryByText('80%')).not.toBeNull();
405
+
406
+ // 70% appears once (connectionPercent warning)
407
+ expect(queryByText('70%')).not.toBeNull();
408
+ });
409
+ });
410
+
411
+ test('warning and critical slider labels render', async () => {
412
+ let renderResult: ReturnType<typeof render>;
413
+ await act(async () => {
414
+ renderResult = render(<SecurityTab />);
415
+ });
416
+ const { getByText, queryAllByText } = renderResult!;
417
+
418
+ await act(async () => {
419
+ clickRadixTab(getByText('Thresholds'));
420
+ });
421
+
422
+ await waitFor(() => {
423
+ // Each metric has a Warning and Critical label (2 metrics = 2 of each)
424
+ const warningLabels = queryAllByText('Warning');
425
+ expect(warningLabels.length).toBe(2);
426
+
427
+ const criticalLabels = queryAllByText('Critical');
428
+ expect(criticalLabels.length).toBe(2);
429
+ });
430
+ });
431
+
432
+ test('thresholds tab renders all metric rows with full structure', async () => {
433
+ let renderResult: ReturnType<typeof render>;
434
+ await act(async () => {
435
+ renderResult = render(<SecurityTab />);
436
+ });
437
+ const { getByText, container, queryByText } = renderResult!;
438
+
439
+ await act(async () => {
440
+ clickRadixTab(getByText('Thresholds'));
441
+ });
442
+
443
+ await waitFor(() => {
444
+ // Title and description are present
445
+ expect(queryByText('Monitoring Thresholds')).not.toBeNull();
446
+ expect(
447
+ queryByText(/Configure warning and critical thresholds/)
448
+ ).not.toBeNull();
449
+
450
+ // Both metric labels
451
+ expect(queryByText('Cache Hit Ratio')).not.toBeNull();
452
+ expect(queryByText('Connection Usage')).not.toBeNull();
453
+
454
+ // Both direction texts
455
+ expect(queryByText('Alert when below')).not.toBeNull();
456
+ expect(queryByText('Alert when above')).not.toBeNull();
457
+
458
+ // Slider elements: 2 metrics x 2 sliders (warning + critical) = 4 slider thumbs
459
+ const sliderThumbs = container.querySelectorAll('[role="slider"]');
460
+ expect(sliderThumbs.length).toBe(4);
461
+
462
+ // Both action buttons
463
+ expect(queryByText('Save Config')).not.toBeNull();
464
+ expect(queryByText('Reset Defaults')).not.toBeNull();
465
+ });
466
+ });
467
+ });
@@ -0,0 +1,111 @@
1
+ import '../../setup-dom';
2
+ import { mock } from 'bun:test';
3
+ import React from 'react';
4
+
5
+ // Mock recharts — DOM-only environment can't render SVG charts
6
+ mock.module('recharts', () => ({
7
+ AreaChart: ({ children, data }: { children: React.ReactNode; data: unknown[] }) =>
8
+ React.createElement('div', { 'data-testid': 'area-chart', 'data-count': data.length }, children),
9
+ Area: (props: Record<string, unknown>) =>
10
+ React.createElement('div', { 'data-testid': 'area', 'data-color': props.stroke }),
11
+ XAxis: () => React.createElement('div', { 'data-testid': 'x-axis' }),
12
+ YAxis: () => React.createElement('div', { 'data-testid': 'y-axis' }),
13
+ ResponsiveContainer: ({ children }: { children: React.ReactNode }) =>
14
+ React.createElement('div', { 'data-testid': 'responsive-container' }, children),
15
+ Tooltip: () => React.createElement('div', { 'data-testid': 'tooltip' }),
16
+ }));
17
+
18
+ const { MetricChart } = await import('@/components/monitoring/tabs/MetricChart');
19
+
20
+ import { afterEach, describe, expect, test } from 'bun:test';
21
+ import { cleanup, render } from '@testing-library/react';
22
+
23
+ describe('MetricChart', () => {
24
+ afterEach(() => { cleanup(); });
25
+
26
+ test('shows collecting message when data has 0 points', () => {
27
+ const { getByText } = render(
28
+ <MetricChart data={[]} color="#3b82f6" title="CPU Usage" />
29
+ );
30
+ expect(getByText('Collecting data for CPU Usage...')).not.toBeNull();
31
+ });
32
+
33
+ test('shows collecting message when data has 1 point', () => {
34
+ const { getByText } = render(
35
+ <MetricChart data={[{ timestamp: Date.now(), value: 42 }]} color="#3b82f6" title="Memory" />
36
+ );
37
+ expect(getByText('Collecting data for Memory...')).not.toBeNull();
38
+ });
39
+
40
+ test('does not render chart when data has fewer than 2 points', () => {
41
+ const { queryByTestId } = render(
42
+ <MetricChart data={[{ timestamp: Date.now(), value: 10 }]} color="#f00" title="Test" />
43
+ );
44
+ expect(queryByTestId('area-chart')).toBeNull();
45
+ });
46
+
47
+ test('renders chart when data has 2+ points', () => {
48
+ const data = [
49
+ { timestamp: 1000, value: 10 },
50
+ { timestamp: 2000, value: 20 },
51
+ ];
52
+ const { getByTestId, queryByText } = render(
53
+ <MetricChart data={data} color="#22c55e" title="Connections" />
54
+ );
55
+ expect(getByTestId('area-chart')).not.toBeNull();
56
+ expect(getByTestId('responsive-container')).not.toBeNull();
57
+ expect(queryByText(/Collecting data/)).toBeNull();
58
+ });
59
+
60
+ test('passes data length to AreaChart', () => {
61
+ const data = [
62
+ { timestamp: 1000, value: 10 },
63
+ { timestamp: 2000, value: 20 },
64
+ { timestamp: 3000, value: 30 },
65
+ ];
66
+ const { getByTestId } = render(
67
+ <MetricChart data={data} color="#3b82f6" title="QPS" />
68
+ );
69
+ expect(getByTestId('area-chart').getAttribute('data-count')).toBe('3');
70
+ });
71
+
72
+ test('passes color to Area stroke', () => {
73
+ const data = [
74
+ { timestamp: 1000, value: 10 },
75
+ { timestamp: 2000, value: 20 },
76
+ ];
77
+ const { getByTestId } = render(
78
+ <MetricChart data={data} color="#ef4444" title="Errors" />
79
+ );
80
+ expect(getByTestId('area').getAttribute('data-color')).toBe('#ef4444');
81
+ });
82
+
83
+ test('renders all chart sub-components', () => {
84
+ const data = [
85
+ { timestamp: 1000, value: 10 },
86
+ { timestamp: 2000, value: 20 },
87
+ ];
88
+ const { getByTestId } = render(
89
+ <MetricChart data={data} color="#3b82f6" title="Latency" unit="ms" />
90
+ );
91
+ expect(getByTestId('x-axis')).not.toBeNull();
92
+ expect(getByTestId('y-axis')).not.toBeNull();
93
+ expect(getByTestId('tooltip')).not.toBeNull();
94
+ expect(getByTestId('area')).not.toBeNull();
95
+ });
96
+
97
+ test('uses title in collecting message', () => {
98
+ const { getByText } = render(
99
+ <MetricChart data={[]} color="#3b82f6" title="Custom Metric" />
100
+ );
101
+ expect(getByText('Collecting data for Custom Metric...')).not.toBeNull();
102
+ });
103
+
104
+ test('defaults unit to empty string', () => {
105
+ const { container } = render(
106
+ <MetricChart data={[]} color="#3b82f6" title="Test" />
107
+ );
108
+ // Should render without error (unit defaults to '')
109
+ expect(container).not.toBeNull();
110
+ });
111
+ });