@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.
- package/.claude/settings.local.json +127 -0
- package/.cursorrules +426 -0
- package/.devin/wiki.json +143 -0
- package/.dockerignore +80 -0
- package/.env.example +159 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +57 -0
- package/.github/workflows/ci.yml +185 -0
- package/.github/workflows/codeql.yml +57 -0
- package/.github/workflows/docker-build-push.yml +118 -0
- package/.github/workflows/helm-release.yml +113 -0
- package/CLAUDE.md +265 -0
- package/CODE_OF_CONDUCT.md +124 -0
- package/CONTRIBUTING.md +154 -0
- package/Dockerfile +73 -0
- package/LICENSE +21 -0
- package/README.md +614 -0
- package/SECURITY.md +107 -0
- package/artifacthub-repo.yml +4 -0
- package/bun.lock +1714 -0
- package/bunfig.toml +3 -0
- package/charts/libredb-studio/.helmignore +11 -0
- package/charts/libredb-studio/Chart.lock +6 -0
- package/charts/libredb-studio/Chart.yaml +50 -0
- package/charts/libredb-studio/README.md +206 -0
- package/charts/libredb-studio/templates/NOTES.txt +59 -0
- package/charts/libredb-studio/templates/_helpers.tpl +135 -0
- package/charts/libredb-studio/templates/configmap.yaml +37 -0
- package/charts/libredb-studio/templates/deployment.yaml +184 -0
- package/charts/libredb-studio/templates/hpa.yaml +32 -0
- package/charts/libredb-studio/templates/ingress.yaml +41 -0
- package/charts/libredb-studio/templates/networkpolicy.yaml +50 -0
- package/charts/libredb-studio/templates/pdb.yaml +18 -0
- package/charts/libredb-studio/templates/pvc.yaml +23 -0
- package/charts/libredb-studio/templates/secret.yaml +30 -0
- package/charts/libredb-studio/templates/seed-configmap.yaml +11 -0
- package/charts/libredb-studio/templates/service.yaml +22 -0
- package/charts/libredb-studio/templates/serviceaccount.yaml +13 -0
- package/charts/libredb-studio/values.schema.json +246 -0
- package/charts/libredb-studio/values.yaml +286 -0
- package/components.json +22 -0
- package/conductor/code_styleguides/typescript.md +43 -0
- package/conductor/product-guidelines.md +43 -0
- package/conductor/product.md +3 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +39 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/metadata.json +8 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/plan.md +44 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/spec.md +31 -0
- package/conductor/tracks.md +8 -0
- package/conductor/workflow.md +333 -0
- package/database-compose.yml +55 -0
- package/docker/postgres-init/01-extensions.sql +10 -0
- package/docker/postgres-init/02-sample-data.sql +585 -0
- package/docker/postgres.yml +68 -0
- package/docker-compose.yml +38 -0
- package/docs/AI_PLAN.md +74 -0
- package/docs/API_DOCS.md +875 -0
- package/docs/ARCHITECTURE.md +218 -0
- package/docs/DATABASE_PROVIDERS.md +358 -0
- package/docs/FEATURES.md +116 -0
- package/docs/HELM_CHART.md +252 -0
- package/docs/LOGIN_PAGE.md +178 -0
- package/docs/MONACO_EDITOR_PERFORMANCE.md +315 -0
- package/docs/OIDC_ARCH.md +681 -0
- package/docs/OIDC_SETUP.md +322 -0
- package/docs/POSTGRES_METRICS.md +516 -0
- package/docs/QUERY_OPTIMIZATION.md +370 -0
- package/docs/SEED_CONNECTIONS.md +468 -0
- package/docs/SQL_ALIAS_COMPLETION.md +190 -0
- package/docs/STORAGE_ARCHITECTURE.md +565 -0
- package/docs/STORAGE_QUICK_SETUP.md +419 -0
- package/docs/TECHNICAL_PLAN.md +36 -0
- package/docs/THEMING.md +345 -0
- package/docs/adding-a-new-database-provider.md +642 -0
- package/docs/backlogs/000-PLATFORM_DATA_SYNC_DATABASE.md +360 -0
- package/docs/backlogs/001-INLINE_DATA_EDITING.md +118 -0
- package/docs/backlogs/002-DATA_IMPORT.md +215 -0
- package/docs/backlogs/003-QUERY_TIME_MACHINE.md +183 -0
- package/docs/backlogs/004-AI_DATA_STORYTELLER.md +292 -0
- package/docs/backlogs/005-QUERY_PLAYGROUND.md +352 -0
- package/docs/backlogs/006-DATA_MASKING.md +418 -0
- package/docs/enterprise-features.md +718 -0
- package/docs/kubernetes-helm-chart-artifacthub-plan.md +803 -0
- package/docs/medium-koyeb-article-en.md +215 -0
- package/docs/plans/test-plans.md +445 -0
- package/docs/releases/RELEASE.V0.3.0.md +22 -0
- package/docs/releases/RELEASE.V0.4.0.md +154 -0
- package/docs/releases/RELEASE.V0.5.0.md +252 -0
- package/docs/releases/RELEASE_v0.5.6.md +145 -0
- package/docs/releases/RELEASE_v0.6.1.md +303 -0
- package/docs/releases/RELEASE_v0.6.7.md +292 -0
- package/docs/releases/RELEASE_v0.7.0.md +332 -0
- package/docs/releases/RELEASE_v0.8.0.md +521 -0
- package/docs/sampledb/titanic.sql +1379 -0
- package/docs/superpowers/plans/2026-03-25-seed-connections.md +1362 -0
- package/docs/superpowers/specs/2026-03-25-seed-connections-design.md +590 -0
- package/e2e/admin-dashboard.spec.ts +64 -0
- package/e2e/connection-management.spec.ts +58 -0
- package/e2e/export.spec.ts +34 -0
- package/e2e/login.spec.ts +85 -0
- package/e2e/query-execution.spec.ts +35 -0
- package/e2e/tab-management.spec.ts +64 -0
- package/eslint.config.mjs +28 -0
- package/fly.toml +43 -0
- package/next.config.ts +32 -0
- package/package.json +130 -0
- package/playwright.config.ts +34 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +32 -0
- package/public/next.svg +1 -0
- package/public/screenshots/code-generator.png +0 -0
- package/public/screenshots/connection-modal.png +0 -0
- package/public/screenshots/data-profiler.png +0 -0
- package/public/screenshots/erd-diagram.png +0 -0
- package/public/screenshots/hero-editor.png +0 -0
- package/public/screenshots/nl2sql.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/render.yaml +58 -0
- package/scripts/merge-lcov.mjs +239 -0
- package/sonar-project.properties +16 -0
- package/src/app/admin/error.tsx +46 -0
- package/src/app/admin/page.tsx +10 -0
- package/src/app/api/admin/audit/route.ts +52 -0
- package/src/app/api/admin/fleet-health/route.ts +81 -0
- package/src/app/api/ai/autopilot/route.ts +105 -0
- package/src/app/api/ai/chat/route.ts +132 -0
- package/src/app/api/ai/describe-schema/route.ts +52 -0
- package/src/app/api/ai/explain/route.ts +86 -0
- package/src/app/api/ai/impact/route.ts +97 -0
- package/src/app/api/ai/index-advisor/route.ts +98 -0
- package/src/app/api/ai/nl2sql/route.ts +87 -0
- package/src/app/api/ai/query-safety/route.ts +87 -0
- package/src/app/api/auth/login/route.ts +62 -0
- package/src/app/api/auth/logout/route.ts +25 -0
- package/src/app/api/auth/me/route.ts +10 -0
- package/src/app/api/auth/oidc/callback/route.ts +82 -0
- package/src/app/api/auth/oidc/login/route.ts +43 -0
- package/src/app/api/connections/managed/route.ts +35 -0
- package/src/app/api/db/cancel/route.ts +42 -0
- package/src/app/api/db/disconnect/route.ts +28 -0
- package/src/app/api/db/health/route.ts +49 -0
- package/src/app/api/db/maintenance/route.ts +72 -0
- package/src/app/api/db/monitoring/route.ts +62 -0
- package/src/app/api/db/multi-query/route.ts +116 -0
- package/src/app/api/db/pool-stats/route.ts +37 -0
- package/src/app/api/db/profile/route.ts +144 -0
- package/src/app/api/db/provider-meta/route.ts +49 -0
- package/src/app/api/db/query/route.ts +50 -0
- package/src/app/api/db/schema/route.ts +47 -0
- package/src/app/api/db/schema-snapshot/route.ts +42 -0
- package/src/app/api/db/test-connection/route.ts +55 -0
- package/src/app/api/db/transaction/route.ts +111 -0
- package/src/app/api/storage/[collection]/route.ts +67 -0
- package/src/app/api/storage/config/route.ts +17 -0
- package/src/app/api/storage/migrate/route.ts +45 -0
- package/src/app/api/storage/route.ts +32 -0
- package/src/app/error.tsx +49 -0
- package/src/app/global-error.tsx +55 -0
- package/src/app/globals.css +146 -0
- package/src/app/icon.svg +42 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/login/login-form.tsx +301 -0
- package/src/app/login/page.tsx +11 -0
- package/src/app/monitoring/page.tsx +8 -0
- package/src/app/not-found.tsx +29 -0
- package/src/app/page.tsx +5 -0
- package/src/components/AIAutopilotPanel.tsx +238 -0
- package/src/components/CodeGenerator.tsx +271 -0
- package/src/components/CommandPalette.tsx +227 -0
- package/src/components/ConnectionModal.tsx +759 -0
- package/src/components/CreateTableModal.tsx +281 -0
- package/src/components/DataCharts.tsx +962 -0
- package/src/components/DataImportModal.tsx +582 -0
- package/src/components/DataProfiler.tsx +335 -0
- package/src/components/DatabaseDocs.tsx +251 -0
- package/src/components/MaskingSettings.tsx +414 -0
- package/src/components/MobileNav.tsx +50 -0
- package/src/components/NL2SQLPanel.tsx +281 -0
- package/src/components/PivotTable.tsx +257 -0
- package/src/components/QueryEditor.tsx +760 -0
- package/src/components/QueryHistory.tsx +344 -0
- package/src/components/QuerySafetyDialog.tsx +290 -0
- package/src/components/ResultsGrid.tsx +644 -0
- package/src/components/SaveQueryModal.tsx +104 -0
- package/src/components/SavedQueries.tsx +128 -0
- package/src/components/SchemaDiagram.tsx +473 -0
- package/src/components/SchemaDiff.tsx +473 -0
- package/src/components/SnapshotTimeline.tsx +116 -0
- package/src/components/Studio.tsx +639 -0
- package/src/components/TestDataGenerator.tsx +261 -0
- package/src/components/VisualExplain.tsx +820 -0
- package/src/components/admin/AdminDashboard.tsx +163 -0
- package/src/components/admin/tabs/AuditTab.tsx +531 -0
- package/src/components/admin/tabs/MonitoringEmbed.tsx +11 -0
- package/src/components/admin/tabs/OperationsTab.tsx +646 -0
- package/src/components/admin/tabs/OverviewTab.tsx +1328 -0
- package/src/components/admin/tabs/SecurityTab.tsx +284 -0
- package/src/components/community-section.tsx +92 -0
- package/src/components/icons/db-icons.tsx +84 -0
- package/src/components/libredb-logo.tsx +61 -0
- package/src/components/monitoring/MonitoringDashboard.tsx +345 -0
- package/src/components/monitoring/tabs/MetricChart.tsx +82 -0
- package/src/components/monitoring/tabs/OverviewTab.tsx +263 -0
- package/src/components/monitoring/tabs/PerformanceTab.tsx +254 -0
- package/src/components/monitoring/tabs/PoolTab.tsx +174 -0
- package/src/components/monitoring/tabs/QueriesTab.tsx +287 -0
- package/src/components/monitoring/tabs/SessionsTab.tsx +316 -0
- package/src/components/monitoring/tabs/StorageTab.tsx +335 -0
- package/src/components/monitoring/tabs/TablesTab.tsx +300 -0
- package/src/components/results-grid/ResultCard.tsx +111 -0
- package/src/components/results-grid/RowDetailSheet.tsx +178 -0
- package/src/components/results-grid/StatsBar.tsx +201 -0
- package/src/components/results-grid/index.ts +1 -0
- package/src/components/results-grid/utils.ts +23 -0
- package/src/components/schema-explorer/ColumnList.tsx +53 -0
- package/src/components/schema-explorer/SchemaExplorer.tsx +182 -0
- package/src/components/schema-explorer/TableItem.tsx +210 -0
- package/src/components/schema-explorer/index.ts +1 -0
- package/src/components/sidebar/ConnectionItem.tsx +105 -0
- package/src/components/sidebar/ConnectionsList.tsx +62 -0
- package/src/components/sidebar/Sidebar.tsx +130 -0
- package/src/components/sidebar/index.ts +2 -0
- package/src/components/studio/BottomPanel.tsx +286 -0
- package/src/components/studio/QueryToolbar.tsx +180 -0
- package/src/components/studio/StudioDesktopHeader.tsx +114 -0
- package/src/components/studio/StudioMobileHeader.tsx +340 -0
- package/src/components/studio/StudioTabBar.tsx +82 -0
- package/src/components/studio/index.ts +5 -0
- package/src/components/ui/accordion.tsx +66 -0
- package/src/components/ui/alert-dialog.tsx +157 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/aspect-ratio.tsx +11 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/calendar.tsx +216 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/carousel.tsx +241 -0
- package/src/components/ui/chart.tsx +357 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/context-menu.tsx +252 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +248 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/hover-card.tsx +44 -0
- package/src/components/ui/input-group.tsx +170 -0
- package/src/components/ui/input-otp.tsx +77 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/item.tsx +193 -0
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/menubar.tsx +276 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +48 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +187 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +40 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/exports/components.ts +15 -0
- package/src/exports/index.ts +4 -0
- package/src/exports/providers.ts +4 -0
- package/src/exports/types.ts +26 -0
- package/src/hooks/use-ai-chat.ts +182 -0
- package/src/hooks/use-all-connections.ts +66 -0
- package/src/hooks/use-api-call.ts +71 -0
- package/src/hooks/use-auth.ts +51 -0
- package/src/hooks/use-connection-form.ts +349 -0
- package/src/hooks/use-connection-manager.ts +169 -0
- package/src/hooks/use-connection-payload.ts +15 -0
- package/src/hooks/use-inline-editing.ts +109 -0
- package/src/hooks/use-mobile.ts +20 -0
- package/src/hooks/use-monitoring-data.ts +270 -0
- package/src/hooks/use-provider-metadata.ts +62 -0
- package/src/hooks/use-query-execution.ts +478 -0
- package/src/hooks/use-storage-sync.ts +259 -0
- package/src/hooks/use-tab-manager.ts +231 -0
- package/src/hooks/use-toast.ts +20 -0
- package/src/hooks/use-transaction-control.ts +64 -0
- package/src/lib/api/error-codes.ts +30 -0
- package/src/lib/api/errors.ts +236 -0
- package/src/lib/api/with-error-handler.ts +41 -0
- package/src/lib/audit.ts +105 -0
- package/src/lib/auth.ts +87 -0
- package/src/lib/connection-string-parser.ts +172 -0
- package/src/lib/data-masking.ts +385 -0
- package/src/lib/db/base-provider.ts +325 -0
- package/src/lib/db/errors.ts +317 -0
- package/src/lib/db/factory.ts +324 -0
- package/src/lib/db/index.ts +123 -0
- package/src/lib/db/providers/document/index.ts +6 -0
- package/src/lib/db/providers/document/mongodb.ts +992 -0
- package/src/lib/db/providers/keyvalue/redis.ts +554 -0
- package/src/lib/db/providers/sql/index.ts +11 -0
- package/src/lib/db/providers/sql/mssql.ts +1065 -0
- package/src/lib/db/providers/sql/mysql.ts +978 -0
- package/src/lib/db/providers/sql/oracle.ts +1044 -0
- package/src/lib/db/providers/sql/postgres.ts +1179 -0
- package/src/lib/db/providers/sql/sql-base.ts +174 -0
- package/src/lib/db/providers/sql/sqlite.ts +721 -0
- package/src/lib/db/types.ts +437 -0
- package/src/lib/db/utils/pool-manager.ts +287 -0
- package/src/lib/db/utils/query-limiter.ts +239 -0
- package/src/lib/db-ui-config.ts +86 -0
- package/src/lib/editor/mongodb-completions.ts +172 -0
- package/src/lib/editor/sql-completions.ts +280 -0
- package/src/lib/llm/base-provider.ts +117 -0
- package/src/lib/llm/factory.ts +102 -0
- package/src/lib/llm/index.ts +90 -0
- package/src/lib/llm/providers/custom.ts +181 -0
- package/src/lib/llm/providers/gemini.ts +126 -0
- package/src/lib/llm/providers/ollama.ts +154 -0
- package/src/lib/llm/providers/openai.ts +146 -0
- package/src/lib/llm/types.ts +173 -0
- package/src/lib/llm/utils/config.ts +187 -0
- package/src/lib/llm/utils/retry.ts +119 -0
- package/src/lib/llm/utils/streaming.ts +202 -0
- package/src/lib/logger.ts +127 -0
- package/src/lib/monitoring-thresholds.ts +44 -0
- package/src/lib/oidc.ts +262 -0
- package/src/lib/query-generators.ts +61 -0
- package/src/lib/schema-diff/diff-engine.ts +273 -0
- package/src/lib/schema-diff/migration-generator.ts +208 -0
- package/src/lib/schema-diff/types.ts +55 -0
- package/src/lib/seed/config-loader.ts +79 -0
- package/src/lib/seed/connection-filter.ts +49 -0
- package/src/lib/seed/credential-resolver.ts +62 -0
- package/src/lib/seed/index.ts +40 -0
- package/src/lib/seed/resolve-connection.ts +57 -0
- package/src/lib/seed/types.ts +69 -0
- package/src/lib/sql/alias-extractor.ts +267 -0
- package/src/lib/sql/index.ts +8 -0
- package/src/lib/sql/statement-splitter.ts +167 -0
- package/src/lib/sql/types.ts +40 -0
- package/src/lib/ssh/tunnel.ts +142 -0
- package/src/lib/storage/factory.ts +84 -0
- package/src/lib/storage/index.ts +14 -0
- package/src/lib/storage/local-storage.ts +99 -0
- package/src/lib/storage/providers/postgres.ts +225 -0
- package/src/lib/storage/providers/sqlite.ts +153 -0
- package/src/lib/storage/storage-facade.ts +272 -0
- package/src/lib/storage/types.ts +75 -0
- package/src/lib/time-series-buffer.ts +58 -0
- package/src/lib/types.ts +173 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +104 -0
- package/src/types/db-drivers.d.ts +23 -0
- package/src/types/html2canvas.d.ts +9 -0
- package/tests/api/admin/audit.test.ts +178 -0
- package/tests/api/admin/fleet-health.test.ts +183 -0
- package/tests/api/ai/autopilot.test.ts +174 -0
- package/tests/api/ai/chat.test.ts +250 -0
- package/tests/api/ai/describe-schema.test.ts +266 -0
- package/tests/api/ai/explain.test.ts +199 -0
- package/tests/api/ai/impact.test.ts +168 -0
- package/tests/api/ai/index-advisor.test.ts +171 -0
- package/tests/api/ai/nl2sql.test.ts +202 -0
- package/tests/api/ai/query-safety.test.ts +196 -0
- package/tests/api/auth/login.test.ts +170 -0
- package/tests/api/auth/logout.test.ts +140 -0
- package/tests/api/auth/me.test.ts +73 -0
- package/tests/api/auth/oidc-callback.test.ts +215 -0
- package/tests/api/auth/oidc-login.test.ts +127 -0
- package/tests/api/db/cancel.test.ts +198 -0
- package/tests/api/db/disconnect.test.ts +124 -0
- package/tests/api/db/health.test.ts +222 -0
- package/tests/api/db/maintenance.test.ts +263 -0
- package/tests/api/db/monitoring.test.ts +221 -0
- package/tests/api/db/multi-query.test.ts +316 -0
- package/tests/api/db/pool-stats.test.ts +135 -0
- package/tests/api/db/profile.test.ts +330 -0
- package/tests/api/db/provider-meta.test.ts +193 -0
- package/tests/api/db/query.test.ts +314 -0
- package/tests/api/db/schema-snapshot.test.ts +170 -0
- package/tests/api/db/schema.test.ts +191 -0
- package/tests/api/db/test-connection.test.ts +185 -0
- package/tests/api/db/transaction.test.ts +314 -0
- package/tests/api/proxy.test.ts +191 -0
- package/tests/api/seed/managed-route.test.ts +113 -0
- package/tests/api/storage/config.test.ts +42 -0
- package/tests/api/storage/storage-routes.test.ts +309 -0
- package/tests/components/AIAutopilotPanel.test.tsx +756 -0
- package/tests/components/AdminPage.test.tsx +33 -0
- package/tests/components/CodeGenerator.test.tsx +182 -0
- package/tests/components/CommandPalette.test.tsx +428 -0
- package/tests/components/CommunitySection.test.tsx +91 -0
- package/tests/components/ConnectionModal.mobile.test.tsx +284 -0
- package/tests/components/ConnectionModal.test.tsx +570 -0
- package/tests/components/CreateTableModal.test.tsx +383 -0
- package/tests/components/DataCharts.test.tsx +739 -0
- package/tests/components/DataImportModal.test.tsx +751 -0
- package/tests/components/DataProfiler.test.tsx +589 -0
- package/tests/components/DatabaseDocs.test.tsx +353 -0
- package/tests/components/LoginPage.test.tsx +163 -0
- package/tests/components/LoginPageOIDC.test.tsx +92 -0
- package/tests/components/MaskingSettings.test.tsx +498 -0
- package/tests/components/MobileNav.test.tsx +30 -0
- package/tests/components/MonitoringPage.test.tsx +32 -0
- package/tests/components/NL2SQLPanel.test.tsx +621 -0
- package/tests/components/Page.test.tsx +33 -0
- package/tests/components/PivotTable.test.tsx +350 -0
- package/tests/components/QueryEditor.test.tsx +1730 -0
- package/tests/components/QueryHistory.test.tsx +572 -0
- package/tests/components/QuerySafetyDialog.test.tsx +586 -0
- package/tests/components/ResultsGrid.test.tsx +804 -0
- package/tests/components/RootLayout.test.tsx +83 -0
- package/tests/components/SaveQueryModal.test.tsx +25 -0
- package/tests/components/SavedQueries.test.tsx +43 -0
- package/tests/components/SchemaDiagram.test.tsx +1034 -0
- package/tests/components/SchemaDiff.test.tsx +906 -0
- package/tests/components/SnapshotTimeline.test.tsx +174 -0
- package/tests/components/Studio.test.tsx +1030 -0
- package/tests/components/TestDataGenerator.test.tsx +291 -0
- package/tests/components/VisualExplain.test.tsx +704 -0
- package/tests/components/admin/AdminDashboard.test.tsx +205 -0
- package/tests/components/admin/AuditTab.test.tsx +220 -0
- package/tests/components/admin/MonitoringEmbed.test.tsx +58 -0
- package/tests/components/admin/OperationsTab.test.tsx +975 -0
- package/tests/components/admin/OverviewTab.test.tsx +254 -0
- package/tests/components/admin/SecurityTab.test.tsx +467 -0
- package/tests/components/monitoring/MetricChart.test.tsx +111 -0
- package/tests/components/monitoring/MonitoringDashboard.test.tsx +259 -0
- package/tests/components/monitoring/OverviewTab.test.tsx +78 -0
- package/tests/components/monitoring/PerformanceTab.test.tsx +87 -0
- package/tests/components/monitoring/PoolTab.test.tsx +42 -0
- package/tests/components/monitoring/QueriesTab.test.tsx +80 -0
- package/tests/components/monitoring/SessionsTab.test.tsx +154 -0
- package/tests/components/monitoring/StorageTab.test.tsx +127 -0
- package/tests/components/monitoring/TablesTab.test.tsx +153 -0
- package/tests/components/results-grid/ResultCard.test.tsx +105 -0
- package/tests/components/results-grid/RowDetailSheet.test.tsx +308 -0
- package/tests/components/results-grid/StatsBar.test.tsx +162 -0
- package/tests/components/schema-explorer/ColumnList.test.tsx +151 -0
- package/tests/components/schema-explorer/SchemaExplorer.test.tsx +461 -0
- package/tests/components/schema-explorer/TableItem.test.tsx +415 -0
- package/tests/components/sidebar/ConnectionItem.test.tsx +201 -0
- package/tests/components/sidebar/ConnectionsList.test.tsx +176 -0
- package/tests/components/sidebar/Sidebar.test.tsx +187 -0
- package/tests/components/studio/BottomPanel.test.tsx +383 -0
- package/tests/components/studio/QueryToolbar.test.tsx +321 -0
- package/tests/components/studio/StudioDesktopHeader.test.tsx +377 -0
- package/tests/components/studio/StudioMobileHeader.test.tsx +198 -0
- package/tests/components/studio/StudioTabBar.test.tsx +331 -0
- package/tests/fixtures/connections.ts +96 -0
- package/tests/fixtures/masking-configs.ts +86 -0
- package/tests/fixtures/query-results.ts +71 -0
- package/tests/fixtures/schemas.ts +64 -0
- package/tests/fixtures/seed-connections/invalid-config.yaml +7 -0
- package/tests/fixtures/seed-connections/minimal-config.yaml +8 -0
- package/tests/fixtures/seed-connections/mixed-credentials.yaml +23 -0
- package/tests/fixtures/seed-connections/multi-role-config.yaml +30 -0
- package/tests/fixtures/seed-connections/valid-config.json +15 -0
- package/tests/fixtures/seed-connections/valid-config.yaml +51 -0
- package/tests/helpers/mock-fetch.ts +59 -0
- package/tests/helpers/mock-monaco.ts +112 -0
- package/tests/helpers/mock-navigation.ts +28 -0
- package/tests/helpers/mock-next.ts +80 -0
- package/tests/helpers/mock-provider.ts +133 -0
- package/tests/helpers/mock-sonner.ts +29 -0
- package/tests/helpers/render-with-providers.tsx +19 -0
- package/tests/hooks/use-ai-chat.test.ts +600 -0
- package/tests/hooks/use-auth.test.ts +371 -0
- package/tests/hooks/use-connection-form.test.ts +743 -0
- package/tests/hooks/use-connection-manager.test.ts +466 -0
- package/tests/hooks/use-inline-editing.test.ts +321 -0
- package/tests/hooks/use-mobile.test.ts +177 -0
- package/tests/hooks/use-monitoring-data.test.ts +819 -0
- package/tests/hooks/use-provider-metadata.test.ts +228 -0
- package/tests/hooks/use-query-execution.test.ts +1212 -0
- package/tests/hooks/use-tab-manager.test.ts +756 -0
- package/tests/hooks/use-toast.test.ts +74 -0
- package/tests/hooks/use-transaction-control.test.ts +211 -0
- package/tests/integration/db/mongodb-provider.test.ts +698 -0
- package/tests/integration/db/mssql-provider.test.ts +840 -0
- package/tests/integration/db/mysql-provider.test.ts +872 -0
- package/tests/integration/db/oracle-provider.test.ts +843 -0
- package/tests/integration/db/postgres-provider.test.ts +1382 -0
- package/tests/integration/db/redis-provider.test.ts +526 -0
- package/tests/integration/db/sqlite-provider.test.ts +480 -0
- package/tests/integration/seed/seed-pipeline.test.ts +102 -0
- package/tests/isolated/factory-singleton.test.ts +150 -0
- package/tests/isolated/use-storage-sync.test.ts +389 -0
- package/tests/run-components.sh +196 -0
- package/tests/setup-dom.ts +58 -0
- package/tests/setup.ts +40 -0
- package/tests/unit/api-errors.test.ts +210 -0
- package/tests/unit/code-generator-functions.test.ts +271 -0
- package/tests/unit/components/column-list.test.tsx +190 -0
- package/tests/unit/components/data-import-modal.test.tsx +441 -0
- package/tests/unit/components/studio-mobile-header.test.tsx +327 -0
- package/tests/unit/data-charts-functions.test.ts +496 -0
- package/tests/unit/data-import-functions.test.ts +320 -0
- package/tests/unit/data-import-utils.test.ts +125 -0
- package/tests/unit/db/base-provider.test.ts +517 -0
- package/tests/unit/db/errors.test.ts +403 -0
- package/tests/unit/db/factory.test.ts +436 -0
- package/tests/unit/db/pool-manager.test.ts +440 -0
- package/tests/unit/db/query-limiter.test.ts +387 -0
- package/tests/unit/db/sql-base.test.ts +438 -0
- package/tests/unit/lib/api/error-codes.test.ts +39 -0
- package/tests/unit/lib/audit.test.ts +326 -0
- package/tests/unit/lib/auth.test.ts +146 -0
- package/tests/unit/lib/connection-string-parser.test.ts +424 -0
- package/tests/unit/lib/data-masking.test.ts +583 -0
- package/tests/unit/lib/db-icons.test.tsx +41 -0
- package/tests/unit/lib/monitoring-thresholds.test.ts +133 -0
- package/tests/unit/lib/oidc.test.ts +509 -0
- package/tests/unit/lib/query-generators.test.ts +127 -0
- package/tests/unit/lib/storage/factory.test.ts +71 -0
- package/tests/unit/lib/storage/local-storage.test.ts +114 -0
- package/tests/unit/lib/storage/providers/postgres.test.ts +312 -0
- package/tests/unit/lib/storage/providers/sqlite.test.ts +232 -0
- package/tests/unit/lib/storage/storage-facade-extended.test.ts +331 -0
- package/tests/unit/lib/storage/storage-facade.test.ts +184 -0
- package/tests/unit/lib/storage.test.ts +317 -0
- package/tests/unit/lib/time-series-buffer.test.ts +212 -0
- package/tests/unit/lib/utils.test.ts +24 -0
- package/tests/unit/llm/base-provider.test.ts +238 -0
- package/tests/unit/llm/config.test.ts +262 -0
- package/tests/unit/llm/custom-provider.test.ts +281 -0
- package/tests/unit/llm/gemini-provider.test.ts +248 -0
- package/tests/unit/llm/llm-factory.test.ts +155 -0
- package/tests/unit/llm/ollama-provider.test.ts +288 -0
- package/tests/unit/llm/openai-provider.test.ts +324 -0
- package/tests/unit/llm/retry.test.ts +180 -0
- package/tests/unit/llm/streaming.test.ts +355 -0
- package/tests/unit/logger.test.ts +198 -0
- package/tests/unit/mongodb-completions.test.ts +516 -0
- package/tests/unit/pivot-table-functions.test.ts +76 -0
- package/tests/unit/query-cancelled-error.test.ts +81 -0
- package/tests/unit/schema-diff/diff-engine.test.ts +367 -0
- package/tests/unit/schema-diff/migration-generator.test.ts +513 -0
- package/tests/unit/seed/config-loader.test.ts +73 -0
- package/tests/unit/seed/connection-filter.test.ts +91 -0
- package/tests/unit/seed/credential-resolver.test.ts +85 -0
- package/tests/unit/seed/index.test.ts +72 -0
- package/tests/unit/seed/resolve-connection.test.ts +74 -0
- package/tests/unit/seed/types.test.ts +129 -0
- package/tests/unit/sql/alias-extractor.test.ts +444 -0
- package/tests/unit/sql/statement-splitter.test.ts +348 -0
- package/tests/unit/sql-completions.test.ts +463 -0
- package/tests/unit/ssh-tunnel.test.ts +465 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import '../setup-dom';
|
|
2
|
+
import '../helpers/mock-sonner';
|
|
3
|
+
import '../helpers/mock-navigation';
|
|
4
|
+
|
|
5
|
+
import { mock } from 'bun:test';
|
|
6
|
+
|
|
7
|
+
// Mock data-masking before component import
|
|
8
|
+
mock.module('@/lib/data-masking', () => ({
|
|
9
|
+
detectSensitiveColumns: mock(() => new Map()),
|
|
10
|
+
maskValue: mock(() => '****'),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
14
|
+
import { render, fireEvent, within, waitFor, cleanup } from '@testing-library/react';
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { mockGlobalFetch, restoreGlobalFetch, type MockFetchResponse } from '../helpers/mock-fetch';
|
|
17
|
+
|
|
18
|
+
import { DataProfiler } from '@/components/DataProfiler';
|
|
19
|
+
import { detectSensitiveColumns, maskValue } from '@/lib/data-masking';
|
|
20
|
+
import { mockPostgresConnection } from '../fixtures/connections';
|
|
21
|
+
import { mockUsersTable } from '../fixtures/schemas';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// DataProfiler Tests
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
const mockProfileResponse = {
|
|
28
|
+
tableName: 'users',
|
|
29
|
+
totalRows: 100,
|
|
30
|
+
columns: [
|
|
31
|
+
{
|
|
32
|
+
name: 'id',
|
|
33
|
+
type: 'integer',
|
|
34
|
+
totalRows: 100,
|
|
35
|
+
nullCount: 0,
|
|
36
|
+
nullPercent: 0,
|
|
37
|
+
distinctCount: 100,
|
|
38
|
+
minValue: '1',
|
|
39
|
+
maxValue: '100',
|
|
40
|
+
sampleValues: ['1', '2', '3'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'name',
|
|
44
|
+
type: 'varchar(255)',
|
|
45
|
+
totalRows: 100,
|
|
46
|
+
nullCount: 5,
|
|
47
|
+
nullPercent: 5,
|
|
48
|
+
distinctCount: 90,
|
|
49
|
+
minValue: 'Alice',
|
|
50
|
+
maxValue: 'Zara',
|
|
51
|
+
sampleValues: ['Alice', 'Bob', 'Carol'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'email',
|
|
55
|
+
type: 'varchar(255)',
|
|
56
|
+
totalRows: 100,
|
|
57
|
+
nullCount: 0,
|
|
58
|
+
nullPercent: 0,
|
|
59
|
+
distinctCount: 100,
|
|
60
|
+
minValue: 'alice@example.com',
|
|
61
|
+
maxValue: 'zara@example.com',
|
|
62
|
+
sampleValues: ['alice@example.com', 'bob@example.com'],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function createDefaultProps(overrides: Partial<Parameters<typeof DataProfiler>[0]> = {}) {
|
|
68
|
+
return {
|
|
69
|
+
isOpen: true,
|
|
70
|
+
onClose: mock(() => {}),
|
|
71
|
+
tableName: 'users',
|
|
72
|
+
tableSchema: mockUsersTable,
|
|
73
|
+
connection: mockPostgresConnection,
|
|
74
|
+
schemaContext: '',
|
|
75
|
+
databaseType: 'postgres',
|
|
76
|
+
...overrides,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('DataProfiler', () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
mockGlobalFetch({
|
|
83
|
+
'/api/db/profile': { ok: true, json: mockProfileResponse },
|
|
84
|
+
'/api/ai/describe-schema': { ok: false, status: 500, json: { error: 'AI not configured' } },
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
cleanup();
|
|
90
|
+
restoreGlobalFetch();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Does not render when isOpen is false ──────────────────────────────────
|
|
94
|
+
|
|
95
|
+
test('does not render when isOpen is false', () => {
|
|
96
|
+
const props = createDefaultProps({ isOpen: false });
|
|
97
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
98
|
+
|
|
99
|
+
expect(container.innerHTML).toBe('');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── Renders dialog when isOpen is true ────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
test('renders dialog when isOpen is true', () => {
|
|
105
|
+
const props = createDefaultProps({ isOpen: true });
|
|
106
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
107
|
+
const view = within(container);
|
|
108
|
+
|
|
109
|
+
expect(view.queryByText('Data Profiler')).not.toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── Shows table name in title ─────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
test('shows table name in title area', () => {
|
|
115
|
+
const props = createDefaultProps({ tableName: 'users' });
|
|
116
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
117
|
+
const view = within(container);
|
|
118
|
+
|
|
119
|
+
expect(view.queryByText('users')).not.toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── Loading state during fetch ────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
test('shows loading state during fetch', () => {
|
|
125
|
+
restoreGlobalFetch();
|
|
126
|
+
// Use a fetch mock that never resolves
|
|
127
|
+
mockGlobalFetch({
|
|
128
|
+
'/api/db/profile': (() => new Promise(() => {})) as () => Promise<MockFetchResponse>,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const props = createDefaultProps();
|
|
132
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
133
|
+
const view = within(container);
|
|
134
|
+
|
|
135
|
+
expect(view.queryByText('Profiling users...')).not.toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── Displays profiled data after successful fetch ─────────────────────────
|
|
139
|
+
|
|
140
|
+
test('displays profiled data after successful fetch', async () => {
|
|
141
|
+
const props = createDefaultProps();
|
|
142
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
143
|
+
const view = within(container);
|
|
144
|
+
|
|
145
|
+
await waitFor(() => {
|
|
146
|
+
expect(view.queryByText('Total Rows')).not.toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Summary stats — "100" appears in multiple places (Total Rows and distinct count)
|
|
150
|
+
const allHundreds = view.queryAllByText('100');
|
|
151
|
+
expect(allHundreds.length).toBeGreaterThan(0);
|
|
152
|
+
expect(view.queryByText('Column Profiles')).not.toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── Close button fires onClose ────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
test('close button fires onClose callback', () => {
|
|
158
|
+
const onClose = mock(() => {});
|
|
159
|
+
const props = createDefaultProps({ onClose });
|
|
160
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
161
|
+
|
|
162
|
+
// The close button is in the header with text-zinc-500 class
|
|
163
|
+
const closeButton = container.querySelector('button.text-zinc-500');
|
|
164
|
+
expect(closeButton).not.toBeNull();
|
|
165
|
+
|
|
166
|
+
fireEvent.click(closeButton!);
|
|
167
|
+
|
|
168
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── Null bar coloring: emerald for <20% ──────────────────────────────────
|
|
172
|
+
|
|
173
|
+
test('null bar uses emerald color when nullPercent < 20', async () => {
|
|
174
|
+
const profileWithLowNull = {
|
|
175
|
+
tableName: 'users',
|
|
176
|
+
totalRows: 100,
|
|
177
|
+
columns: [
|
|
178
|
+
{ name: 'id', type: 'integer', totalRows: 100, nullCount: 10, nullPercent: 10, distinctCount: 90, minValue: '1', maxValue: '100', sampleValues: ['1'] },
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
restoreGlobalFetch();
|
|
183
|
+
mockGlobalFetch({
|
|
184
|
+
'/api/db/profile': { ok: true, json: profileWithLowNull },
|
|
185
|
+
'/api/ai/describe-schema': { ok: false, status: 500, json: { error: 'AI not configured' } },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const props = createDefaultProps();
|
|
189
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
190
|
+
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(within(container).queryByText('Column Profiles')).not.toBeNull();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// The null bar inner div should have emerald class
|
|
196
|
+
const nullBar = container.querySelector('.bg-emerald-500');
|
|
197
|
+
expect(nullBar).not.toBeNull();
|
|
198
|
+
|
|
199
|
+
// The percent label should also be emerald
|
|
200
|
+
const nullLabel = within(container).queryByText('10% null');
|
|
201
|
+
expect(nullLabel).not.toBeNull();
|
|
202
|
+
expect(nullLabel!.className).toContain('text-emerald-400');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ── Null bar coloring: amber for 20-50% ──────────────────────────────────
|
|
206
|
+
|
|
207
|
+
test('null bar uses amber color when nullPercent is between 20 and 50', async () => {
|
|
208
|
+
const profileWithMidNull = {
|
|
209
|
+
tableName: 'users',
|
|
210
|
+
totalRows: 100,
|
|
211
|
+
columns: [
|
|
212
|
+
{ name: 'status', type: 'varchar', totalRows: 100, nullCount: 35, nullPercent: 35, distinctCount: 65, minValue: 'A', maxValue: 'Z', sampleValues: ['A'] },
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
restoreGlobalFetch();
|
|
217
|
+
mockGlobalFetch({
|
|
218
|
+
'/api/db/profile': { ok: true, json: profileWithMidNull },
|
|
219
|
+
'/api/ai/describe-schema': { ok: false, status: 500, json: { error: 'AI not configured' } },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const props = createDefaultProps();
|
|
223
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
224
|
+
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
expect(within(container).queryByText('Column Profiles')).not.toBeNull();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const nullBar = container.querySelector('.bg-amber-500');
|
|
230
|
+
expect(nullBar).not.toBeNull();
|
|
231
|
+
|
|
232
|
+
const nullLabel = within(container).queryByText('35% null');
|
|
233
|
+
expect(nullLabel).not.toBeNull();
|
|
234
|
+
expect(nullLabel!.className).toContain('text-amber-400');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── Null bar coloring: red for >50% ──────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
test('null bar uses red color when nullPercent > 50', async () => {
|
|
240
|
+
const profileWithHighNull = {
|
|
241
|
+
tableName: 'users',
|
|
242
|
+
totalRows: 100,
|
|
243
|
+
columns: [
|
|
244
|
+
{ name: 'notes', type: 'text', totalRows: 100, nullCount: 75, nullPercent: 75, distinctCount: 25, minValue: 'a', maxValue: 'z', sampleValues: ['a'] },
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
restoreGlobalFetch();
|
|
249
|
+
mockGlobalFetch({
|
|
250
|
+
'/api/db/profile': { ok: true, json: profileWithHighNull },
|
|
251
|
+
'/api/ai/describe-schema': { ok: false, status: 500, json: { error: 'AI not configured' } },
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const props = createDefaultProps();
|
|
255
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
256
|
+
|
|
257
|
+
await waitFor(() => {
|
|
258
|
+
expect(within(container).queryByText('Column Profiles')).not.toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const nullBar = container.querySelector('.bg-red-500');
|
|
262
|
+
expect(nullBar).not.toBeNull();
|
|
263
|
+
|
|
264
|
+
const nullLabel = within(container).queryByText('75% null');
|
|
265
|
+
expect(nullLabel).not.toBeNull();
|
|
266
|
+
expect(nullLabel!.className).toContain('text-red-400');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── Min/Max value display ────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
test('displays min and max values for each column', async () => {
|
|
272
|
+
const props = createDefaultProps();
|
|
273
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
274
|
+
const view = within(container);
|
|
275
|
+
|
|
276
|
+
await waitFor(() => {
|
|
277
|
+
expect(view.queryByText('Column Profiles')).not.toBeNull();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Check min/max labels exist in the rendered output
|
|
281
|
+
const allSpans = container.querySelectorAll('span');
|
|
282
|
+
const minTexts: string[] = [];
|
|
283
|
+
const maxTexts: string[] = [];
|
|
284
|
+
allSpans.forEach((el) => {
|
|
285
|
+
if (el.textContent?.startsWith('min:')) minTexts.push(el.textContent);
|
|
286
|
+
if (el.textContent?.startsWith('max:')) maxTexts.push(el.textContent);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// We have 3 columns with min/max values
|
|
290
|
+
expect(minTexts.length).toBe(3);
|
|
291
|
+
expect(maxTexts.length).toBe(3);
|
|
292
|
+
|
|
293
|
+
// Verify specific values appear (use queryAllByText since values may appear in both min and sample)
|
|
294
|
+
const aliceMatches = view.queryAllByText('Alice');
|
|
295
|
+
expect(aliceMatches.length).toBeGreaterThan(0);
|
|
296
|
+
const zaraMatches = view.queryAllByText('Zara');
|
|
297
|
+
expect(zaraMatches.length).toBeGreaterThan(0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ── Sample values rendered as chips ──────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
test('renders sample values as styled chips', async () => {
|
|
303
|
+
const props = createDefaultProps();
|
|
304
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
305
|
+
const view = within(container);
|
|
306
|
+
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(view.queryByText('Column Profiles')).not.toBeNull();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Sample values from mockProfileResponse — use queryAllByText since values may appear in multiple places
|
|
312
|
+
const onesMatches = view.queryAllByText('1');
|
|
313
|
+
expect(onesMatches.length).toBeGreaterThan(0);
|
|
314
|
+
|
|
315
|
+
// 'Bob' and 'Carol' are unique sample values from name column
|
|
316
|
+
expect(view.queryByText('Bob')).not.toBeNull();
|
|
317
|
+
expect(view.queryByText('Carol')).not.toBeNull();
|
|
318
|
+
|
|
319
|
+
// Chips should have the bg-zinc-800 + rounded + font-mono classes
|
|
320
|
+
const chips = container.querySelectorAll('span.bg-zinc-800.rounded.font-mono');
|
|
321
|
+
expect(chips.length).toBeGreaterThanOrEqual(8); // 3 + 3 + 2 sample values
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ── AI summary streaming display ─────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
test('displays AI summary when streaming succeeds', async () => {
|
|
327
|
+
const aiText = 'This table stores user account data with good data quality.';
|
|
328
|
+
restoreGlobalFetch();
|
|
329
|
+
|
|
330
|
+
// Override fetch directly to control the streaming response
|
|
331
|
+
const originalFetch = globalThis.fetch;
|
|
332
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
333
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
334
|
+
const pathname = new URL(url, 'http://localhost:3000').pathname;
|
|
335
|
+
|
|
336
|
+
if (pathname.includes('/api/db/profile')) {
|
|
337
|
+
return new Response(JSON.stringify(mockProfileResponse), {
|
|
338
|
+
status: 200,
|
|
339
|
+
headers: { 'content-type': 'application/json' },
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (pathname.includes('/api/ai/describe-schema')) {
|
|
344
|
+
// Create a readable stream that yields the AI text
|
|
345
|
+
const stream = new ReadableStream({
|
|
346
|
+
start(controller) {
|
|
347
|
+
controller.enqueue(new TextEncoder().encode(aiText));
|
|
348
|
+
controller.close();
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
return new Response(stream, {
|
|
352
|
+
status: 200,
|
|
353
|
+
headers: { 'content-type': 'text/plain' },
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return new Response('Not found', { status: 404 });
|
|
358
|
+
}) as unknown as typeof fetch;
|
|
359
|
+
|
|
360
|
+
const props = createDefaultProps();
|
|
361
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
362
|
+
const view = within(container);
|
|
363
|
+
|
|
364
|
+
// Wait for profile data to load first
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
expect(view.queryByText('Column Profiles')).not.toBeNull();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// The AI section should appear with the streamed content
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(view.queryByText('AI Analysis')).not.toBeNull();
|
|
372
|
+
}, { timeout: 3000 });
|
|
373
|
+
|
|
374
|
+
// The actual AI text should be displayed
|
|
375
|
+
await waitFor(() => {
|
|
376
|
+
expect(view.queryByText(aiText)).not.toBeNull();
|
|
377
|
+
}, { timeout: 3000 });
|
|
378
|
+
|
|
379
|
+
globalThis.fetch = originalFetch;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// ── Profile fetch error handling ─────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
test('displays error message when profile fetch fails', async () => {
|
|
385
|
+
restoreGlobalFetch();
|
|
386
|
+
mockGlobalFetch({
|
|
387
|
+
'/api/db/profile': { ok: false, status: 500, json: { error: 'Connection refused' } },
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const props = createDefaultProps();
|
|
391
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
392
|
+
const view = within(container);
|
|
393
|
+
|
|
394
|
+
await waitFor(() => {
|
|
395
|
+
expect(view.queryByText('Connection refused')).not.toBeNull();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Error should be in a red-styled container
|
|
399
|
+
const errorDiv = container.querySelector('.bg-red-500\\/10');
|
|
400
|
+
expect(errorDiv).not.toBeNull();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// ── Column error message display ─────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
test('displays column-level error when col.error is present', async () => {
|
|
406
|
+
const profileWithColError = {
|
|
407
|
+
tableName: 'users',
|
|
408
|
+
totalRows: 100,
|
|
409
|
+
columns: [
|
|
410
|
+
{ name: 'id', type: 'integer', totalRows: 100, nullCount: 0, nullPercent: 0, distinctCount: 100, error: 'Permission denied for column' },
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
restoreGlobalFetch();
|
|
415
|
+
mockGlobalFetch({
|
|
416
|
+
'/api/db/profile': { ok: true, json: profileWithColError },
|
|
417
|
+
'/api/ai/describe-schema': { ok: false, status: 500, json: { error: 'AI not configured' } },
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const props = createDefaultProps();
|
|
421
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
422
|
+
const view = within(container);
|
|
423
|
+
|
|
424
|
+
await waitFor(() => {
|
|
425
|
+
expect(view.queryByText('Permission denied for column')).not.toBeNull();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Error text should have amber color class
|
|
429
|
+
const errorEl = view.queryByText('Permission denied for column');
|
|
430
|
+
expect(errorEl).not.toBeNull();
|
|
431
|
+
expect(errorEl!.className).toContain('text-amber-400');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// ── Sensitive column masking (lock icon + masked values) ─────────────────
|
|
435
|
+
|
|
436
|
+
test('shows lock icon and masked values for sensitive columns', async () => {
|
|
437
|
+
// Override detectSensitiveColumns to return a map with 'email' as sensitive
|
|
438
|
+
const mockRule = { pattern: /email/i, label: 'Email', mask: (v: string) => v };
|
|
439
|
+
(detectSensitiveColumns as ReturnType<typeof mock>).mockImplementation(
|
|
440
|
+
() => new Map([['email', mockRule]])
|
|
441
|
+
);
|
|
442
|
+
(maskValue as ReturnType<typeof mock>).mockImplementation(() => '****');
|
|
443
|
+
|
|
444
|
+
const props = createDefaultProps();
|
|
445
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
446
|
+
const view = within(container);
|
|
447
|
+
|
|
448
|
+
await waitFor(() => {
|
|
449
|
+
expect(view.queryByText('Column Profiles')).not.toBeNull();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Lock icon should be present (title attribute = 'Sensitive column - values masked')
|
|
453
|
+
const lockIcon = container.querySelector('[title="Sensitive column - values masked"]');
|
|
454
|
+
expect(lockIcon).not.toBeNull();
|
|
455
|
+
|
|
456
|
+
// Masked values should appear as '****'
|
|
457
|
+
const maskedValues = view.queryAllByText('****');
|
|
458
|
+
expect(maskedValues.length).toBeGreaterThan(0);
|
|
459
|
+
|
|
460
|
+
// Restore default mock
|
|
461
|
+
(detectSensitiveColumns as ReturnType<typeof mock>).mockImplementation(() => new Map());
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// ── No fetch when connection is null ─────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
test('does not fetch profile when connection is null', () => {
|
|
467
|
+
const fetchMock = restoreGlobalFetch();
|
|
468
|
+
void fetchMock;
|
|
469
|
+
const fetchSpy = mockGlobalFetch({
|
|
470
|
+
'/api/db/profile': { ok: true, json: mockProfileResponse },
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const props = createDefaultProps({ connection: null });
|
|
474
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
475
|
+
const view = within(container);
|
|
476
|
+
|
|
477
|
+
// Should not show loading state or profile data
|
|
478
|
+
expect(view.queryByText('Profiling users...')).toBeNull();
|
|
479
|
+
|
|
480
|
+
// Fetch should not have been called
|
|
481
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// ── No fetch when tableSchema is null ────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
test('does not fetch profile when tableSchema is null', () => {
|
|
487
|
+
restoreGlobalFetch();
|
|
488
|
+
const fetchSpy = mockGlobalFetch({
|
|
489
|
+
'/api/db/profile': { ok: true, json: mockProfileResponse },
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const props = createDefaultProps({ tableSchema: null });
|
|
493
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
494
|
+
const view = within(container);
|
|
495
|
+
|
|
496
|
+
// Should not show loading or profile data
|
|
497
|
+
expect(view.queryByText('Profiling users...')).toBeNull();
|
|
498
|
+
|
|
499
|
+
// The effect calls fetchProfile which returns early if !tableSchema,
|
|
500
|
+
// but it still calls fetch because the guard is inside fetchProfile.
|
|
501
|
+
// Actually looking at the code: useEffect guards on `connection` but not `tableSchema`.
|
|
502
|
+
// fetchProfile guards on both: `if (!connection || !tableSchema) return;`
|
|
503
|
+
// But the useEffect only checks: `if (isOpen && tableName && connection)`
|
|
504
|
+
// Since connection is provided but tableSchema is null, the effect fires but fetchProfile returns early.
|
|
505
|
+
// So fetch should NOT have been called.
|
|
506
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// ── Average null % in summary ────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
test('displays correct average null percentage in summary', async () => {
|
|
512
|
+
// mockProfileResponse columns: id=0%, name=5%, email=0% → avg = (0+5+0)/3 ≈ 2%
|
|
513
|
+
const props = createDefaultProps();
|
|
514
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
515
|
+
const view = within(container);
|
|
516
|
+
|
|
517
|
+
await waitFor(() => {
|
|
518
|
+
expect(view.queryByText('Avg Null %')).not.toBeNull();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Math.round((0 + 5 + 0) / 3) = Math.round(1.666) = 2
|
|
522
|
+
expect(view.queryByText('2%')).not.toBeNull();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// ── Columns count in summary ─────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
test('displays correct column count in summary', async () => {
|
|
528
|
+
const props = createDefaultProps();
|
|
529
|
+
const { container } = render(<DataProfiler {...props} />);
|
|
530
|
+
|
|
531
|
+
await waitFor(() => {
|
|
532
|
+
expect(within(container).queryByText('Columns')).not.toBeNull();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Find the Columns summary card and verify its value
|
|
536
|
+
// The summary grid has 3 cards; the Columns card contains "3"
|
|
537
|
+
const summaryCards = container.querySelectorAll('.bg-\\[\\#0a0a0a\\]');
|
|
538
|
+
const columnsCard = Array.from(summaryCards).find(
|
|
539
|
+
(card) => card.textContent?.includes('Columns')
|
|
540
|
+
);
|
|
541
|
+
expect(columnsCard).not.toBeNull();
|
|
542
|
+
expect(columnsCard!.textContent).toContain('3');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// ── State reset on close/reopen ──────────────────────────────────────────
|
|
546
|
+
|
|
547
|
+
test('resets state when closed and reopened', async () => {
|
|
548
|
+
const props = createDefaultProps();
|
|
549
|
+
const { container, rerender } = render(<DataProfiler {...props} />);
|
|
550
|
+
const view = within(container);
|
|
551
|
+
|
|
552
|
+
// Wait for profile data to load
|
|
553
|
+
await waitFor(() => {
|
|
554
|
+
expect(view.queryByText('Column Profiles')).not.toBeNull();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Close the profiler
|
|
558
|
+
rerender(<DataProfiler {...createDefaultProps({ isOpen: false })} />);
|
|
559
|
+
|
|
560
|
+
// Should not render anything when closed
|
|
561
|
+
expect(container.innerHTML).toBe('');
|
|
562
|
+
|
|
563
|
+
// Setup a different profile response for reopen
|
|
564
|
+
restoreGlobalFetch();
|
|
565
|
+
const newProfile = {
|
|
566
|
+
tableName: 'users',
|
|
567
|
+
totalRows: 999,
|
|
568
|
+
columns: [
|
|
569
|
+
{ name: 'id', type: 'integer', totalRows: 999, nullCount: 0, nullPercent: 0, distinctCount: 999, minValue: '1', maxValue: '999', sampleValues: ['1'] },
|
|
570
|
+
],
|
|
571
|
+
};
|
|
572
|
+
mockGlobalFetch({
|
|
573
|
+
'/api/db/profile': { ok: true, json: newProfile },
|
|
574
|
+
'/api/ai/describe-schema': { ok: false, status: 500, json: { error: 'AI not configured' } },
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// Reopen
|
|
578
|
+
rerender(<DataProfiler {...createDefaultProps({ isOpen: true })} />);
|
|
579
|
+
|
|
580
|
+
// Wait for new data
|
|
581
|
+
await waitFor(() => {
|
|
582
|
+
expect(within(container).queryByText('Column Profiles')).not.toBeNull();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// Should show new total rows (999) — may appear in multiple places (totalRows, distinct, maxValue)
|
|
586
|
+
const nineNineNine = within(container).queryAllByText('999');
|
|
587
|
+
expect(nineNineNine.length).toBeGreaterThan(0);
|
|
588
|
+
});
|
|
589
|
+
});
|