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