@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,1328 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
7
|
+
import { storage } from '@/lib/storage';
|
|
8
|
+
import { useAllConnections } from '@/hooks/use-all-connections';
|
|
9
|
+
import { getDBIcon, getDBColor } from '@/lib/db-ui-config';
|
|
10
|
+
import {
|
|
11
|
+
type DatabaseType,
|
|
12
|
+
type DatabaseConnection,
|
|
13
|
+
type QueryHistoryItem,
|
|
14
|
+
ENVIRONMENT_COLORS,
|
|
15
|
+
ENVIRONMENT_LABELS,
|
|
16
|
+
} from '@/lib/types';
|
|
17
|
+
import {
|
|
18
|
+
AreaChart,
|
|
19
|
+
Area,
|
|
20
|
+
XAxis,
|
|
21
|
+
YAxis,
|
|
22
|
+
Tooltip,
|
|
23
|
+
ResponsiveContainer,
|
|
24
|
+
RadialBarChart,
|
|
25
|
+
RadialBar,
|
|
26
|
+
} from 'recharts';
|
|
27
|
+
import { format, subDays, startOfDay } from 'date-fns';
|
|
28
|
+
import {
|
|
29
|
+
Activity,
|
|
30
|
+
RefreshCw,
|
|
31
|
+
TrendingUp,
|
|
32
|
+
TrendingDown,
|
|
33
|
+
Zap,
|
|
34
|
+
Clock,
|
|
35
|
+
Database,
|
|
36
|
+
Wrench,
|
|
37
|
+
Shield,
|
|
38
|
+
ArrowRight,
|
|
39
|
+
CheckCircle2,
|
|
40
|
+
XCircle,
|
|
41
|
+
Link2,
|
|
42
|
+
HardDrive,
|
|
43
|
+
Sparkles,
|
|
44
|
+
Radio,
|
|
45
|
+
Gauge,
|
|
46
|
+
} from 'lucide-react';
|
|
47
|
+
import { motion, AnimatePresence, type Variants } from 'framer-motion';
|
|
48
|
+
import Link from 'next/link';
|
|
49
|
+
import type { FleetHealthItem } from '@/app/api/admin/fleet-health/route';
|
|
50
|
+
import type { AuditEvent } from '@/lib/audit';
|
|
51
|
+
|
|
52
|
+
// ─── Animation Variants ─────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const containerVariants = {
|
|
55
|
+
hidden: { opacity: 0 },
|
|
56
|
+
visible: {
|
|
57
|
+
opacity: 1,
|
|
58
|
+
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
|
|
59
|
+
},
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
const itemVariants = {
|
|
63
|
+
hidden: { opacity: 0, y: 20, scale: 0.95 },
|
|
64
|
+
visible: {
|
|
65
|
+
opacity: 1,
|
|
66
|
+
y: 0,
|
|
67
|
+
scale: 1,
|
|
68
|
+
transition: { type: 'spring' as const, stiffness: 300, damping: 24 },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const heroVariants = {
|
|
73
|
+
hidden: { opacity: 0, y: -30 },
|
|
74
|
+
visible: {
|
|
75
|
+
opacity: 1,
|
|
76
|
+
y: 0,
|
|
77
|
+
transition: { type: 'spring' as const, stiffness: 200, damping: 20 },
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const feedItemVariants: Variants = {
|
|
82
|
+
hidden: { opacity: 0, x: -10 },
|
|
83
|
+
visible: (i: number) => ({
|
|
84
|
+
opacity: 1,
|
|
85
|
+
x: 0,
|
|
86
|
+
transition: { delay: i * 0.05, type: 'spring' as const, stiffness: 300, damping: 24 },
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const DARK_TOOLTIP_STYLE = {
|
|
93
|
+
backgroundColor: '#18181b',
|
|
94
|
+
border: '1px solid rgba(255,255,255,0.1)',
|
|
95
|
+
borderRadius: '8px',
|
|
96
|
+
fontSize: 12,
|
|
97
|
+
color: '#a1a1aa',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const GAUGE_COLORS = {
|
|
101
|
+
excellent: '#10b981',
|
|
102
|
+
good: '#3b82f6',
|
|
103
|
+
warning: '#f59e0b',
|
|
104
|
+
critical: '#ef4444',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function getGaugeColor(value: number, thresholds = { warning: 70, critical: 50 }) {
|
|
110
|
+
if (value >= 90) return GAUGE_COLORS.excellent;
|
|
111
|
+
if (value >= thresholds.warning) return GAUGE_COLORS.good;
|
|
112
|
+
if (value >= thresholds.critical) return GAUGE_COLORS.warning;
|
|
113
|
+
return GAUGE_COLORS.critical;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getGaugeColorReverse(value: number, thresholds = { warning: 200, critical: 500 }) {
|
|
117
|
+
if (value <= 50) return GAUGE_COLORS.excellent;
|
|
118
|
+
if (value <= thresholds.warning) return GAUGE_COLORS.good;
|
|
119
|
+
if (value <= thresholds.critical) return GAUGE_COLORS.warning;
|
|
120
|
+
return GAUGE_COLORS.critical;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatRelativeTime(date: Date | string) {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const then = new Date(date).getTime();
|
|
126
|
+
const diff = Math.max(0, now - then);
|
|
127
|
+
const secs = Math.floor(diff / 1000);
|
|
128
|
+
if (secs < 60) return 'just now';
|
|
129
|
+
const mins = Math.floor(secs / 60);
|
|
130
|
+
if (mins < 60) return `${mins}m ago`;
|
|
131
|
+
const hours = Math.floor(mins / 60);
|
|
132
|
+
if (hours < 24) return `${hours}h ago`;
|
|
133
|
+
const days = Math.floor(hours / 24);
|
|
134
|
+
return `${days}d ago`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatNumber(n: number) {
|
|
138
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
139
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
140
|
+
return n.toString();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
interface ActivityFeedItem {
|
|
146
|
+
id: string;
|
|
147
|
+
type: 'audit' | 'query';
|
|
148
|
+
text: string;
|
|
149
|
+
status: 'success' | 'failure';
|
|
150
|
+
time: string | Date;
|
|
151
|
+
connectionName?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── useAnimatedCounter Hook ─────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
function useAnimatedCounter(target: number, duration = 1500) {
|
|
157
|
+
const [value, setValue] = useState(0);
|
|
158
|
+
const prevTarget = useRef(0);
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const start = prevTarget.current;
|
|
162
|
+
prevTarget.current = target;
|
|
163
|
+
if (target === 0) {
|
|
164
|
+
setValue(0);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const startTime = performance.now();
|
|
169
|
+
let raf: number;
|
|
170
|
+
|
|
171
|
+
const tick = (now: number) => {
|
|
172
|
+
const elapsed = now - startTime;
|
|
173
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
174
|
+
// ease-out cubic
|
|
175
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
176
|
+
setValue(Math.round(start + (target - start) * eased));
|
|
177
|
+
if (progress < 1) {
|
|
178
|
+
raf = requestAnimationFrame(tick);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
raf = requestAnimationFrame(tick);
|
|
183
|
+
return () => cancelAnimationFrame(raf);
|
|
184
|
+
}, [target, duration]);
|
|
185
|
+
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Main Component ──────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
interface OverviewTabProps {
|
|
192
|
+
user: { username: string; role: string } | null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function OverviewTab({ user }: OverviewTabProps) {
|
|
196
|
+
const [connections, setConnections] = useState<DatabaseConnection[]>([]);
|
|
197
|
+
const [history, setHistory] = useState<QueryHistoryItem[]>([]);
|
|
198
|
+
const [fleetHealth, setFleetHealth] = useState<FleetHealthItem[]>([]);
|
|
199
|
+
const [fleetLoading, setFleetLoading] = useState(false);
|
|
200
|
+
const [auditEvents, setAuditEvents] = useState<AuditEvent[]>([]);
|
|
201
|
+
|
|
202
|
+
const { connections: allConns } = useAllConnections();
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
setConnections(allConns);
|
|
205
|
+
setHistory(storage.getHistory());
|
|
206
|
+
}, [allConns]);
|
|
207
|
+
|
|
208
|
+
// Fetch audit events for activity feed
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
fetch('/api/admin/audit?limit=10')
|
|
211
|
+
.then((r) => r.json())
|
|
212
|
+
.then((d) => setAuditEvents(d.events || []))
|
|
213
|
+
.catch(() => {});
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
const fetchFleetHealth = useCallback(async () => {
|
|
217
|
+
if (connections.length === 0) return;
|
|
218
|
+
setFleetLoading(true);
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch('/api/admin/fleet-health', {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { 'Content-Type': 'application/json' },
|
|
223
|
+
body: JSON.stringify({ connections }),
|
|
224
|
+
});
|
|
225
|
+
const data = await res.json();
|
|
226
|
+
if (data.results) setFleetHealth(data.results);
|
|
227
|
+
} catch {
|
|
228
|
+
// silently fail
|
|
229
|
+
} finally {
|
|
230
|
+
setFleetLoading(false);
|
|
231
|
+
}
|
|
232
|
+
}, [connections]);
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (connections.length > 0) fetchFleetHealth();
|
|
236
|
+
}, [connections, fetchFleetHealth]);
|
|
237
|
+
|
|
238
|
+
// Auto-refresh fleet health every 60 seconds
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (connections.length === 0) return;
|
|
241
|
+
const interval = setInterval(fetchFleetHealth, 60000);
|
|
242
|
+
return () => clearInterval(interval);
|
|
243
|
+
}, [connections, fetchFleetHealth]);
|
|
244
|
+
|
|
245
|
+
const queryStats = useMemo(() => {
|
|
246
|
+
const total = history.length;
|
|
247
|
+
const successful = history.filter((h) => h.status === 'success').length;
|
|
248
|
+
const failed = total - successful;
|
|
249
|
+
const successRate = total > 0 ? Math.round((successful / total) * 100) : 0;
|
|
250
|
+
const avgTime =
|
|
251
|
+
total > 0
|
|
252
|
+
? Math.round(
|
|
253
|
+
history.reduce((sum, h) => sum + h.executionTime, 0) / total
|
|
254
|
+
)
|
|
255
|
+
: 0;
|
|
256
|
+
|
|
257
|
+
const now = new Date();
|
|
258
|
+
const byDay: { day: string; success: number; fail: number }[] = [];
|
|
259
|
+
for (let i = 6; i >= 0; i--) {
|
|
260
|
+
const dayStart = startOfDay(subDays(now, i));
|
|
261
|
+
const dayEnd = startOfDay(subDays(now, i - 1));
|
|
262
|
+
const dayItems = history.filter((h) => {
|
|
263
|
+
const t = new Date(h.executedAt).getTime();
|
|
264
|
+
return t >= dayStart.getTime() && t < dayEnd.getTime();
|
|
265
|
+
});
|
|
266
|
+
byDay.push({
|
|
267
|
+
day: format(dayStart, 'EEE'),
|
|
268
|
+
success: dayItems.filter((h) => h.status === 'success').length,
|
|
269
|
+
fail: dayItems.filter((h) => h.status !== 'success').length,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { total, successful, failed, successRate, avgTime, byDay };
|
|
274
|
+
}, [history]);
|
|
275
|
+
|
|
276
|
+
const healthScore = useMemo(() => {
|
|
277
|
+
if (fleetHealth.length === 0) return 0;
|
|
278
|
+
const healthy = fleetHealth.filter((h) => h.status === 'healthy').length;
|
|
279
|
+
return Math.round((healthy / fleetHealth.length) * 100);
|
|
280
|
+
}, [fleetHealth]);
|
|
281
|
+
|
|
282
|
+
const todayQueries = useMemo(() => {
|
|
283
|
+
const todayStart = startOfDay(new Date()).getTime();
|
|
284
|
+
return history.filter((h) => new Date(h.executedAt).getTime() >= todayStart).length;
|
|
285
|
+
}, [history]);
|
|
286
|
+
|
|
287
|
+
const yesterdayQueries = useMemo(() => {
|
|
288
|
+
const now = new Date();
|
|
289
|
+
const yesterdayStart = startOfDay(subDays(now, 1)).getTime();
|
|
290
|
+
const todayStart = startOfDay(now).getTime();
|
|
291
|
+
return history.filter((h) => {
|
|
292
|
+
const t = new Date(h.executedAt).getTime();
|
|
293
|
+
return t >= yesterdayStart && t < todayStart;
|
|
294
|
+
}).length;
|
|
295
|
+
}, [history]);
|
|
296
|
+
|
|
297
|
+
const avgLatency = useMemo(() => {
|
|
298
|
+
const healthy = fleetHealth.filter((h) => h.status !== 'error');
|
|
299
|
+
if (healthy.length === 0) return 0;
|
|
300
|
+
return Math.round(
|
|
301
|
+
healthy.reduce((sum, h) => sum + h.latencyMs, 0) / healthy.length
|
|
302
|
+
);
|
|
303
|
+
}, [fleetHealth]);
|
|
304
|
+
|
|
305
|
+
const totalDBSize = useMemo(() => {
|
|
306
|
+
let totalBytes = 0;
|
|
307
|
+
for (const item of fleetHealth) {
|
|
308
|
+
if (!item.databaseSize) continue;
|
|
309
|
+
const s = item.databaseSize.toLowerCase();
|
|
310
|
+
const num = parseFloat(s);
|
|
311
|
+
if (isNaN(num)) continue;
|
|
312
|
+
if (s.includes('gb')) totalBytes += num * 1024 * 1024 * 1024;
|
|
313
|
+
else if (s.includes('mb')) totalBytes += num * 1024 * 1024;
|
|
314
|
+
else if (s.includes('kb')) totalBytes += num * 1024;
|
|
315
|
+
else totalBytes += num;
|
|
316
|
+
}
|
|
317
|
+
if (totalBytes === 0) return '0';
|
|
318
|
+
if (totalBytes >= 1024 * 1024 * 1024) return `${(totalBytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
319
|
+
if (totalBytes >= 1024 * 1024) return `${(totalBytes / (1024 * 1024)).toFixed(0)} MB`;
|
|
320
|
+
return `${(totalBytes / 1024).toFixed(0)} KB`;
|
|
321
|
+
}, [fleetHealth]);
|
|
322
|
+
|
|
323
|
+
// Activity feed: merge audit events + recent history
|
|
324
|
+
const activityFeed = useMemo(() => {
|
|
325
|
+
const items: ActivityFeedItem[] = [];
|
|
326
|
+
|
|
327
|
+
for (const e of auditEvents) {
|
|
328
|
+
items.push({
|
|
329
|
+
id: e.id,
|
|
330
|
+
type: 'audit',
|
|
331
|
+
text: `${e.action} ${e.target}`,
|
|
332
|
+
status: e.result,
|
|
333
|
+
time: e.timestamp,
|
|
334
|
+
connectionName: e.connectionName,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const h of history.slice(0, 10)) {
|
|
339
|
+
items.push({
|
|
340
|
+
id: `q-${h.executedAt}-${Math.random().toString(36).slice(2, 6)}`,
|
|
341
|
+
type: 'query',
|
|
342
|
+
text: h.query.length > 60 ? h.query.slice(0, 60) + '...' : h.query,
|
|
343
|
+
status: h.status === 'success' ? 'success' : 'failure',
|
|
344
|
+
time: h.executedAt,
|
|
345
|
+
connectionName: h.connectionName,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
items.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime());
|
|
350
|
+
return items.slice(0, 15);
|
|
351
|
+
}, [auditEvents, history]);
|
|
352
|
+
|
|
353
|
+
const hasConnections = connections.length > 0;
|
|
354
|
+
|
|
355
|
+
if (!hasConnections) {
|
|
356
|
+
return <EmptyState />;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<motion.div
|
|
361
|
+
className="space-y-6"
|
|
362
|
+
variants={containerVariants}
|
|
363
|
+
initial="hidden"
|
|
364
|
+
animate="visible"
|
|
365
|
+
>
|
|
366
|
+
{/* SECTION 1: Hero Status Banner */}
|
|
367
|
+
<HeroStatusBanner
|
|
368
|
+
healthScore={healthScore}
|
|
369
|
+
fleetHealth={fleetHealth}
|
|
370
|
+
connections={connections}
|
|
371
|
+
queryStats={queryStats}
|
|
372
|
+
todayQueries={todayQueries}
|
|
373
|
+
yesterdayQueries={yesterdayQueries}
|
|
374
|
+
totalDBSize={totalDBSize}
|
|
375
|
+
user={user}
|
|
376
|
+
fleetLoading={fleetLoading}
|
|
377
|
+
onRefresh={fetchFleetHealth}
|
|
378
|
+
/>
|
|
379
|
+
|
|
380
|
+
{/* SECTION 2: Fleet Health Grid */}
|
|
381
|
+
<FleetHealthSection
|
|
382
|
+
fleetHealth={fleetHealth}
|
|
383
|
+
fleetLoading={fleetLoading}
|
|
384
|
+
connections={connections}
|
|
385
|
+
/>
|
|
386
|
+
|
|
387
|
+
{/* SECTION 3: Key Metrics */}
|
|
388
|
+
<KeyMetricsSection
|
|
389
|
+
queryStats={queryStats}
|
|
390
|
+
healthScore={healthScore}
|
|
391
|
+
avgLatency={avgLatency}
|
|
392
|
+
todayQueries={todayQueries}
|
|
393
|
+
yesterdayQueries={yesterdayQueries}
|
|
394
|
+
/>
|
|
395
|
+
|
|
396
|
+
{/* SECTION 4: Analytics */}
|
|
397
|
+
<AnalyticsSection
|
|
398
|
+
queryStats={queryStats}
|
|
399
|
+
activityFeed={activityFeed}
|
|
400
|
+
/>
|
|
401
|
+
|
|
402
|
+
{/* SECTION 5: Quick Actions */}
|
|
403
|
+
<QuickActionsSection />
|
|
404
|
+
</motion.div>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ─── SECTION 1: Hero Status Banner ──────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
function HeroStatusBanner({
|
|
411
|
+
healthScore,
|
|
412
|
+
fleetHealth,
|
|
413
|
+
connections,
|
|
414
|
+
queryStats,
|
|
415
|
+
todayQueries,
|
|
416
|
+
yesterdayQueries,
|
|
417
|
+
totalDBSize,
|
|
418
|
+
user,
|
|
419
|
+
fleetLoading,
|
|
420
|
+
onRefresh,
|
|
421
|
+
}: {
|
|
422
|
+
healthScore: number;
|
|
423
|
+
fleetHealth: FleetHealthItem[];
|
|
424
|
+
connections: DatabaseConnection[];
|
|
425
|
+
queryStats: { total: number; successRate: number; avgTime: number };
|
|
426
|
+
todayQueries: number;
|
|
427
|
+
yesterdayQueries: number;
|
|
428
|
+
totalDBSize: string;
|
|
429
|
+
user: { username: string; role: string } | null;
|
|
430
|
+
fleetLoading: boolean;
|
|
431
|
+
onRefresh: () => void;
|
|
432
|
+
}) {
|
|
433
|
+
const animatedScore = useAnimatedCounter(healthScore);
|
|
434
|
+
const animatedConns = useAnimatedCounter(connections.length);
|
|
435
|
+
const animatedQueries = useAnimatedCounter(queryStats.total);
|
|
436
|
+
const animatedToday = useAnimatedCounter(todayQueries);
|
|
437
|
+
|
|
438
|
+
const gaugeColor = getGaugeColor(healthScore);
|
|
439
|
+
const gaugeData = [{ value: healthScore, fill: gaugeColor }];
|
|
440
|
+
|
|
441
|
+
const healthyCount = fleetHealth.filter((h) => h.status === 'healthy').length;
|
|
442
|
+
const degradedCount = fleetHealth.filter((h) => h.status === 'degraded').length;
|
|
443
|
+
const errorCount = fleetHealth.filter((h) => h.status === 'error').length;
|
|
444
|
+
|
|
445
|
+
const statusText =
|
|
446
|
+
errorCount > 0
|
|
447
|
+
? 'Attention Required'
|
|
448
|
+
: degradedCount > 0
|
|
449
|
+
? 'Degraded Performance'
|
|
450
|
+
: 'All Systems Operational';
|
|
451
|
+
|
|
452
|
+
const statusColor =
|
|
453
|
+
errorCount > 0
|
|
454
|
+
? 'text-red-400'
|
|
455
|
+
: degradedCount > 0
|
|
456
|
+
? 'text-amber-400'
|
|
457
|
+
: 'text-emerald-400';
|
|
458
|
+
|
|
459
|
+
const statusGlow =
|
|
460
|
+
errorCount > 0
|
|
461
|
+
? 'shadow-[0_0_20px_rgba(239,68,68,0.15)]'
|
|
462
|
+
: degradedCount > 0
|
|
463
|
+
? 'shadow-[0_0_20px_rgba(245,158,11,0.15)]'
|
|
464
|
+
: 'shadow-[0_0_20px_rgba(16,185,129,0.1)]';
|
|
465
|
+
|
|
466
|
+
const queryTrend = todayQueries - yesterdayQueries;
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<motion.div variants={heroVariants} className="relative">
|
|
470
|
+
<div
|
|
471
|
+
className={`relative overflow-hidden rounded-2xl border border-white/5 bg-gradient-to-br from-zinc-900/80 via-zinc-950 to-zinc-900/80 p-6 ${statusGlow}`}
|
|
472
|
+
>
|
|
473
|
+
{/* Decorative blur orbs */}
|
|
474
|
+
<div className="absolute top-0 left-1/4 w-64 h-64 bg-blue-500/5 rounded-full blur-3xl pointer-events-none" />
|
|
475
|
+
<div className="absolute bottom-0 right-1/4 w-48 h-48 bg-emerald-500/5 rounded-full blur-3xl pointer-events-none" />
|
|
476
|
+
|
|
477
|
+
<div className="relative flex flex-col md:flex-row gap-6 items-center">
|
|
478
|
+
{/* Left: Radial Health Gauge */}
|
|
479
|
+
<div className="relative flex-shrink-0">
|
|
480
|
+
<div className="w-[160px] h-[160px]">
|
|
481
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
482
|
+
<RadialBarChart
|
|
483
|
+
innerRadius="70%"
|
|
484
|
+
outerRadius="90%"
|
|
485
|
+
barSize={12}
|
|
486
|
+
data={gaugeData}
|
|
487
|
+
startAngle={90}
|
|
488
|
+
endAngle={-270}
|
|
489
|
+
>
|
|
490
|
+
<RadialBar
|
|
491
|
+
dataKey="value"
|
|
492
|
+
cornerRadius={6}
|
|
493
|
+
background={{ fill: 'rgba(255,255,255,0.04)' }}
|
|
494
|
+
/>
|
|
495
|
+
</RadialBarChart>
|
|
496
|
+
</ResponsiveContainer>
|
|
497
|
+
</div>
|
|
498
|
+
{/* Center overlay */}
|
|
499
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
500
|
+
<span
|
|
501
|
+
className="text-3xl font-bold tabular-nums"
|
|
502
|
+
style={{ color: gaugeColor }}
|
|
503
|
+
>
|
|
504
|
+
{animatedScore}%
|
|
505
|
+
</span>
|
|
506
|
+
<span className="text-[10px] text-zinc-500 uppercase tracking-wider">
|
|
507
|
+
Health
|
|
508
|
+
</span>
|
|
509
|
+
</div>
|
|
510
|
+
{/* LIVE badge */}
|
|
511
|
+
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 flex items-center gap-1.5">
|
|
512
|
+
<motion.div
|
|
513
|
+
className="w-2 h-2 rounded-full bg-emerald-500"
|
|
514
|
+
animate={{ scale: [1, 1.05, 1], opacity: [0.7, 1, 0.7] }}
|
|
515
|
+
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
|
|
516
|
+
/>
|
|
517
|
+
<span className="text-[9px] font-bold text-emerald-400 uppercase tracking-widest">
|
|
518
|
+
Live
|
|
519
|
+
</span>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
{/* Right: Counter Cards + Status */}
|
|
524
|
+
<div className="flex-1 min-w-0 space-y-4">
|
|
525
|
+
{/* Status Line */}
|
|
526
|
+
<div className="flex items-center justify-between gap-3 flex-wrap">
|
|
527
|
+
<div className="flex items-center gap-3">
|
|
528
|
+
<span className={`text-sm font-bold ${statusColor}`}>
|
|
529
|
+
{statusText}
|
|
530
|
+
</span>
|
|
531
|
+
<div className="flex gap-1.5 text-[10px]">
|
|
532
|
+
{healthyCount > 0 && (
|
|
533
|
+
<Badge
|
|
534
|
+
variant="outline"
|
|
535
|
+
className="border-emerald-500/30 text-emerald-400 h-5 text-[9px]"
|
|
536
|
+
>
|
|
537
|
+
{healthyCount} healthy
|
|
538
|
+
</Badge>
|
|
539
|
+
)}
|
|
540
|
+
{degradedCount > 0 && (
|
|
541
|
+
<Badge
|
|
542
|
+
variant="outline"
|
|
543
|
+
className="border-amber-500/30 text-amber-400 h-5 text-[9px]"
|
|
544
|
+
>
|
|
545
|
+
{degradedCount} degraded
|
|
546
|
+
</Badge>
|
|
547
|
+
)}
|
|
548
|
+
{errorCount > 0 && (
|
|
549
|
+
<Badge
|
|
550
|
+
variant="outline"
|
|
551
|
+
className="border-red-500/30 text-red-400 h-5 text-[9px]"
|
|
552
|
+
>
|
|
553
|
+
{errorCount} error
|
|
554
|
+
</Badge>
|
|
555
|
+
)}
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
<div className="flex items-center gap-2">
|
|
559
|
+
{user && (
|
|
560
|
+
<Badge
|
|
561
|
+
variant="outline"
|
|
562
|
+
className="border-white/10 text-zinc-500 h-5 text-[9px]"
|
|
563
|
+
>
|
|
564
|
+
{user.username} ({user.role})
|
|
565
|
+
</Badge>
|
|
566
|
+
)}
|
|
567
|
+
<Button
|
|
568
|
+
variant="ghost"
|
|
569
|
+
size="sm"
|
|
570
|
+
className="h-7 text-[10px] text-zinc-500 hover:text-zinc-300"
|
|
571
|
+
onClick={onRefresh}
|
|
572
|
+
disabled={fleetLoading}
|
|
573
|
+
>
|
|
574
|
+
<RefreshCw
|
|
575
|
+
className={`w-3 h-3 mr-1 ${fleetLoading ? 'animate-spin' : ''}`}
|
|
576
|
+
/>
|
|
577
|
+
Refresh
|
|
578
|
+
</Button>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
{/* Counter Cards */}
|
|
583
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
584
|
+
<CounterCard
|
|
585
|
+
icon={Link2}
|
|
586
|
+
label="Connections"
|
|
587
|
+
value={animatedConns}
|
|
588
|
+
suffix=""
|
|
589
|
+
color="text-blue-400"
|
|
590
|
+
/>
|
|
591
|
+
<CounterCard
|
|
592
|
+
icon={Zap}
|
|
593
|
+
label="Total Queries"
|
|
594
|
+
value={animatedQueries}
|
|
595
|
+
suffix=""
|
|
596
|
+
color="text-purple-400"
|
|
597
|
+
formatValue={formatNumber}
|
|
598
|
+
/>
|
|
599
|
+
<CounterCard
|
|
600
|
+
icon={HardDrive}
|
|
601
|
+
label="DB Size"
|
|
602
|
+
value={totalDBSize}
|
|
603
|
+
suffix=""
|
|
604
|
+
color="text-emerald-400"
|
|
605
|
+
isString
|
|
606
|
+
/>
|
|
607
|
+
<CounterCard
|
|
608
|
+
icon={Activity}
|
|
609
|
+
label="Today"
|
|
610
|
+
value={animatedToday}
|
|
611
|
+
suffix=""
|
|
612
|
+
color="text-amber-400"
|
|
613
|
+
trend={queryTrend}
|
|
614
|
+
/>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</motion.div>
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function CounterCard({
|
|
624
|
+
icon: Icon,
|
|
625
|
+
label,
|
|
626
|
+
value,
|
|
627
|
+
suffix,
|
|
628
|
+
color,
|
|
629
|
+
trend,
|
|
630
|
+
isString,
|
|
631
|
+
formatValue,
|
|
632
|
+
}: {
|
|
633
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
634
|
+
label: string;
|
|
635
|
+
value: number | string;
|
|
636
|
+
suffix: string;
|
|
637
|
+
color: string;
|
|
638
|
+
trend?: number;
|
|
639
|
+
isString?: boolean;
|
|
640
|
+
formatValue?: (n: number) => string;
|
|
641
|
+
}) {
|
|
642
|
+
const displayValue = isString
|
|
643
|
+
? value
|
|
644
|
+
: formatValue
|
|
645
|
+
? formatValue(value as number)
|
|
646
|
+
: value;
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-3">
|
|
650
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
651
|
+
<Icon className={`w-3.5 h-3.5 ${color}`} />
|
|
652
|
+
<span className="text-[10px] text-zinc-500 uppercase tracking-wider">
|
|
653
|
+
{label}
|
|
654
|
+
</span>
|
|
655
|
+
</div>
|
|
656
|
+
<div className="flex items-baseline gap-1">
|
|
657
|
+
<span className="text-2xl font-bold text-zinc-100 tabular-nums">
|
|
658
|
+
{displayValue}
|
|
659
|
+
</span>
|
|
660
|
+
{suffix && (
|
|
661
|
+
<span className="text-xs text-zinc-500">{suffix}</span>
|
|
662
|
+
)}
|
|
663
|
+
</div>
|
|
664
|
+
{trend !== undefined && trend !== 0 && (
|
|
665
|
+
<div
|
|
666
|
+
className={`flex items-center gap-0.5 mt-1 text-[10px] ${
|
|
667
|
+
trend > 0 ? 'text-emerald-400' : 'text-red-400'
|
|
668
|
+
}`}
|
|
669
|
+
>
|
|
670
|
+
{trend > 0 ? (
|
|
671
|
+
<TrendingUp className="w-3 h-3" />
|
|
672
|
+
) : (
|
|
673
|
+
<TrendingDown className="w-3 h-3" />
|
|
674
|
+
)}
|
|
675
|
+
<span>
|
|
676
|
+
{trend > 0 ? '+' : ''}
|
|
677
|
+
{trend} vs yesterday
|
|
678
|
+
</span>
|
|
679
|
+
</div>
|
|
680
|
+
)}
|
|
681
|
+
</div>
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ─── SECTION 2: Fleet Health Grid ────────────────────────────────────────────
|
|
686
|
+
|
|
687
|
+
function FleetHealthSection({
|
|
688
|
+
fleetHealth,
|
|
689
|
+
fleetLoading,
|
|
690
|
+
connections,
|
|
691
|
+
}: {
|
|
692
|
+
fleetHealth: FleetHealthItem[];
|
|
693
|
+
fleetLoading: boolean;
|
|
694
|
+
connections: DatabaseConnection[];
|
|
695
|
+
}) {
|
|
696
|
+
if (connections.length === 0) return null;
|
|
697
|
+
|
|
698
|
+
const getStatusColor = (status: string) => {
|
|
699
|
+
switch (status) {
|
|
700
|
+
case 'healthy':
|
|
701
|
+
return {
|
|
702
|
+
dot: 'bg-emerald-500',
|
|
703
|
+
border: 'border-emerald-500/20 hover:border-emerald-500/40',
|
|
704
|
+
glow: 'hover:shadow-[0_0_30px_rgba(16,185,129,0.08)]',
|
|
705
|
+
gradient: 'from-emerald-500/60 via-emerald-400/40',
|
|
706
|
+
latencyColor: '#10b981',
|
|
707
|
+
};
|
|
708
|
+
case 'degraded':
|
|
709
|
+
return {
|
|
710
|
+
dot: 'bg-amber-500',
|
|
711
|
+
border: 'border-amber-500/20 hover:border-amber-500/40',
|
|
712
|
+
glow: 'hover:shadow-[0_0_30px_rgba(245,158,11,0.08)]',
|
|
713
|
+
gradient: 'from-amber-500/60 via-amber-400/40',
|
|
714
|
+
latencyColor: '#f59e0b',
|
|
715
|
+
};
|
|
716
|
+
default:
|
|
717
|
+
return {
|
|
718
|
+
dot: 'bg-red-500',
|
|
719
|
+
border: 'border-red-500/20 hover:border-red-500/40',
|
|
720
|
+
glow: 'hover:shadow-[0_0_30px_rgba(239,68,68,0.08)]',
|
|
721
|
+
gradient: 'from-red-500/60 via-red-400/40',
|
|
722
|
+
latencyColor: '#ef4444',
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
return (
|
|
728
|
+
<motion.div variants={itemVariants}>
|
|
729
|
+
<div className="flex items-center gap-2 mb-3">
|
|
730
|
+
<Radio className="h-4 w-4 text-blue-400" />
|
|
731
|
+
<h2 className="text-sm font-bold text-zinc-300">Fleet Status</h2>
|
|
732
|
+
<span className="text-[10px] text-zinc-600">
|
|
733
|
+
{fleetHealth.length} endpoint{fleetHealth.length !== 1 ? 's' : ''}
|
|
734
|
+
</span>
|
|
735
|
+
</div>
|
|
736
|
+
|
|
737
|
+
{fleetLoading && fleetHealth.length === 0 ? (
|
|
738
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
739
|
+
{[...Array(3)].map((_, i) => (
|
|
740
|
+
<div
|
|
741
|
+
key={i}
|
|
742
|
+
className="rounded-xl border border-white/5 bg-zinc-900/50 p-4"
|
|
743
|
+
>
|
|
744
|
+
<Skeleton className="h-4 w-24 mb-3 bg-zinc-800" />
|
|
745
|
+
<Skeleton className="h-3 w-32 mb-2 bg-zinc-800" />
|
|
746
|
+
<Skeleton className="h-2 w-full bg-zinc-800" />
|
|
747
|
+
</div>
|
|
748
|
+
))}
|
|
749
|
+
</div>
|
|
750
|
+
) : (
|
|
751
|
+
<motion.div
|
|
752
|
+
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
|
|
753
|
+
variants={containerVariants}
|
|
754
|
+
initial="hidden"
|
|
755
|
+
animate="visible"
|
|
756
|
+
>
|
|
757
|
+
{fleetHealth.map((item) => {
|
|
758
|
+
const colors = getStatusColor(item.status);
|
|
759
|
+
const Icon = getDBIcon(item.type as DatabaseType);
|
|
760
|
+
const maxLatency = 500;
|
|
761
|
+
const latencyPct = Math.min((item.latencyMs / maxLatency) * 100, 100);
|
|
762
|
+
|
|
763
|
+
return (
|
|
764
|
+
<motion.a
|
|
765
|
+
key={item.connectionId}
|
|
766
|
+
href="/admin?tab=monitoring"
|
|
767
|
+
variants={itemVariants}
|
|
768
|
+
whileHover={{ scale: 1.02, y: -2 }}
|
|
769
|
+
className={`group relative rounded-xl border-2 ${colors.border} bg-zinc-900/50 p-4 transition-all duration-200 hover:bg-white/[0.04] ${colors.glow} cursor-pointer block overflow-hidden`}
|
|
770
|
+
>
|
|
771
|
+
{/* Top gradient glow line */}
|
|
772
|
+
<div
|
|
773
|
+
className={`absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent ${colors.gradient} to-transparent`}
|
|
774
|
+
/>
|
|
775
|
+
|
|
776
|
+
<div className="flex items-center justify-between mb-3">
|
|
777
|
+
<div className="flex items-center gap-2">
|
|
778
|
+
<div
|
|
779
|
+
className={`w-2 h-2 rounded-full ${colors.dot} ${item.status === 'healthy' ? 'animate-pulse' : ''}`}
|
|
780
|
+
/>
|
|
781
|
+
<span className="text-sm font-medium text-zinc-200 truncate max-w-[150px]">
|
|
782
|
+
{item.connectionName}
|
|
783
|
+
</span>
|
|
784
|
+
</div>
|
|
785
|
+
<div className="flex items-center gap-2">
|
|
786
|
+
{item.environment && (
|
|
787
|
+
<Badge
|
|
788
|
+
variant="outline"
|
|
789
|
+
className="text-[9px] h-4"
|
|
790
|
+
style={{
|
|
791
|
+
borderColor:
|
|
792
|
+
ENVIRONMENT_COLORS[
|
|
793
|
+
item.environment as keyof typeof ENVIRONMENT_COLORS
|
|
794
|
+
],
|
|
795
|
+
color:
|
|
796
|
+
ENVIRONMENT_COLORS[
|
|
797
|
+
item.environment as keyof typeof ENVIRONMENT_COLORS
|
|
798
|
+
],
|
|
799
|
+
}}
|
|
800
|
+
>
|
|
801
|
+
{ENVIRONMENT_LABELS[
|
|
802
|
+
item.environment as keyof typeof ENVIRONMENT_LABELS
|
|
803
|
+
] || item.environment}
|
|
804
|
+
</Badge>
|
|
805
|
+
)}
|
|
806
|
+
<div className="p-1 rounded bg-white/[0.04]">
|
|
807
|
+
<Icon
|
|
808
|
+
className={`w-3.5 h-3.5 ${getDBColor(item.type as DatabaseType)}`}
|
|
809
|
+
/>
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
{/* Latency bar */}
|
|
815
|
+
<div className="mb-2">
|
|
816
|
+
<div className="h-1.5 rounded-full bg-white/[0.04] overflow-hidden">
|
|
817
|
+
<motion.div
|
|
818
|
+
className="h-full rounded-full"
|
|
819
|
+
style={{ backgroundColor: colors.latencyColor }}
|
|
820
|
+
initial={{ width: 0 }}
|
|
821
|
+
animate={{ width: `${latencyPct}%` }}
|
|
822
|
+
transition={{ duration: 1, delay: 0.3 }}
|
|
823
|
+
/>
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<div className="flex items-center gap-3 text-[11px] text-zinc-500">
|
|
828
|
+
<span className="font-mono text-zinc-400">
|
|
829
|
+
{item.status === 'error' ? 'timeout' : `${item.latencyMs}ms`}
|
|
830
|
+
</span>
|
|
831
|
+
{item.databaseSize && (
|
|
832
|
+
<>
|
|
833
|
+
<span className="text-zinc-700">·</span>
|
|
834
|
+
<span className="font-mono text-zinc-400">
|
|
835
|
+
{item.databaseSize}
|
|
836
|
+
</span>
|
|
837
|
+
</>
|
|
838
|
+
)}
|
|
839
|
+
{item.activeConnections !== undefined && (
|
|
840
|
+
<>
|
|
841
|
+
<span className="text-zinc-700">·</span>
|
|
842
|
+
<span className="font-mono text-zinc-400">
|
|
843
|
+
{item.activeConnections} conn
|
|
844
|
+
</span>
|
|
845
|
+
</>
|
|
846
|
+
)}
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
{item.error && (
|
|
850
|
+
<div className="text-red-400 text-[10px] truncate mt-1.5">
|
|
851
|
+
{item.error}
|
|
852
|
+
</div>
|
|
853
|
+
)}
|
|
854
|
+
</motion.a>
|
|
855
|
+
);
|
|
856
|
+
})}
|
|
857
|
+
</motion.div>
|
|
858
|
+
)}
|
|
859
|
+
</motion.div>
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ─── SECTION 3: Key Metrics Dashboard ────────────────────────────────────────
|
|
864
|
+
|
|
865
|
+
function KeyMetricsSection({
|
|
866
|
+
queryStats,
|
|
867
|
+
healthScore,
|
|
868
|
+
avgLatency,
|
|
869
|
+
todayQueries,
|
|
870
|
+
yesterdayQueries,
|
|
871
|
+
}: {
|
|
872
|
+
queryStats: { total: number; successRate: number; avgTime: number };
|
|
873
|
+
healthScore: number;
|
|
874
|
+
avgLatency: number;
|
|
875
|
+
todayQueries: number;
|
|
876
|
+
yesterdayQueries: number;
|
|
877
|
+
}) {
|
|
878
|
+
return (
|
|
879
|
+
<motion.div variants={itemVariants}>
|
|
880
|
+
<div className="flex items-center gap-2 mb-3">
|
|
881
|
+
<Gauge className="h-4 w-4 text-blue-400" />
|
|
882
|
+
<h2 className="text-sm font-bold text-zinc-300">Key Metrics</h2>
|
|
883
|
+
</div>
|
|
884
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
885
|
+
<MetricGauge
|
|
886
|
+
label="Query Success"
|
|
887
|
+
value={queryStats.successRate}
|
|
888
|
+
unit="%"
|
|
889
|
+
color={getGaugeColor(queryStats.successRate)}
|
|
890
|
+
/>
|
|
891
|
+
<MetricGauge
|
|
892
|
+
label="Fleet Health"
|
|
893
|
+
value={healthScore}
|
|
894
|
+
unit="%"
|
|
895
|
+
color={getGaugeColor(healthScore)}
|
|
896
|
+
/>
|
|
897
|
+
<MetricGauge
|
|
898
|
+
label="Avg Response"
|
|
899
|
+
value={Math.min(avgLatency, 500)}
|
|
900
|
+
displayValue={`${avgLatency}`}
|
|
901
|
+
unit="ms"
|
|
902
|
+
maxValue={500}
|
|
903
|
+
color={getGaugeColorReverse(avgLatency)}
|
|
904
|
+
/>
|
|
905
|
+
<MetricBigNumber
|
|
906
|
+
label="Total Queries"
|
|
907
|
+
value={queryStats.total}
|
|
908
|
+
trend={todayQueries - yesterdayQueries}
|
|
909
|
+
icon={Zap}
|
|
910
|
+
/>
|
|
911
|
+
</div>
|
|
912
|
+
</motion.div>
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function MetricGauge({
|
|
917
|
+
label,
|
|
918
|
+
value,
|
|
919
|
+
displayValue,
|
|
920
|
+
unit,
|
|
921
|
+
color,
|
|
922
|
+
maxValue = 100,
|
|
923
|
+
}: {
|
|
924
|
+
label: string;
|
|
925
|
+
value: number;
|
|
926
|
+
displayValue?: string;
|
|
927
|
+
unit: string;
|
|
928
|
+
color: string;
|
|
929
|
+
maxValue?: number;
|
|
930
|
+
}) {
|
|
931
|
+
const pct = Math.round((value / maxValue) * 100);
|
|
932
|
+
const animatedValue = useAnimatedCounter(value);
|
|
933
|
+
const gaugeData = [{ value: pct, fill: color }];
|
|
934
|
+
|
|
935
|
+
return (
|
|
936
|
+
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-4 flex flex-col items-center">
|
|
937
|
+
<div className="relative w-[100px] h-[100px]">
|
|
938
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
939
|
+
<RadialBarChart
|
|
940
|
+
innerRadius="70%"
|
|
941
|
+
outerRadius="90%"
|
|
942
|
+
barSize={8}
|
|
943
|
+
data={gaugeData}
|
|
944
|
+
startAngle={90}
|
|
945
|
+
endAngle={-270}
|
|
946
|
+
>
|
|
947
|
+
<RadialBar
|
|
948
|
+
dataKey="value"
|
|
949
|
+
cornerRadius={4}
|
|
950
|
+
background={{ fill: 'rgba(255,255,255,0.03)' }}
|
|
951
|
+
/>
|
|
952
|
+
</RadialBarChart>
|
|
953
|
+
</ResponsiveContainer>
|
|
954
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
955
|
+
<span
|
|
956
|
+
className="text-xl font-bold tabular-nums"
|
|
957
|
+
style={{ color }}
|
|
958
|
+
>
|
|
959
|
+
{displayValue ?? animatedValue}
|
|
960
|
+
</span>
|
|
961
|
+
<span className="text-[9px] text-zinc-500">{unit}</span>
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
964
|
+
<span className="text-[10px] text-zinc-500 mt-1 uppercase tracking-wider">
|
|
965
|
+
{label}
|
|
966
|
+
</span>
|
|
967
|
+
</div>
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function MetricBigNumber({
|
|
972
|
+
label,
|
|
973
|
+
value,
|
|
974
|
+
trend,
|
|
975
|
+
icon: Icon,
|
|
976
|
+
}: {
|
|
977
|
+
label: string;
|
|
978
|
+
value: number;
|
|
979
|
+
trend: number;
|
|
980
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
981
|
+
}) {
|
|
982
|
+
const animatedValue = useAnimatedCounter(value);
|
|
983
|
+
|
|
984
|
+
return (
|
|
985
|
+
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-4 flex flex-col items-center justify-center">
|
|
986
|
+
<div className="p-2 rounded-lg bg-purple-500/10 mb-2">
|
|
987
|
+
<Icon className="w-5 h-5 text-purple-400" />
|
|
988
|
+
</div>
|
|
989
|
+
<span className="text-3xl font-bold text-zinc-100 tabular-nums">
|
|
990
|
+
{formatNumber(animatedValue)}
|
|
991
|
+
</span>
|
|
992
|
+
<span className="text-[10px] text-zinc-500 mt-1 uppercase tracking-wider">
|
|
993
|
+
{label}
|
|
994
|
+
</span>
|
|
995
|
+
{trend !== 0 && (
|
|
996
|
+
<div
|
|
997
|
+
className={`flex items-center gap-0.5 mt-1.5 text-[10px] ${
|
|
998
|
+
trend > 0 ? 'text-emerald-400' : 'text-red-400'
|
|
999
|
+
}`}
|
|
1000
|
+
>
|
|
1001
|
+
{trend > 0 ? (
|
|
1002
|
+
<TrendingUp className="w-3 h-3" />
|
|
1003
|
+
) : (
|
|
1004
|
+
<TrendingDown className="w-3 h-3" />
|
|
1005
|
+
)}
|
|
1006
|
+
<span>
|
|
1007
|
+
{trend > 0 ? '+' : ''}
|
|
1008
|
+
{trend} today
|
|
1009
|
+
</span>
|
|
1010
|
+
</div>
|
|
1011
|
+
)}
|
|
1012
|
+
</div>
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// ─── SECTION 4: Analytics ────────────────────────────────────────────────────
|
|
1017
|
+
|
|
1018
|
+
function AnalyticsSection({
|
|
1019
|
+
queryStats,
|
|
1020
|
+
activityFeed,
|
|
1021
|
+
}: {
|
|
1022
|
+
queryStats: { total: number; byDay: { day: string; success: number; fail: number }[] };
|
|
1023
|
+
activityFeed: ActivityFeedItem[];
|
|
1024
|
+
}) {
|
|
1025
|
+
return (
|
|
1026
|
+
<motion.div variants={itemVariants}>
|
|
1027
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
1028
|
+
{/* Gradient AreaChart */}
|
|
1029
|
+
<div className="rounded-xl border border-white/5 bg-zinc-900/50 p-5">
|
|
1030
|
+
<h3 className="text-sm font-bold text-zinc-300 mb-4 flex items-center gap-2">
|
|
1031
|
+
<Activity className="h-4 w-4 text-blue-400" />
|
|
1032
|
+
Query Volume (7 days)
|
|
1033
|
+
</h3>
|
|
1034
|
+
{queryStats.total === 0 ? (
|
|
1035
|
+
<div className="flex items-center justify-center py-8 text-sm text-zinc-600">
|
|
1036
|
+
No query history yet.
|
|
1037
|
+
</div>
|
|
1038
|
+
) : (
|
|
1039
|
+
<div className="h-[200px]">
|
|
1040
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
1041
|
+
<AreaChart data={queryStats.byDay}>
|
|
1042
|
+
<defs>
|
|
1043
|
+
<linearGradient id="gradSuccess" x1="0" y1="0" x2="0" y2="1">
|
|
1044
|
+
<stop offset="5%" stopColor="#10b981" stopOpacity={0.4} />
|
|
1045
|
+
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
|
|
1046
|
+
</linearGradient>
|
|
1047
|
+
<linearGradient id="gradFail" x1="0" y1="0" x2="0" y2="1">
|
|
1048
|
+
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.4} />
|
|
1049
|
+
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
|
|
1050
|
+
</linearGradient>
|
|
1051
|
+
</defs>
|
|
1052
|
+
<XAxis
|
|
1053
|
+
dataKey="day"
|
|
1054
|
+
tick={{ fontSize: 11, fill: '#71717a' }}
|
|
1055
|
+
axisLine={false}
|
|
1056
|
+
tickLine={false}
|
|
1057
|
+
/>
|
|
1058
|
+
<YAxis
|
|
1059
|
+
allowDecimals={false}
|
|
1060
|
+
tick={{ fontSize: 11, fill: '#71717a' }}
|
|
1061
|
+
axisLine={false}
|
|
1062
|
+
tickLine={false}
|
|
1063
|
+
width={30}
|
|
1064
|
+
/>
|
|
1065
|
+
<Tooltip contentStyle={DARK_TOOLTIP_STYLE} />
|
|
1066
|
+
<Area
|
|
1067
|
+
type="monotone"
|
|
1068
|
+
dataKey="success"
|
|
1069
|
+
name="Success"
|
|
1070
|
+
stroke="#10b981"
|
|
1071
|
+
strokeWidth={2}
|
|
1072
|
+
fill="url(#gradSuccess)"
|
|
1073
|
+
stackId="1"
|
|
1074
|
+
animationDuration={1500}
|
|
1075
|
+
/>
|
|
1076
|
+
<Area
|
|
1077
|
+
type="monotone"
|
|
1078
|
+
dataKey="fail"
|
|
1079
|
+
name="Failed"
|
|
1080
|
+
stroke="#ef4444"
|
|
1081
|
+
strokeWidth={2}
|
|
1082
|
+
fill="url(#gradFail)"
|
|
1083
|
+
stackId="1"
|
|
1084
|
+
animationDuration={1500}
|
|
1085
|
+
/>
|
|
1086
|
+
</AreaChart>
|
|
1087
|
+
</ResponsiveContainer>
|
|
1088
|
+
</div>
|
|
1089
|
+
)}
|
|
1090
|
+
</div>
|
|
1091
|
+
|
|
1092
|
+
{/* Recent Activity Feed */}
|
|
1093
|
+
<div className="rounded-xl border border-white/5 bg-zinc-900/50 p-5">
|
|
1094
|
+
<h3 className="text-sm font-bold text-zinc-300 mb-4 flex items-center gap-2">
|
|
1095
|
+
<Clock className="h-4 w-4 text-blue-400" />
|
|
1096
|
+
Recent Activity
|
|
1097
|
+
</h3>
|
|
1098
|
+
{activityFeed.length === 0 ? (
|
|
1099
|
+
<div className="flex items-center justify-center py-8 text-sm text-zinc-600">
|
|
1100
|
+
No recent activity.
|
|
1101
|
+
</div>
|
|
1102
|
+
) : (
|
|
1103
|
+
<div className="max-h-[260px] overflow-y-auto editor-scrollbar space-y-1">
|
|
1104
|
+
<AnimatePresence>
|
|
1105
|
+
{activityFeed.map((item, i) => (
|
|
1106
|
+
<motion.div
|
|
1107
|
+
key={item.id}
|
|
1108
|
+
custom={i}
|
|
1109
|
+
variants={feedItemVariants}
|
|
1110
|
+
initial="hidden"
|
|
1111
|
+
animate="visible"
|
|
1112
|
+
className="flex items-center gap-2.5 py-1.5 px-2 rounded-lg hover:bg-white/[0.03] transition-colors"
|
|
1113
|
+
>
|
|
1114
|
+
{item.type === 'audit' ? (
|
|
1115
|
+
<Wrench className="w-3.5 h-3.5 text-zinc-600 flex-shrink-0" />
|
|
1116
|
+
) : (
|
|
1117
|
+
<Zap className="w-3.5 h-3.5 text-zinc-600 flex-shrink-0" />
|
|
1118
|
+
)}
|
|
1119
|
+
<div className="flex-1 min-w-0">
|
|
1120
|
+
<div className="text-xs text-zinc-400 truncate">
|
|
1121
|
+
{item.text}
|
|
1122
|
+
</div>
|
|
1123
|
+
{item.connectionName && (
|
|
1124
|
+
<div className="text-[10px] text-zinc-600 truncate">
|
|
1125
|
+
{item.connectionName}
|
|
1126
|
+
</div>
|
|
1127
|
+
)}
|
|
1128
|
+
</div>
|
|
1129
|
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
1130
|
+
{item.status === 'success' ? (
|
|
1131
|
+
<CheckCircle2 className="w-3 h-3 text-emerald-500" />
|
|
1132
|
+
) : (
|
|
1133
|
+
<XCircle className="w-3 h-3 text-red-500" />
|
|
1134
|
+
)}
|
|
1135
|
+
<span className="text-[10px] text-zinc-600 whitespace-nowrap">
|
|
1136
|
+
{formatRelativeTime(item.time)}
|
|
1137
|
+
</span>
|
|
1138
|
+
</div>
|
|
1139
|
+
</motion.div>
|
|
1140
|
+
))}
|
|
1141
|
+
</AnimatePresence>
|
|
1142
|
+
</div>
|
|
1143
|
+
)}
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
</motion.div>
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// ─── SECTION 5: Quick Actions ────────────────────────────────────────────────
|
|
1151
|
+
|
|
1152
|
+
function QuickActionsSection() {
|
|
1153
|
+
const actions = [
|
|
1154
|
+
{
|
|
1155
|
+
label: 'Maintenance',
|
|
1156
|
+
description: 'VACUUM, ANALYZE, and optimize your databases',
|
|
1157
|
+
icon: Wrench,
|
|
1158
|
+
href: '/admin?tab=operations',
|
|
1159
|
+
gradient: 'from-blue-500/20 to-cyan-500/20',
|
|
1160
|
+
iconColor: 'text-blue-400',
|
|
1161
|
+
borderColor: 'hover:border-blue-500/30',
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
label: 'Security & Masking',
|
|
1165
|
+
description: 'Configure data masking rules and access control',
|
|
1166
|
+
icon: Shield,
|
|
1167
|
+
href: '/admin?tab=security',
|
|
1168
|
+
gradient: 'from-emerald-500/20 to-teal-500/20',
|
|
1169
|
+
iconColor: 'text-emerald-400',
|
|
1170
|
+
borderColor: 'hover:border-emerald-500/30',
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
label: 'Real-time Monitoring',
|
|
1174
|
+
description: 'Live metrics, connection pools, and alert thresholds',
|
|
1175
|
+
icon: Activity,
|
|
1176
|
+
href: '/admin?tab=monitoring',
|
|
1177
|
+
gradient: 'from-purple-500/20 to-pink-500/20',
|
|
1178
|
+
iconColor: 'text-purple-400',
|
|
1179
|
+
borderColor: 'hover:border-purple-500/30',
|
|
1180
|
+
},
|
|
1181
|
+
];
|
|
1182
|
+
|
|
1183
|
+
return (
|
|
1184
|
+
<motion.div variants={itemVariants}>
|
|
1185
|
+
<div className="flex items-center gap-2 mb-3">
|
|
1186
|
+
<Sparkles className="h-4 w-4 text-blue-400" />
|
|
1187
|
+
<h2 className="text-sm font-bold text-zinc-300">Quick Actions</h2>
|
|
1188
|
+
</div>
|
|
1189
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
1190
|
+
{actions.map((action) => (
|
|
1191
|
+
<motion.a
|
|
1192
|
+
key={action.label}
|
|
1193
|
+
href={action.href}
|
|
1194
|
+
whileHover={{ scale: 1.02, y: -4 }}
|
|
1195
|
+
className={`group relative rounded-xl border border-white/5 ${action.borderColor} bg-zinc-900/50 p-5 transition-all duration-200 cursor-pointer block overflow-hidden hover:shadow-[0_0_30px_rgba(59,130,246,0.06)]`}
|
|
1196
|
+
>
|
|
1197
|
+
{/* Background gradient on hover */}
|
|
1198
|
+
<div
|
|
1199
|
+
className={`absolute inset-0 bg-gradient-to-br ${action.gradient} opacity-0 group-hover:opacity-100 transition-opacity duration-300`}
|
|
1200
|
+
/>
|
|
1201
|
+
|
|
1202
|
+
<div className="relative">
|
|
1203
|
+
<div className={`p-2 rounded-lg bg-white/[0.04] w-fit mb-3`}>
|
|
1204
|
+
<action.icon className={`w-5 h-5 ${action.iconColor}`} />
|
|
1205
|
+
</div>
|
|
1206
|
+
<h3 className="text-sm font-bold text-zinc-200 mb-1">
|
|
1207
|
+
{action.label}
|
|
1208
|
+
</h3>
|
|
1209
|
+
<p className="text-[11px] text-zinc-500 mb-3">
|
|
1210
|
+
{action.description}
|
|
1211
|
+
</p>
|
|
1212
|
+
<div className="flex items-center gap-1 text-[11px] text-zinc-600 group-hover:text-zinc-400 transition-colors">
|
|
1213
|
+
<span>Open</span>
|
|
1214
|
+
<ArrowRight className="w-3 h-3 group-hover:translate-x-1 transition-transform" />
|
|
1215
|
+
</div>
|
|
1216
|
+
</div>
|
|
1217
|
+
</motion.a>
|
|
1218
|
+
))}
|
|
1219
|
+
</div>
|
|
1220
|
+
</motion.div>
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// ─── Empty State ─────────────────────────────────────────────────────────────
|
|
1225
|
+
|
|
1226
|
+
function EmptyState() {
|
|
1227
|
+
const features = [
|
|
1228
|
+
{
|
|
1229
|
+
icon: Database,
|
|
1230
|
+
label: '7 DB Types',
|
|
1231
|
+
description: 'PostgreSQL, MySQL, SQLite, MongoDB, Redis, Oracle, MSSQL',
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
icon: Sparkles,
|
|
1235
|
+
label: 'AI Queries',
|
|
1236
|
+
description: 'Natural language to SQL with multi-model AI support',
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
icon: Activity,
|
|
1240
|
+
label: 'Real-time Monitor',
|
|
1241
|
+
description: 'Live metrics, alerts, and connection pool monitoring',
|
|
1242
|
+
},
|
|
1243
|
+
];
|
|
1244
|
+
|
|
1245
|
+
return (
|
|
1246
|
+
<div className="relative min-h-[70vh] flex flex-col items-center justify-center px-4">
|
|
1247
|
+
{/* Animated breathing orbs */}
|
|
1248
|
+
<motion.div
|
|
1249
|
+
className="absolute top-1/4 left-1/3 w-72 h-72 bg-blue-500/5 rounded-full blur-3xl pointer-events-none"
|
|
1250
|
+
animate={{ scale: [1, 1.2, 1], opacity: [0.3, 0.5, 0.3] }}
|
|
1251
|
+
transition={{ duration: 8, repeat: Infinity, ease: 'easeInOut' }}
|
|
1252
|
+
/>
|
|
1253
|
+
<motion.div
|
|
1254
|
+
className="absolute bottom-1/4 right-1/3 w-56 h-56 bg-emerald-500/5 rounded-full blur-3xl pointer-events-none"
|
|
1255
|
+
animate={{ scale: [1.2, 1, 1.2], opacity: [0.3, 0.5, 0.3] }}
|
|
1256
|
+
transition={{ duration: 8, repeat: Infinity, ease: 'easeInOut', delay: 2 }}
|
|
1257
|
+
/>
|
|
1258
|
+
|
|
1259
|
+
<motion.div
|
|
1260
|
+
className="relative z-10 flex flex-col items-center text-center"
|
|
1261
|
+
variants={containerVariants}
|
|
1262
|
+
initial="hidden"
|
|
1263
|
+
animate="visible"
|
|
1264
|
+
>
|
|
1265
|
+
{/* Database icon with pulse glow */}
|
|
1266
|
+
<motion.div
|
|
1267
|
+
variants={itemVariants}
|
|
1268
|
+
className="relative mb-6"
|
|
1269
|
+
>
|
|
1270
|
+
<div className="absolute inset-0 w-20 h-20 bg-blue-500/20 rounded-full blur-xl" />
|
|
1271
|
+
<motion.div
|
|
1272
|
+
className="relative p-5 rounded-2xl border border-white/10 bg-zinc-900/80"
|
|
1273
|
+
animate={{ boxShadow: ['0 0 20px rgba(59,130,246,0.1)', '0 0 40px rgba(59,130,246,0.2)', '0 0 20px rgba(59,130,246,0.1)'] }}
|
|
1274
|
+
transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }}
|
|
1275
|
+
>
|
|
1276
|
+
<Database className="w-10 h-10 text-blue-400" />
|
|
1277
|
+
</motion.div>
|
|
1278
|
+
</motion.div>
|
|
1279
|
+
|
|
1280
|
+
<motion.h2
|
|
1281
|
+
variants={itemVariants}
|
|
1282
|
+
className="text-2xl font-bold text-zinc-100 mb-2"
|
|
1283
|
+
>
|
|
1284
|
+
Welcome to Command Center
|
|
1285
|
+
</motion.h2>
|
|
1286
|
+
<motion.p
|
|
1287
|
+
variants={itemVariants}
|
|
1288
|
+
className="text-sm text-zinc-500 max-w-md mb-8"
|
|
1289
|
+
>
|
|
1290
|
+
Connect your first database to unlock real-time fleet monitoring,
|
|
1291
|
+
analytics, and intelligent query assistance.
|
|
1292
|
+
</motion.p>
|
|
1293
|
+
|
|
1294
|
+
{/* Feature cards */}
|
|
1295
|
+
<motion.div
|
|
1296
|
+
variants={containerVariants}
|
|
1297
|
+
className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8 w-full max-w-2xl"
|
|
1298
|
+
>
|
|
1299
|
+
{features.map((f) => (
|
|
1300
|
+
<motion.div
|
|
1301
|
+
key={f.label}
|
|
1302
|
+
variants={itemVariants}
|
|
1303
|
+
className="rounded-xl border border-white/5 bg-white/[0.02] p-4 text-center"
|
|
1304
|
+
>
|
|
1305
|
+
<div className="p-2 rounded-lg bg-blue-500/10 w-fit mx-auto mb-2">
|
|
1306
|
+
<f.icon className="w-5 h-5 text-blue-400" />
|
|
1307
|
+
</div>
|
|
1308
|
+
<div className="text-sm font-bold text-zinc-200 mb-1">
|
|
1309
|
+
{f.label}
|
|
1310
|
+
</div>
|
|
1311
|
+
<div className="text-[11px] text-zinc-500">{f.description}</div>
|
|
1312
|
+
</motion.div>
|
|
1313
|
+
))}
|
|
1314
|
+
</motion.div>
|
|
1315
|
+
|
|
1316
|
+
{/* CTA Button */}
|
|
1317
|
+
<motion.div variants={itemVariants}>
|
|
1318
|
+
<Button
|
|
1319
|
+
asChild
|
|
1320
|
+
className="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2.5 shadow-[0_0_30px_rgba(59,130,246,0.3)] hover:shadow-[0_0_40px_rgba(59,130,246,0.4)] transition-all"
|
|
1321
|
+
>
|
|
1322
|
+
<Link href="/">Connect Your First Database</Link>
|
|
1323
|
+
</Button>
|
|
1324
|
+
</motion.div>
|
|
1325
|
+
</motion.div>
|
|
1326
|
+
</div>
|
|
1327
|
+
);
|
|
1328
|
+
}
|