@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,975 @@
|
|
|
1
|
+
import '../../setup-dom';
|
|
2
|
+
import '../../helpers/mock-sonner';
|
|
3
|
+
import '../../helpers/mock-navigation';
|
|
4
|
+
|
|
5
|
+
import { mock } from 'bun:test';
|
|
6
|
+
import { setupRechartssMock, setupFramerMotionMock } from '../../helpers/mock-monaco';
|
|
7
|
+
|
|
8
|
+
setupRechartssMock();
|
|
9
|
+
setupFramerMotionMock();
|
|
10
|
+
|
|
11
|
+
// ---- Trackable mock functions ----
|
|
12
|
+
const mockRefresh = mock(() => {});
|
|
13
|
+
const mockKillSession = mock(() => true);
|
|
14
|
+
const mockRunMaintenance = mock(() => true);
|
|
15
|
+
|
|
16
|
+
// ---- Override objects ----
|
|
17
|
+
let monitoringOverride: Record<string, unknown> = {};
|
|
18
|
+
let mockConnectionsList: Record<string, unknown>[] = [
|
|
19
|
+
{
|
|
20
|
+
id: 'c1',
|
|
21
|
+
name: 'PG Dev',
|
|
22
|
+
type: 'postgres',
|
|
23
|
+
host: 'localhost',
|
|
24
|
+
port: 5432,
|
|
25
|
+
database: 'dev',
|
|
26
|
+
createdAt: new Date(),
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
let mockActiveConnectionId: string | null = 'c1';
|
|
30
|
+
|
|
31
|
+
const defaultSessions = [
|
|
32
|
+
{
|
|
33
|
+
pid: 1234,
|
|
34
|
+
user: 'admin',
|
|
35
|
+
state: 'active',
|
|
36
|
+
query: 'SELECT 1',
|
|
37
|
+
duration: '00:01:00',
|
|
38
|
+
durationMs: 60000,
|
|
39
|
+
database: 'dev',
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const defaultTables = [
|
|
44
|
+
{
|
|
45
|
+
tableName: 'users',
|
|
46
|
+
schemaName: 'public',
|
|
47
|
+
rowCount: 1000,
|
|
48
|
+
tableSize: '16 MB',
|
|
49
|
+
totalSize: '20 MB',
|
|
50
|
+
bloatRatio: 5,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
mock.module('@/hooks/use-monitoring-data', () => ({
|
|
55
|
+
useMonitoringData: mock(() => ({
|
|
56
|
+
data: {
|
|
57
|
+
activeSessions: defaultSessions,
|
|
58
|
+
tables: defaultTables,
|
|
59
|
+
},
|
|
60
|
+
loading: false,
|
|
61
|
+
error: null,
|
|
62
|
+
refresh: mockRefresh,
|
|
63
|
+
killSession: mockKillSession,
|
|
64
|
+
runMaintenance: mockRunMaintenance,
|
|
65
|
+
...monitoringOverride,
|
|
66
|
+
})),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
mock.module('@/lib/storage', () => ({
|
|
70
|
+
storage: {
|
|
71
|
+
getConnections: mock(() => mockConnectionsList),
|
|
72
|
+
getActiveConnectionId: mock(() => mockActiveConnectionId),
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
mock.module('@/lib/db-ui-config', () => ({
|
|
77
|
+
getDBIcon: () => {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
79
|
+
const React = require('react');
|
|
80
|
+
return (props: Record<string, unknown>) => React.createElement('span', { ...props, 'data-testid': 'db-icon' });
|
|
81
|
+
},
|
|
82
|
+
getDBColor: () => 'text-blue-400',
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
86
|
+
import { render, act, cleanup, fireEvent } from '@testing-library/react';
|
|
87
|
+
import React from 'react';
|
|
88
|
+
|
|
89
|
+
import { OperationsTab } from '@/components/admin/tabs/OperationsTab';
|
|
90
|
+
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// Test data
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
95
|
+
const multiSessions = [
|
|
96
|
+
{ pid: 100, user: 'admin', state: 'active', query: 'SELECT 1', duration: '00:00:05', durationMs: 5000, database: 'dev' },
|
|
97
|
+
{ pid: 101, user: 'user1', state: 'idle', query: '', duration: '00:00:10', durationMs: 10000, database: 'dev' },
|
|
98
|
+
{ pid: 102, user: 'user2', state: 'idle in transaction', query: 'UPDATE users SET x=1', duration: '00:02:00', durationMs: 120000, database: 'dev' },
|
|
99
|
+
{ pid: 103, user: 'user3', state: 'idle in transaction (aborted)', query: 'INSERT INTO t', duration: '00:00:30', durationMs: 30000, database: 'dev' },
|
|
100
|
+
{ pid: 104, user: 'user4', state: 'fastpath function call', query: '', duration: '00:00:01', durationMs: 1000, database: 'dev', waitEventType: 'Lock' },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const multiTables = [
|
|
104
|
+
{ tableName: 'users', schemaName: 'public', rowCount: 1000, tableSize: '16 MB', totalSize: '20 MB', bloatRatio: 5 },
|
|
105
|
+
{ tableName: 'orders', schemaName: 'public', rowCount: 50000, tableSize: '128 MB', totalSize: '200 MB', bloatRatio: 25 },
|
|
106
|
+
{ tableName: 'products', schemaName: 'public', rowCount: 200, tableSize: '2 MB', totalSize: '3 MB', bloatRatio: 0 },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// OperationsTab Tests
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
describe('OperationsTab', () => {
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
// Reset overrides
|
|
116
|
+
monitoringOverride = {};
|
|
117
|
+
mockConnectionsList = [
|
|
118
|
+
{ id: 'c1', name: 'PG Dev', type: 'postgres', host: 'localhost', port: 5432, database: 'dev', createdAt: new Date() },
|
|
119
|
+
];
|
|
120
|
+
mockActiveConnectionId = 'c1';
|
|
121
|
+
|
|
122
|
+
// Clear mocks
|
|
123
|
+
mockRefresh.mockClear();
|
|
124
|
+
mockKillSession.mockClear();
|
|
125
|
+
mockKillSession.mockImplementation(() => true);
|
|
126
|
+
mockRunMaintenance.mockClear();
|
|
127
|
+
mockRunMaintenance.mockImplementation(() => true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
cleanup();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// =========================================================================
|
|
135
|
+
// Existing rendering tests
|
|
136
|
+
// =========================================================================
|
|
137
|
+
|
|
138
|
+
test('renders connection selector', async () => {
|
|
139
|
+
let renderResult: ReturnType<typeof render>;
|
|
140
|
+
await act(async () => {
|
|
141
|
+
renderResult = render(<OperationsTab />);
|
|
142
|
+
});
|
|
143
|
+
const { queryByText } = renderResult!;
|
|
144
|
+
expect(queryByText('PG Dev')).not.toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('shows global operations section', async () => {
|
|
148
|
+
let renderResult: ReturnType<typeof render>;
|
|
149
|
+
await act(async () => {
|
|
150
|
+
renderResult = render(<OperationsTab />);
|
|
151
|
+
});
|
|
152
|
+
const { queryByText } = renderResult!;
|
|
153
|
+
expect(queryByText('Global Operations')).not.toBeNull();
|
|
154
|
+
expect(queryByText('Update Statistics')).not.toBeNull();
|
|
155
|
+
expect(queryByText('Reclaim Space')).not.toBeNull();
|
|
156
|
+
expect(queryByText('Rebuild Indexes')).not.toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('shows tables panel with table list', async () => {
|
|
160
|
+
let renderResult: ReturnType<typeof render>;
|
|
161
|
+
await act(async () => {
|
|
162
|
+
renderResult = render(<OperationsTab />);
|
|
163
|
+
});
|
|
164
|
+
const { queryByText } = renderResult!;
|
|
165
|
+
expect(queryByText('Tables (1)')).not.toBeNull();
|
|
166
|
+
expect(queryByText('users')).not.toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('shows sessions panel with session list', async () => {
|
|
170
|
+
let renderResult: ReturnType<typeof render>;
|
|
171
|
+
await act(async () => {
|
|
172
|
+
renderResult = render(<OperationsTab />);
|
|
173
|
+
});
|
|
174
|
+
const { queryByText } = renderResult!;
|
|
175
|
+
expect(queryByText('Sessions (1)')).not.toBeNull();
|
|
176
|
+
expect(queryByText('1234')).not.toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('maintenance buttons present', async () => {
|
|
180
|
+
let renderResult: ReturnType<typeof render>;
|
|
181
|
+
await act(async () => {
|
|
182
|
+
renderResult = render(<OperationsTab />);
|
|
183
|
+
});
|
|
184
|
+
const { queryByText } = renderResult!;
|
|
185
|
+
expect(queryByText('Run Analyze')).not.toBeNull();
|
|
186
|
+
expect(queryByText('Run Vacuum')).not.toBeNull();
|
|
187
|
+
expect(queryByText('Run Reindex')).not.toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('warning card present', async () => {
|
|
191
|
+
let renderResult: ReturnType<typeof render>;
|
|
192
|
+
await act(async () => {
|
|
193
|
+
renderResult = render(<OperationsTab />);
|
|
194
|
+
});
|
|
195
|
+
const { queryByText } = renderResult!;
|
|
196
|
+
expect(queryByText('Warning')).not.toBeNull();
|
|
197
|
+
expect(queryByText(/resource-intensive/)).not.toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('shows table size information', async () => {
|
|
201
|
+
let renderResult: ReturnType<typeof render>;
|
|
202
|
+
await act(async () => {
|
|
203
|
+
renderResult = render(<OperationsTab />);
|
|
204
|
+
});
|
|
205
|
+
const { queryByText } = renderResult!;
|
|
206
|
+
expect(queryByText('16 MB')).not.toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('shows session user info', async () => {
|
|
210
|
+
let renderResult: ReturnType<typeof render>;
|
|
211
|
+
await act(async () => {
|
|
212
|
+
renderResult = render(<OperationsTab />);
|
|
213
|
+
});
|
|
214
|
+
const { queryByText } = renderResult!;
|
|
215
|
+
expect(queryByText('admin')).not.toBeNull();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('shows session query info', async () => {
|
|
219
|
+
let renderResult: ReturnType<typeof render>;
|
|
220
|
+
await act(async () => {
|
|
221
|
+
renderResult = render(<OperationsTab />);
|
|
222
|
+
});
|
|
223
|
+
const { container } = renderResult!;
|
|
224
|
+
expect(container.textContent).toContain('SELECT 1');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('shows session duration', async () => {
|
|
228
|
+
let renderResult: ReturnType<typeof render>;
|
|
229
|
+
await act(async () => {
|
|
230
|
+
renderResult = render(<OperationsTab />);
|
|
231
|
+
});
|
|
232
|
+
const { container } = renderResult!;
|
|
233
|
+
expect(container.textContent).toContain('00:01:00');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('shows row count for tables', async () => {
|
|
237
|
+
let renderResult: ReturnType<typeof render>;
|
|
238
|
+
await act(async () => {
|
|
239
|
+
renderResult = render(<OperationsTab />);
|
|
240
|
+
});
|
|
241
|
+
const { container } = renderResult!;
|
|
242
|
+
expect(container.textContent).toMatch(/1,?000/);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('shows connection type in selector', async () => {
|
|
246
|
+
let renderResult: ReturnType<typeof render>;
|
|
247
|
+
await act(async () => {
|
|
248
|
+
renderResult = render(<OperationsTab />);
|
|
249
|
+
});
|
|
250
|
+
const { container } = renderResult!;
|
|
251
|
+
expect(container.textContent).toContain('(postgres)');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('shows session state as Active badge', async () => {
|
|
255
|
+
let renderResult: ReturnType<typeof render>;
|
|
256
|
+
await act(async () => {
|
|
257
|
+
renderResult = render(<OperationsTab />);
|
|
258
|
+
});
|
|
259
|
+
const { container } = renderResult!;
|
|
260
|
+
expect(container.textContent).toContain('Active');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// =========================================================================
|
|
264
|
+
// Empty state: no connections
|
|
265
|
+
// =========================================================================
|
|
266
|
+
|
|
267
|
+
test('shows empty state when no connections', async () => {
|
|
268
|
+
mockConnectionsList = [];
|
|
269
|
+
let renderResult: ReturnType<typeof render>;
|
|
270
|
+
await act(async () => {
|
|
271
|
+
renderResult = render(<OperationsTab />);
|
|
272
|
+
});
|
|
273
|
+
const { queryByText } = renderResult!;
|
|
274
|
+
expect(queryByText('No Database Connections')).not.toBeNull();
|
|
275
|
+
expect(queryByText(/add a database connection/i)).not.toBeNull();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// =========================================================================
|
|
279
|
+
// Error state
|
|
280
|
+
// =========================================================================
|
|
281
|
+
|
|
282
|
+
test('shows error message when error and no data', async () => {
|
|
283
|
+
monitoringOverride = { data: null, error: 'Connection refused' };
|
|
284
|
+
let renderResult: ReturnType<typeof render>;
|
|
285
|
+
await act(async () => {
|
|
286
|
+
renderResult = render(<OperationsTab />);
|
|
287
|
+
});
|
|
288
|
+
const { queryByText } = renderResult!;
|
|
289
|
+
expect(queryByText('Connection refused')).not.toBeNull();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// =========================================================================
|
|
293
|
+
// Loading state
|
|
294
|
+
// =========================================================================
|
|
295
|
+
|
|
296
|
+
test('shows loading skeletons when loading with no data', async () => {
|
|
297
|
+
monitoringOverride = {
|
|
298
|
+
data: { activeSessions: [], tables: [] },
|
|
299
|
+
loading: true,
|
|
300
|
+
};
|
|
301
|
+
let renderResult: ReturnType<typeof render>;
|
|
302
|
+
await act(async () => {
|
|
303
|
+
renderResult = render(<OperationsTab />);
|
|
304
|
+
});
|
|
305
|
+
const { container } = renderResult!;
|
|
306
|
+
// At minimum, the component renders skeleton placeholders when loading
|
|
307
|
+
expect(container.textContent).toContain('Tables (0)');
|
|
308
|
+
expect(container.textContent).toContain('Sessions (0)');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// =========================================================================
|
|
312
|
+
// Empty sessions / empty tables
|
|
313
|
+
// =========================================================================
|
|
314
|
+
|
|
315
|
+
test('shows no sessions message when empty', async () => {
|
|
316
|
+
monitoringOverride = {
|
|
317
|
+
data: { activeSessions: [], tables: defaultTables },
|
|
318
|
+
};
|
|
319
|
+
let renderResult: ReturnType<typeof render>;
|
|
320
|
+
await act(async () => {
|
|
321
|
+
renderResult = render(<OperationsTab />);
|
|
322
|
+
});
|
|
323
|
+
const { queryByText } = renderResult!;
|
|
324
|
+
expect(queryByText('No active sessions found.')).not.toBeNull();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// =========================================================================
|
|
328
|
+
// Table search filter
|
|
329
|
+
// =========================================================================
|
|
330
|
+
|
|
331
|
+
test('filters tables by search input', async () => {
|
|
332
|
+
monitoringOverride = {
|
|
333
|
+
data: { activeSessions: defaultSessions, tables: multiTables },
|
|
334
|
+
};
|
|
335
|
+
let renderResult: ReturnType<typeof render>;
|
|
336
|
+
await act(async () => {
|
|
337
|
+
renderResult = render(<OperationsTab />);
|
|
338
|
+
});
|
|
339
|
+
const { container, queryByText } = renderResult!;
|
|
340
|
+
|
|
341
|
+
// All 3 tables visible initially
|
|
342
|
+
expect(queryByText('Tables (3)')).not.toBeNull();
|
|
343
|
+
expect(queryByText('users')).not.toBeNull();
|
|
344
|
+
expect(queryByText('orders')).not.toBeNull();
|
|
345
|
+
expect(queryByText('products')).not.toBeNull();
|
|
346
|
+
|
|
347
|
+
// Type in filter input
|
|
348
|
+
const filterInput = container.querySelector('input[placeholder="Filter..."]') as HTMLInputElement;
|
|
349
|
+
expect(filterInput).not.toBeNull();
|
|
350
|
+
await act(async () => {
|
|
351
|
+
fireEvent.change(filterInput, { target: { value: 'ord' } });
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Only 'orders' should match
|
|
355
|
+
expect(queryByText('orders')).not.toBeNull();
|
|
356
|
+
expect(queryByText('users')).toBeNull();
|
|
357
|
+
expect(queryByText('products')).toBeNull();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('shows no tables found when filter matches nothing', async () => {
|
|
361
|
+
let renderResult: ReturnType<typeof render>;
|
|
362
|
+
await act(async () => {
|
|
363
|
+
renderResult = render(<OperationsTab />);
|
|
364
|
+
});
|
|
365
|
+
const { container, queryByText } = renderResult!;
|
|
366
|
+
|
|
367
|
+
const filterInput = container.querySelector('input[placeholder="Filter..."]') as HTMLInputElement;
|
|
368
|
+
await act(async () => {
|
|
369
|
+
fireEvent.change(filterInput, { target: { value: 'zzz_nonexistent' } });
|
|
370
|
+
});
|
|
371
|
+
expect(queryByText('No tables found.')).not.toBeNull();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// =========================================================================
|
|
375
|
+
// Bloat ratio badge
|
|
376
|
+
// =========================================================================
|
|
377
|
+
|
|
378
|
+
test('shows bloat ratio badge for high-bloat tables', async () => {
|
|
379
|
+
monitoringOverride = {
|
|
380
|
+
data: { activeSessions: defaultSessions, tables: multiTables },
|
|
381
|
+
};
|
|
382
|
+
let renderResult: ReturnType<typeof render>;
|
|
383
|
+
await act(async () => {
|
|
384
|
+
renderResult = render(<OperationsTab />);
|
|
385
|
+
});
|
|
386
|
+
const { container } = renderResult!;
|
|
387
|
+
|
|
388
|
+
// 'orders' table has 25% bloat (>10%) — should show badge
|
|
389
|
+
expect(container.textContent).toContain('25% bloat');
|
|
390
|
+
// 'products' table has 0% bloat — no bloat badge (only one bloat badge total)
|
|
391
|
+
const bloatBadges = container.textContent!.match(/\d+% bloat/g) || [];
|
|
392
|
+
expect(bloatBadges).toEqual(['25% bloat']);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// =========================================================================
|
|
396
|
+
// Session state badge variants
|
|
397
|
+
// =========================================================================
|
|
398
|
+
|
|
399
|
+
test('renders correct badges for different session states', async () => {
|
|
400
|
+
monitoringOverride = {
|
|
401
|
+
data: { activeSessions: multiSessions, tables: defaultTables },
|
|
402
|
+
};
|
|
403
|
+
let renderResult: ReturnType<typeof render>;
|
|
404
|
+
await act(async () => {
|
|
405
|
+
renderResult = render(<OperationsTab />);
|
|
406
|
+
});
|
|
407
|
+
const { container } = renderResult!;
|
|
408
|
+
const text = container.textContent || '';
|
|
409
|
+
|
|
410
|
+
expect(text).toContain('Active');
|
|
411
|
+
expect(text).toContain('Idle');
|
|
412
|
+
expect(text).toContain('Idle TX');
|
|
413
|
+
expect(text).toContain('Abort');
|
|
414
|
+
// Default state — 'fastpath function call'
|
|
415
|
+
expect(text).toContain('fastpath function call');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// =========================================================================
|
|
419
|
+
// Session summary counts
|
|
420
|
+
// =========================================================================
|
|
421
|
+
|
|
422
|
+
test('shows correct session summary counts', async () => {
|
|
423
|
+
monitoringOverride = {
|
|
424
|
+
data: { activeSessions: multiSessions, tables: defaultTables },
|
|
425
|
+
};
|
|
426
|
+
let renderResult: ReturnType<typeof render>;
|
|
427
|
+
await act(async () => {
|
|
428
|
+
renderResult = render(<OperationsTab />);
|
|
429
|
+
});
|
|
430
|
+
const { container } = renderResult!;
|
|
431
|
+
const text = container.textContent || '';
|
|
432
|
+
|
|
433
|
+
// multiSessions: 1 active, 1 idle, 2 idle in tx (one normal, one aborted), 1 waiting
|
|
434
|
+
expect(text).toContain('Sessions (5)');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// =========================================================================
|
|
438
|
+
// Refresh button
|
|
439
|
+
// =========================================================================
|
|
440
|
+
|
|
441
|
+
test('refresh button calls refresh', async () => {
|
|
442
|
+
let renderResult: ReturnType<typeof render>;
|
|
443
|
+
await act(async () => {
|
|
444
|
+
renderResult = render(<OperationsTab />);
|
|
445
|
+
});
|
|
446
|
+
const { queryByText } = renderResult!;
|
|
447
|
+
|
|
448
|
+
const refreshBtn = queryByText('Refresh');
|
|
449
|
+
expect(refreshBtn).not.toBeNull();
|
|
450
|
+
await act(async () => {
|
|
451
|
+
fireEvent.click(refreshBtn!.closest('button')!);
|
|
452
|
+
});
|
|
453
|
+
expect(mockRefresh).toHaveBeenCalledTimes(1);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// =========================================================================
|
|
457
|
+
// handleRunMaintenance — success
|
|
458
|
+
// =========================================================================
|
|
459
|
+
|
|
460
|
+
test('handleRunMaintenance success adds success log entry', async () => {
|
|
461
|
+
let renderResult: ReturnType<typeof render>;
|
|
462
|
+
await act(async () => {
|
|
463
|
+
renderResult = render(<OperationsTab />);
|
|
464
|
+
});
|
|
465
|
+
const { queryByText } = renderResult!;
|
|
466
|
+
|
|
467
|
+
// Click "Run Analyze"
|
|
468
|
+
const analyzeBtn = queryByText('Run Analyze');
|
|
469
|
+
expect(analyzeBtn).not.toBeNull();
|
|
470
|
+
await act(async () => {
|
|
471
|
+
fireEvent.click(analyzeBtn!.closest('button')!);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(mockRunMaintenance).toHaveBeenCalledWith('analyze', undefined);
|
|
475
|
+
// Operation log should appear with success
|
|
476
|
+
expect(queryByText('Operation Log (this session)')).not.toBeNull();
|
|
477
|
+
expect(queryByText('ANALYZE')).not.toBeNull();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('handleRunMaintenance vacuum', async () => {
|
|
481
|
+
let renderResult: ReturnType<typeof render>;
|
|
482
|
+
await act(async () => {
|
|
483
|
+
renderResult = render(<OperationsTab />);
|
|
484
|
+
});
|
|
485
|
+
const { queryByText } = renderResult!;
|
|
486
|
+
|
|
487
|
+
const vacuumBtn = queryByText('Run Vacuum');
|
|
488
|
+
await act(async () => {
|
|
489
|
+
fireEvent.click(vacuumBtn!.closest('button')!);
|
|
490
|
+
});
|
|
491
|
+
expect(mockRunMaintenance).toHaveBeenCalledWith('vacuum', undefined);
|
|
492
|
+
expect(queryByText('VACUUM')).not.toBeNull();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test('handleRunMaintenance reindex', async () => {
|
|
496
|
+
let renderResult: ReturnType<typeof render>;
|
|
497
|
+
await act(async () => {
|
|
498
|
+
renderResult = render(<OperationsTab />);
|
|
499
|
+
});
|
|
500
|
+
const { queryByText } = renderResult!;
|
|
501
|
+
|
|
502
|
+
const reindexBtn = queryByText('Run Reindex');
|
|
503
|
+
await act(async () => {
|
|
504
|
+
fireEvent.click(reindexBtn!.closest('button')!);
|
|
505
|
+
});
|
|
506
|
+
expect(mockRunMaintenance).toHaveBeenCalledWith('reindex', undefined);
|
|
507
|
+
expect(queryByText('REINDEX')).not.toBeNull();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// =========================================================================
|
|
511
|
+
// handleRunMaintenance — failure (returns false)
|
|
512
|
+
// =========================================================================
|
|
513
|
+
|
|
514
|
+
test('handleRunMaintenance failure shows failure in log', async () => {
|
|
515
|
+
mockRunMaintenance.mockImplementation(() => false);
|
|
516
|
+
let renderResult: ReturnType<typeof render>;
|
|
517
|
+
await act(async () => {
|
|
518
|
+
renderResult = render(<OperationsTab />);
|
|
519
|
+
});
|
|
520
|
+
const { queryByText, container } = renderResult!;
|
|
521
|
+
|
|
522
|
+
const analyzeBtn = queryByText('Run Analyze');
|
|
523
|
+
await act(async () => {
|
|
524
|
+
fireEvent.click(analyzeBtn!.closest('button')!);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
expect(queryByText('ANALYZE')).not.toBeNull();
|
|
528
|
+
// The log entry should show — the component uses XCircle icon for failure
|
|
529
|
+
// We verify the log appears
|
|
530
|
+
expect(queryByText('Operation Log (this session)')).not.toBeNull();
|
|
531
|
+
// Target is 'all' for global operation
|
|
532
|
+
expect(container.textContent).toContain('all');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// =========================================================================
|
|
536
|
+
// handleRunMaintenance — exception (catch block)
|
|
537
|
+
// =========================================================================
|
|
538
|
+
|
|
539
|
+
test('handleRunMaintenance exception adds failure log entry', async () => {
|
|
540
|
+
mockRunMaintenance.mockImplementation(() => { throw new Error('DB error'); });
|
|
541
|
+
let renderResult: ReturnType<typeof render>;
|
|
542
|
+
await act(async () => {
|
|
543
|
+
renderResult = render(<OperationsTab />);
|
|
544
|
+
});
|
|
545
|
+
const { queryByText } = renderResult!;
|
|
546
|
+
|
|
547
|
+
const analyzeBtn = queryByText('Run Analyze');
|
|
548
|
+
await act(async () => {
|
|
549
|
+
fireEvent.click(analyzeBtn!.closest('button')!);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Log should appear with failure entry
|
|
553
|
+
expect(queryByText('Operation Log (this session)')).not.toBeNull();
|
|
554
|
+
expect(queryByText('ANALYZE')).not.toBeNull();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// =========================================================================
|
|
558
|
+
// handleRunMaintenance — per-table operation
|
|
559
|
+
// =========================================================================
|
|
560
|
+
|
|
561
|
+
test('handleRunMaintenance for specific table', async () => {
|
|
562
|
+
let renderResult: ReturnType<typeof render>;
|
|
563
|
+
await act(async () => {
|
|
564
|
+
renderResult = render(<OperationsTab />);
|
|
565
|
+
});
|
|
566
|
+
const { container } = renderResult!;
|
|
567
|
+
|
|
568
|
+
// Find the table row for 'users' and click its analyze button (first icon button)
|
|
569
|
+
const tableRows = container.querySelectorAll('.divide-y > div');
|
|
570
|
+
const usersRow = Array.from(tableRows).find(row => row.textContent?.includes('users'));
|
|
571
|
+
expect(usersRow).not.toBeNull();
|
|
572
|
+
|
|
573
|
+
const buttons = usersRow!.querySelectorAll('button');
|
|
574
|
+
// First button is Analyze, second is Vacuum
|
|
575
|
+
expect(buttons.length).toBeGreaterThanOrEqual(2);
|
|
576
|
+
await act(async () => {
|
|
577
|
+
fireEvent.click(buttons[0]!);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
expect(mockRunMaintenance).toHaveBeenCalledWith('analyze', 'users');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test('per-table vacuum button calls runMaintenance with table name', async () => {
|
|
584
|
+
let renderResult: ReturnType<typeof render>;
|
|
585
|
+
await act(async () => {
|
|
586
|
+
renderResult = render(<OperationsTab />);
|
|
587
|
+
});
|
|
588
|
+
const { container } = renderResult!;
|
|
589
|
+
|
|
590
|
+
const tableRows = container.querySelectorAll('.divide-y > div');
|
|
591
|
+
const usersRow = Array.from(tableRows).find(row => row.textContent?.includes('users'));
|
|
592
|
+
const buttons = usersRow!.querySelectorAll('button');
|
|
593
|
+
await act(async () => {
|
|
594
|
+
fireEvent.click(buttons[1]!);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
expect(mockRunMaintenance).toHaveBeenCalledWith('vacuum', 'users');
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// =========================================================================
|
|
601
|
+
// Kill session flow
|
|
602
|
+
// =========================================================================
|
|
603
|
+
|
|
604
|
+
test('kill button opens confirmation dialog', async () => {
|
|
605
|
+
let renderResult: ReturnType<typeof render>;
|
|
606
|
+
await act(async () => {
|
|
607
|
+
renderResult = render(<OperationsTab />);
|
|
608
|
+
});
|
|
609
|
+
const { container, baseElement } = renderResult!;
|
|
610
|
+
|
|
611
|
+
// Find the session row by PID 1234
|
|
612
|
+
const cells = container.querySelectorAll('td');
|
|
613
|
+
const pidCell = Array.from(cells).find(td => td.textContent?.includes('1234'));
|
|
614
|
+
expect(pidCell).not.toBeNull();
|
|
615
|
+
const row = pidCell!.closest('tr');
|
|
616
|
+
const killBtn = row!.querySelector('td:last-child button');
|
|
617
|
+
expect(killBtn).not.toBeNull();
|
|
618
|
+
|
|
619
|
+
await act(async () => {
|
|
620
|
+
fireEvent.click(killBtn!);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Confirmation dialog should appear (may be portaled)
|
|
624
|
+
const dialogText = baseElement.textContent || '';
|
|
625
|
+
expect(dialogText).toContain('Terminate Session?');
|
|
626
|
+
expect(dialogText).toContain('1234');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test('confirming kill calls killSession and adds log entry', async () => {
|
|
630
|
+
let renderResult: ReturnType<typeof render>;
|
|
631
|
+
await act(async () => {
|
|
632
|
+
renderResult = render(<OperationsTab />);
|
|
633
|
+
});
|
|
634
|
+
const { container, baseElement } = renderResult!;
|
|
635
|
+
|
|
636
|
+
// Click kill button
|
|
637
|
+
const cells = container.querySelectorAll('td');
|
|
638
|
+
const pidCell = Array.from(cells).find(td => td.textContent?.includes('1234'));
|
|
639
|
+
const row = pidCell!.closest('tr');
|
|
640
|
+
const killBtn = row!.querySelector('td:last-child button');
|
|
641
|
+
await act(async () => {
|
|
642
|
+
fireEvent.click(killBtn!);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Find and click "Terminate" button in the dialog
|
|
646
|
+
const allButtons = baseElement.querySelectorAll('button');
|
|
647
|
+
const terminateBtn = Array.from(allButtons).find(btn => btn.textContent?.trim() === 'Terminate');
|
|
648
|
+
expect(terminateBtn).not.toBeNull();
|
|
649
|
+
await act(async () => {
|
|
650
|
+
fireEvent.click(terminateBtn!);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
expect(mockKillSession).toHaveBeenCalledWith(1234);
|
|
654
|
+
// Log entry should appear
|
|
655
|
+
expect(baseElement.textContent).toContain('KILL');
|
|
656
|
+
expect(baseElement.textContent).toContain('PID:1234');
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('cancel kill dialog does not call killSession', async () => {
|
|
660
|
+
let renderResult: ReturnType<typeof render>;
|
|
661
|
+
await act(async () => {
|
|
662
|
+
renderResult = render(<OperationsTab />);
|
|
663
|
+
});
|
|
664
|
+
const { container, baseElement } = renderResult!;
|
|
665
|
+
|
|
666
|
+
// Click kill button
|
|
667
|
+
const cells = container.querySelectorAll('td');
|
|
668
|
+
const pidCell = Array.from(cells).find(td => td.textContent?.includes('1234'));
|
|
669
|
+
const row = pidCell!.closest('tr');
|
|
670
|
+
const killBtn = row!.querySelector('td:last-child button');
|
|
671
|
+
await act(async () => {
|
|
672
|
+
fireEvent.click(killBtn!);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Find and click "Cancel" button
|
|
676
|
+
const allButtons = baseElement.querySelectorAll('button');
|
|
677
|
+
const cancelBtn = Array.from(allButtons).find(btn => btn.textContent?.trim() === 'Cancel');
|
|
678
|
+
expect(cancelBtn).not.toBeNull();
|
|
679
|
+
await act(async () => {
|
|
680
|
+
fireEvent.click(cancelBtn!);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
expect(mockKillSession).not.toHaveBeenCalled();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// =========================================================================
|
|
687
|
+
// Session duration badge variants
|
|
688
|
+
// =========================================================================
|
|
689
|
+
|
|
690
|
+
test('session with >60s shows destructive duration badge', async () => {
|
|
691
|
+
monitoringOverride = {
|
|
692
|
+
data: {
|
|
693
|
+
activeSessions: [
|
|
694
|
+
{ pid: 200, user: 'u1', state: 'active', query: 'Q', duration: '00:02:00', durationMs: 120000, database: 'dev' },
|
|
695
|
+
{ pid: 201, user: 'u2', state: 'idle', query: '', duration: '00:00:05', durationMs: 5000, database: 'dev' },
|
|
696
|
+
],
|
|
697
|
+
tables: defaultTables,
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
let renderResult: ReturnType<typeof render>;
|
|
701
|
+
await act(async () => {
|
|
702
|
+
renderResult = render(<OperationsTab />);
|
|
703
|
+
});
|
|
704
|
+
const { container } = renderResult!;
|
|
705
|
+
const text = container.textContent || '';
|
|
706
|
+
expect(text).toContain('00:02:00');
|
|
707
|
+
expect(text).toContain('00:00:05');
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// =========================================================================
|
|
711
|
+
// Connection selection with saved active ID
|
|
712
|
+
// =========================================================================
|
|
713
|
+
|
|
714
|
+
test('selects saved active connection on mount', async () => {
|
|
715
|
+
mockConnectionsList = [
|
|
716
|
+
{ id: 'c1', name: 'PG Dev', type: 'postgres', host: 'localhost', port: 5432, database: 'dev', createdAt: new Date() },
|
|
717
|
+
{ id: 'c2', name: 'MySQL Prod', type: 'mysql', host: 'localhost', port: 3306, database: 'prod', createdAt: new Date() },
|
|
718
|
+
];
|
|
719
|
+
mockActiveConnectionId = 'c2';
|
|
720
|
+
let renderResult: ReturnType<typeof render>;
|
|
721
|
+
await act(async () => {
|
|
722
|
+
renderResult = render(<OperationsTab />);
|
|
723
|
+
});
|
|
724
|
+
const { queryByText } = renderResult!;
|
|
725
|
+
// Should show MySQL Prod as selected (savedId matches c2)
|
|
726
|
+
expect(queryByText('MySQL Prod')).not.toBeNull();
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('falls back to first connection when savedId not found', async () => {
|
|
730
|
+
mockActiveConnectionId = 'nonexistent';
|
|
731
|
+
let renderResult: ReturnType<typeof render>;
|
|
732
|
+
await act(async () => {
|
|
733
|
+
renderResult = render(<OperationsTab />);
|
|
734
|
+
});
|
|
735
|
+
const { queryByText } = renderResult!;
|
|
736
|
+
expect(queryByText('PG Dev')).not.toBeNull();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test('falls back to first connection when no savedId', async () => {
|
|
740
|
+
mockActiveConnectionId = null;
|
|
741
|
+
let renderResult: ReturnType<typeof render>;
|
|
742
|
+
await act(async () => {
|
|
743
|
+
renderResult = render(<OperationsTab />);
|
|
744
|
+
});
|
|
745
|
+
const { queryByText } = renderResult!;
|
|
746
|
+
expect(queryByText('PG Dev')).not.toBeNull();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// =========================================================================
|
|
750
|
+
// Session with no query shows dash
|
|
751
|
+
// =========================================================================
|
|
752
|
+
|
|
753
|
+
test('session with no query shows dash', async () => {
|
|
754
|
+
monitoringOverride = {
|
|
755
|
+
data: {
|
|
756
|
+
activeSessions: [
|
|
757
|
+
{ pid: 300, user: 'admin', state: 'idle', query: '', duration: '00:00:01', durationMs: 1000, database: 'dev' },
|
|
758
|
+
],
|
|
759
|
+
tables: defaultTables,
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
let renderResult: ReturnType<typeof render>;
|
|
763
|
+
await act(async () => {
|
|
764
|
+
renderResult = render(<OperationsTab />);
|
|
765
|
+
});
|
|
766
|
+
const { container } = renderResult!;
|
|
767
|
+
// When query is empty, component shows '-'
|
|
768
|
+
const cells = container.querySelectorAll('td');
|
|
769
|
+
const queryCell = Array.from(cells).find(td => td.textContent?.trim() === '-');
|
|
770
|
+
expect(queryCell).not.toBeNull();
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// =========================================================================
|
|
774
|
+
// Loading skeletons — tables panel
|
|
775
|
+
// =========================================================================
|
|
776
|
+
|
|
777
|
+
test('shows loading skeletons in tables panel when loading=true and tables empty', async () => {
|
|
778
|
+
monitoringOverride = {
|
|
779
|
+
data: { activeSessions: defaultSessions, tables: [] },
|
|
780
|
+
loading: true,
|
|
781
|
+
};
|
|
782
|
+
let renderResult: ReturnType<typeof render>;
|
|
783
|
+
await act(async () => {
|
|
784
|
+
renderResult = render(<OperationsTab />);
|
|
785
|
+
});
|
|
786
|
+
const { container } = renderResult!;
|
|
787
|
+
|
|
788
|
+
// The tables panel shows 5 Skeleton divs when loading && tables.length === 0
|
|
789
|
+
const allSkeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
|
790
|
+
// Tables panel renders 5 skeletons; sessions panel has data so no skeletons there
|
|
791
|
+
expect(allSkeletons.length).toBe(5);
|
|
792
|
+
// Tables count header still shows 0
|
|
793
|
+
expect(container.textContent).toContain('Tables (0)');
|
|
794
|
+
// Sessions should render normally (not skeletons) since sessions have data
|
|
795
|
+
expect(container.textContent).toContain('Sessions (1)');
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// =========================================================================
|
|
799
|
+
// Loading skeletons — sessions panel
|
|
800
|
+
// =========================================================================
|
|
801
|
+
|
|
802
|
+
test('shows loading skeletons in sessions panel when loading=true and sessions empty', async () => {
|
|
803
|
+
monitoringOverride = {
|
|
804
|
+
data: { activeSessions: [], tables: defaultTables },
|
|
805
|
+
loading: true,
|
|
806
|
+
};
|
|
807
|
+
let renderResult: ReturnType<typeof render>;
|
|
808
|
+
await act(async () => {
|
|
809
|
+
renderResult = render(<OperationsTab />);
|
|
810
|
+
});
|
|
811
|
+
const { container } = renderResult!;
|
|
812
|
+
|
|
813
|
+
// The sessions panel shows 4 Skeleton divs when loading && sessions.length === 0
|
|
814
|
+
const allSkeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
|
815
|
+
expect(allSkeletons.length).toBe(4);
|
|
816
|
+
// Sessions count header still shows 0
|
|
817
|
+
expect(container.textContent).toContain('Sessions (0)');
|
|
818
|
+
// Tables should render normally since tables have data
|
|
819
|
+
expect(container.textContent).toContain('Tables (1)');
|
|
820
|
+
expect(container.textContent).toContain('users');
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// =========================================================================
|
|
824
|
+
// handleConnectionChange with non-existent connection id (guard)
|
|
825
|
+
// =========================================================================
|
|
826
|
+
|
|
827
|
+
test('handleConnectionChange with non-existent id does not change selection', async () => {
|
|
828
|
+
// Use a non-existent savedId to test the guard in handleConnectionChange
|
|
829
|
+
// When savedId doesn't match any connection, it falls back to first connection
|
|
830
|
+
mockConnectionsList = [
|
|
831
|
+
{ id: 'c1', name: 'PG Dev', type: 'postgres', host: 'localhost', port: 5432, database: 'dev', createdAt: new Date() },
|
|
832
|
+
{ id: 'c2', name: 'MySQL Prod', type: 'mysql', host: 'localhost', port: 3306, database: 'prod', createdAt: new Date() },
|
|
833
|
+
];
|
|
834
|
+
// Set savedId to a non-existent id
|
|
835
|
+
mockActiveConnectionId = 'nonexistent-id';
|
|
836
|
+
let renderResult: ReturnType<typeof render>;
|
|
837
|
+
await act(async () => {
|
|
838
|
+
renderResult = render(<OperationsTab />);
|
|
839
|
+
});
|
|
840
|
+
const { container } = renderResult!;
|
|
841
|
+
|
|
842
|
+
// Since savedId doesn't match any connection, the guard falls back to first connection (PG Dev)
|
|
843
|
+
expect(container.textContent).toContain('PG Dev');
|
|
844
|
+
expect(container.textContent).toContain('(postgres)');
|
|
845
|
+
|
|
846
|
+
// The component should not crash and should still render the monitoring data
|
|
847
|
+
expect(container.textContent).toContain('Global Operations');
|
|
848
|
+
expect(container.textContent).toContain('Sessions');
|
|
849
|
+
expect(container.textContent).toContain('Tables');
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// =========================================================================
|
|
853
|
+
// Session duration badge outline variant (10s-60s range)
|
|
854
|
+
// =========================================================================
|
|
855
|
+
|
|
856
|
+
test('session with 10s-60s duration shows outline variant badge', async () => {
|
|
857
|
+
monitoringOverride = {
|
|
858
|
+
data: {
|
|
859
|
+
activeSessions: [
|
|
860
|
+
{ pid: 400, user: 'u1', state: 'active', query: 'SELECT slow()', duration: '00:00:30', durationMs: 30000, database: 'dev' },
|
|
861
|
+
{ pid: 401, user: 'u2', state: 'idle', query: '', duration: '00:00:05', durationMs: 5000, database: 'dev' },
|
|
862
|
+
{ pid: 402, user: 'u3', state: 'active', query: 'SELECT very_slow()', duration: '00:02:00', durationMs: 120000, database: 'dev' },
|
|
863
|
+
],
|
|
864
|
+
tables: defaultTables,
|
|
865
|
+
},
|
|
866
|
+
};
|
|
867
|
+
let renderResult: ReturnType<typeof render>;
|
|
868
|
+
await act(async () => {
|
|
869
|
+
renderResult = render(<OperationsTab />);
|
|
870
|
+
});
|
|
871
|
+
const { container } = renderResult!;
|
|
872
|
+
|
|
873
|
+
// Duration badge variant logic:
|
|
874
|
+
// PID 400: durationMs=30000 (>10000, <=60000) -> variant="outline"
|
|
875
|
+
// PID 401: durationMs=5000 (<=10000) -> variant="secondary"
|
|
876
|
+
// PID 402: durationMs=120000 (>60000) -> variant="destructive"
|
|
877
|
+
|
|
878
|
+
// Badge component renders as <span data-slot="badge">
|
|
879
|
+
const allBadges = Array.from(container.querySelectorAll('span[data-slot="badge"]'));
|
|
880
|
+
|
|
881
|
+
// Find the badge containing '00:00:30' (outline variant for 10s-60s range)
|
|
882
|
+
const durationBadge400 = allBadges.find(
|
|
883
|
+
badge => badge.textContent?.includes('00:00:30')
|
|
884
|
+
);
|
|
885
|
+
expect(durationBadge400).toBeDefined();
|
|
886
|
+
// Outline variant: has text-foreground, does NOT have bg-destructive or bg-secondary
|
|
887
|
+
expect(durationBadge400!.className).toContain('text-foreground');
|
|
888
|
+
expect(durationBadge400!.className).not.toContain('bg-destructive');
|
|
889
|
+
expect(durationBadge400!.className).not.toContain('bg-secondary');
|
|
890
|
+
expect(durationBadge400!.className).not.toContain('border-transparent');
|
|
891
|
+
|
|
892
|
+
// Find the badge containing '00:02:00' (destructive variant for >60s)
|
|
893
|
+
const durationBadge402 = allBadges.find(
|
|
894
|
+
badge => badge.textContent?.includes('00:02:00')
|
|
895
|
+
);
|
|
896
|
+
expect(durationBadge402).toBeDefined();
|
|
897
|
+
expect(durationBadge402!.className).toContain('bg-destructive');
|
|
898
|
+
|
|
899
|
+
// Find the badge containing '00:00:05' (secondary variant for <=10s)
|
|
900
|
+
const durationBadge401 = allBadges.find(
|
|
901
|
+
badge => badge.textContent?.includes('00:00:05')
|
|
902
|
+
);
|
|
903
|
+
expect(durationBadge401).toBeDefined();
|
|
904
|
+
expect(durationBadge401!.className).toContain('bg-secondary');
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// =========================================================================
|
|
908
|
+
// Kill dialog shows user and state in description
|
|
909
|
+
// =========================================================================
|
|
910
|
+
|
|
911
|
+
test('kill dialog shows user and state in description', async () => {
|
|
912
|
+
monitoringOverride = {
|
|
913
|
+
data: {
|
|
914
|
+
activeSessions: [
|
|
915
|
+
{ pid: 500, user: 'db_admin', state: 'idle in transaction', query: 'UPDATE t SET x=1', duration: '00:05:00', durationMs: 300000, database: 'dev' },
|
|
916
|
+
],
|
|
917
|
+
tables: defaultTables,
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
let renderResult: ReturnType<typeof render>;
|
|
921
|
+
await act(async () => {
|
|
922
|
+
renderResult = render(<OperationsTab />);
|
|
923
|
+
});
|
|
924
|
+
const { container, baseElement } = renderResult!;
|
|
925
|
+
|
|
926
|
+
// Find and click the kill button for PID 500
|
|
927
|
+
const cells = container.querySelectorAll('td');
|
|
928
|
+
const pidCell = Array.from(cells).find(td => td.textContent?.includes('500'));
|
|
929
|
+
expect(pidCell).not.toBeNull();
|
|
930
|
+
const row = pidCell!.closest('tr');
|
|
931
|
+
const killBtn = row!.querySelector('td:last-child button');
|
|
932
|
+
expect(killBtn).not.toBeNull();
|
|
933
|
+
|
|
934
|
+
await act(async () => {
|
|
935
|
+
fireEvent.click(killBtn!);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
// Dialog should be open and show user and state info
|
|
939
|
+
const dialogText = baseElement.textContent || '';
|
|
940
|
+
expect(dialogText).toContain('Terminate Session?');
|
|
941
|
+
expect(dialogText).toContain('500');
|
|
942
|
+
// User is shown in the description
|
|
943
|
+
expect(dialogText).toContain('db_admin');
|
|
944
|
+
// State is shown in the description
|
|
945
|
+
expect(dialogText).toContain('idle in transaction');
|
|
946
|
+
// Also verify the warning about uncommitted transactions
|
|
947
|
+
expect(dialogText).toContain('uncommitted transactions');
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// =========================================================================
|
|
951
|
+
// Error hidden when both error AND data present
|
|
952
|
+
// =========================================================================
|
|
953
|
+
|
|
954
|
+
test('error message is hidden when both error and data are present', async () => {
|
|
955
|
+
monitoringOverride = {
|
|
956
|
+
data: { activeSessions: defaultSessions, tables: defaultTables },
|
|
957
|
+
error: 'Intermittent connection error',
|
|
958
|
+
};
|
|
959
|
+
let renderResult: ReturnType<typeof render>;
|
|
960
|
+
await act(async () => {
|
|
961
|
+
renderResult = render(<OperationsTab />);
|
|
962
|
+
});
|
|
963
|
+
const { queryByText, container } = renderResult!;
|
|
964
|
+
|
|
965
|
+
// The error message should NOT be displayed because data is present
|
|
966
|
+
// (source code: `error && !data` — data is truthy, so error div is skipped)
|
|
967
|
+
expect(queryByText('Intermittent connection error')).toBeNull();
|
|
968
|
+
|
|
969
|
+
// But data should still render normally
|
|
970
|
+
expect(queryByText('Sessions (1)')).not.toBeNull();
|
|
971
|
+
expect(queryByText('Tables (1)')).not.toBeNull();
|
|
972
|
+
expect(container.textContent).toContain('users');
|
|
973
|
+
expect(container.textContent).toContain('1234');
|
|
974
|
+
});
|
|
975
|
+
});
|