From 3352d3cd5ce23559218a168eca68acccac87bb27 Mon Sep 17 00:00:00 2001 From: abdullahmia Date: Tue, 23 Jun 2026 22:11:15 +0600 Subject: [PATCH 1/2] fix(connection): surface classified mongodb errors and fix state inconsistencies --- .../connection/connection-page.component.tsx | 49 ++++----- .../src/lib/driver/connection-manager.ts | 100 +++++++----------- .../compooss/src/lib/driver/mongodb.driver.ts | 14 +-- .../compooss/src/lib/utils/connection.util.ts | 46 ++++++++ 4 files changed, 114 insertions(+), 95 deletions(-) create mode 100644 apps/compooss/src/lib/utils/connection.util.ts diff --git a/apps/compooss/src/lib/components/connection/connection-page.component.tsx b/apps/compooss/src/lib/components/connection/connection-page.component.tsx index ed37ab5..56f83e1 100644 --- a/apps/compooss/src/lib/components/connection/connection-page.component.tsx +++ b/apps/compooss/src/lib/components/connection/connection-page.component.tsx @@ -16,11 +16,12 @@ import { ConnectionList } from "./connection-list.component"; export const ConnectionPage: React.FC = () => { const router = useRouter(); - const { connect, testConnection, isConnecting } = useConnection(); + const { connect, disconnect, testConnection, isConnecting } = useConnection(); const [connections, setConnections] = useState([]); const [editingConnection, setEditingConnection] = useState(null); const [connectingId, setConnectingId] = useState(null); + const [submitError, setSubmitError] = useState(null); const loadConnections = useCallback(async () => { const all = await connectionDB.getAll(); @@ -32,21 +33,7 @@ export const ConnectionPage: React.FC = () => { }, [loadConnections]); const handleFormSubmit = async (data: TConnectionForm) => { - let effectiveUri = data.connectionString; - - try { - const result = await testConnection(data.connectionString); - if (!result.ok) { - toast.error(result.message || "Connection test failed"); - return; - } - if (result.resolvedUri) { - effectiveUri = result.resolvedUri; - } - } catch { - toast.error("Connection test failed"); - return; - } + setSubmitError(null); const id = editingConnection?.id ?? crypto.randomUUID(); const now = new Date().toISOString(); @@ -54,7 +41,7 @@ export const ConnectionPage: React.FC = () => { const saved: SavedConnection = { id, name: data.connectionName, - uri: effectiveUri, + uri: data.connectionString, color: data.color, label: data.label, isFavorite: data.isFavorite, @@ -67,19 +54,25 @@ export const ConnectionPage: React.FC = () => { lastUsedAt: now, }; + let status; try { - const status = await connect(saved); - if (status?.resolvedUri) { - toast.info("Docker detected — saved connection using `mongo` instead of `localhost`"); - } + status = await connect(saved); } catch (err) { - toast.error(err instanceof Error ? err.message : "Connection failed"); + const apiErr = err as { payload?: { message?: string } }; + const msg = apiErr?.payload?.message ?? (err instanceof Error ? err.message : "Connection failed"); + setSubmitError(msg); return; } + if (status?.resolvedUri) { + saved.uri = status.resolvedUri; + toast.info("Docker detected — saved connection using `mongo` instead of `localhost`"); + } + try { await apiClient.get(ENDPOINTS.databases.root); } catch { + await disconnect(); toast.error("Unable to reach the database. Please check your connection string."); return; } @@ -100,17 +93,13 @@ export const ConnectionPage: React.FC = () => { const handleConnect = async (connection: SavedConnection) => { setConnectingId(connection.id); try { - const result = await testConnection(connection.uri); - if (!result.ok) { - toast.error(result.message || "Connection test failed"); - return; - } - await connect(connection); toast.success(`Connected to ${connection.name}`); router.push("/"); } catch (err) { - toast.error(err instanceof Error ? err.message : "Connection failed"); + const apiErr = err as { payload?: { message?: string } }; + const msg = apiErr?.payload?.message ?? (err instanceof Error ? err.message : "Connection failed"); + toast.error(msg); } finally { setConnectingId(null); } @@ -136,6 +125,7 @@ export const ConnectionPage: React.FC = () => { const handleCancelEdit = () => { setEditingConnection(null); + setSubmitError(null); }; const formDefaults: Partial | undefined = editingConnection @@ -218,6 +208,7 @@ export const ConnectionPage: React.FC = () => { onTest={handleTest} isConnecting={isConnecting} editMode={!!editingConnection} + submitError={submitError} /> diff --git a/apps/compooss/src/lib/driver/connection-manager.ts b/apps/compooss/src/lib/driver/connection-manager.ts index 6326bf0..0e5bffe 100644 --- a/apps/compooss/src/lib/driver/connection-manager.ts +++ b/apps/compooss/src/lib/driver/connection-manager.ts @@ -1,31 +1,13 @@ import type { MongoClientOptions } from "mongodb"; import type { ConnectionStatus, ConnectionTestResult } from "@compooss/types"; +import { + classifyMongoError, + isAuthError, + maskUri, + resolveDockerHost, +} from "@/lib/utils/connection.util"; import { MongoDriver } from "./mongodb.driver"; -function maskUri(uri: string): string { - try { - return uri.replace( - /^mongodb(\+srv)?:\/\/([^:]+):([^@]+)@/, - "mongodb$1://$2:***@", - ); - } catch { - return uri; - } -} - -function resolveDockerHost(uri: string): string | null { - try { - const url = new URL(uri); - if (url.hostname === "localhost" || url.hostname === "127.0.0.1") { - url.hostname = "mongo"; - return url.toString(); - } - return null; - } catch { - return null; - } -} - class ConnectionManager { private static _instance: ConnectionManager | null = null; private activeDriver: MongoDriver | null = null; @@ -47,36 +29,38 @@ class ConnectionManager { } const driver = new MongoDriver(uri, options); - let ok = await driver.ping(); let resolvedUri: string | undefined; - if (!ok) { + try { + await driver.ping(); + this.activeDriver = driver; + this.activeUri = uri; + } catch (err) { await driver.disconnect(); + + if (isAuthError(err)) { + throw new Error(classifyMongoError(err)); + } + const fallback = resolveDockerHost(uri); - if (fallback) { - const fallbackDriver = new MongoDriver(fallback, options); - const fallbackOk = await fallbackDriver.ping(); - if (!fallbackOk) { - await fallbackDriver.disconnect(); - throw new Error( - "Could not connect to MongoDB. Check the connection string and ensure the server is running.", - ); - } - this.activeDriver = fallbackDriver; - this.activeUri = fallback; - resolvedUri = fallback; - ok = true; - } else { - throw new Error( - "Could not connect to MongoDB. Check the connection string and ensure the server is running.", - ); + if (!fallback) { + throw new Error(classifyMongoError(err)); } - } else { - this.activeDriver = driver; - this.activeUri = uri; + + const fallbackDriver = new MongoDriver(fallback, options); + try { + await fallbackDriver.ping(); + } catch (fallbackErr) { + await fallbackDriver.disconnect(); + throw new Error(classifyMongoError(fallbackErr)); + } + + this.activeDriver = fallbackDriver; + this.activeUri = fallback; + resolvedUri = fallback; } - const serverInfo = await this.activeDriver.getServerInfo(); + const serverInfo = await this.activeDriver!.getServerInfo(); return { connected: true, maskedUri: maskUri(this.activeUri!), @@ -99,32 +83,30 @@ class ConnectionManager { ): Promise { const driver = new MongoDriver(uri, options); try { - const ok = await driver.ping(); - if (ok) { - const serverInfo = await driver.getServerInfo(); - return { ok: true, message: "Connection successful", serverInfo }; + await driver.ping(); + const serverInfo = await driver.getServerInfo(); + return { ok: true, message: "Connection successful", serverInfo }; + } catch (err) { + if (isAuthError(err)) { + return { ok: false, message: classifyMongoError(err) }; } const fallback = resolveDockerHost(uri); if (!fallback) { - return { ok: false, message: "Ping failed. Check your connection string." }; + return { ok: false, message: classifyMongoError(err) }; } await driver.disconnect(); const fallbackDriver = new MongoDriver(fallback, options); try { - const fallbackOk = await fallbackDriver.ping(); - if (!fallbackOk) { - return { ok: false, message: "Ping failed. Check your connection string." }; - } + await fallbackDriver.ping(); const serverInfo = await fallbackDriver.getServerInfo(); return { ok: true, message: "Connection successful", resolvedUri: fallback, serverInfo }; + } catch (fallbackErr) { + return { ok: false, message: classifyMongoError(fallbackErr) }; } finally { await fallbackDriver.disconnect(); } - } catch (err) { - const message = err instanceof Error ? err.message : "Connection test failed"; - return { ok: false, message }; } finally { await driver.disconnect(); } diff --git a/apps/compooss/src/lib/driver/mongodb.driver.ts b/apps/compooss/src/lib/driver/mongodb.driver.ts index 2cb2d89..5e30261 100644 --- a/apps/compooss/src/lib/driver/mongodb.driver.ts +++ b/apps/compooss/src/lib/driver/mongodb.driver.ts @@ -36,20 +36,20 @@ export class MongoDriver { } } - async ping(): Promise { + async ping(): Promise { + const client = await this.getClient(); + await client.db("admin").command({ ping: 1 }); + } + + async isConnected(): Promise { try { - const client = await this.getClient(); - await client.db("admin").command({ ping: 1 }); + await this.ping(); return true; } catch { return false; } } - async isConnected(): Promise { - return this.ping(); - } - async getDb(dbName: string): Promise { const client = await this.getClient(); return client.db(dbName); diff --git a/apps/compooss/src/lib/utils/connection.util.ts b/apps/compooss/src/lib/utils/connection.util.ts new file mode 100644 index 0000000..758e258 --- /dev/null +++ b/apps/compooss/src/lib/utils/connection.util.ts @@ -0,0 +1,46 @@ +import { + MongoParseError, + MongoServerError, + MongoServerSelectionError, +} from "mongodb"; + +export function classifyMongoError(err: unknown): string { + if (err instanceof MongoServerError && err.code === 18) { + return "Authentication failed. Check the username and password in your connection string."; + } + if (err instanceof MongoServerSelectionError) { + return "Could not reach the server. Check the host and port."; + } + if (err instanceof MongoParseError) { + return "Invalid connection string format."; + } + return err instanceof Error ? err.message : "Connection failed."; +} + +export function isAuthError(err: unknown): boolean { + return err instanceof MongoServerError && err.code === 18; +} + +export function maskUri(uri: string): string { + try { + return uri.replace( + /^mongodb(\+srv)?:\/\/([^:]+):([^@]+)@/, + "mongodb$1://$2:***@", + ); + } catch { + return uri; + } +} + +export function resolveDockerHost(uri: string): string | null { + try { + const url = new URL(uri); + if (url.hostname === "localhost" || url.hostname === "127.0.0.1") { + url.hostname = "mongo"; + return url.toString(); + } + return null; + } catch { + return null; + } +} From c27464dd0ee6294a10bbfacb6930479bdba74cd5 Mon Sep 17 00:00:00 2001 From: abdullahmia Date: Tue, 23 Jun 2026 22:24:00 +0600 Subject: [PATCH 2/2] feat(connection): enhance ui and improve connection list --- .../connection/connection-card.component.tsx | 37 ++++--- .../connection/connection-list.component.tsx | 99 +++++++------------ .../connection/connection-page.component.tsx | 21 ++-- 3 files changed, 71 insertions(+), 86 deletions(-) diff --git a/apps/compooss/src/lib/components/connection/connection-card.component.tsx b/apps/compooss/src/lib/components/connection/connection-card.component.tsx index 0c426a1..632eb67 100644 --- a/apps/compooss/src/lib/components/connection/connection-card.component.tsx +++ b/apps/compooss/src/lib/components/connection/connection-card.component.tsx @@ -26,10 +26,18 @@ export const ConnectionCard: React.FC = ({ return (
onConnect(connection)} > - {/* Color strip */} + {/* Subtle color tint overlay */} + {connection.color && ( +
+ )} + + {/* Color accent strip */} {connection.color && (
= ({ /> )} -
- {/* Color dot (when no strip, still show dot) */} +
{!connection.color && ( -
+
)} {/* Info */} @@ -50,24 +57,24 @@ export const ConnectionCard: React.FC = ({ {connection.name} {connection.isFavorite && ( - + )} {connection.label && ( - + {connection.label} )}
{connection.lastUsedAt && ( - + {formatDistanceToNow(new Date(connection.lastUsedAt), { addSuffix: true })} )}
- {/* Actions — visible on hover */} -
+ {/* Hover actions */} +
= ({ } label={connection.isFavorite ? "Unfavorite" : "Favorite"} className={cn( - "rounded-lg", - !connection.isFavorite && "text-muted-foreground/50 hover:text-warning", + "rounded-md", + !connection.isFavorite && "text-muted-foreground/40 hover:text-warning", )} onClick={(e) => { e.stopPropagation(); @@ -90,7 +97,7 @@ export const ConnectionCard: React.FC = ({ } label="Edit" - className="rounded-lg text-muted-foreground/50 hover:text-foreground" + className="rounded-md text-muted-foreground/40 hover:text-foreground" onClick={(e) => { e.stopPropagation(); onEdit(connection); @@ -100,7 +107,7 @@ export const ConnectionCard: React.FC = ({ variant="danger" icon={} label="Delete" - className="rounded-lg" + className="rounded-md" onClick={(e) => { e.stopPropagation(); onDelete(connection.id); @@ -113,7 +120,7 @@ export const ConnectionCard: React.FC = ({ size="sm" icon={} loading={isConnecting} - className="rounded-lg shrink-0 ml-1" + className="rounded-lg shrink-0 text-xs h-7 px-2.5" onClick={(e) => { e.stopPropagation(); onConnect(connection); diff --git a/apps/compooss/src/lib/components/connection/connection-list.component.tsx b/apps/compooss/src/lib/components/connection/connection-list.component.tsx index 2f044ce..02c1b28 100644 --- a/apps/compooss/src/lib/components/connection/connection-list.component.tsx +++ b/apps/compooss/src/lib/components/connection/connection-list.component.tsx @@ -1,13 +1,11 @@ "use client"; import type { SavedConnection } from "@compooss/types"; -import { IconButton, Input, cn } from "@compooss/ui"; -import { ArrowDownAZ, Clock, Search, Star, X } from "lucide-react"; +import { cn } from "@compooss/ui"; +import { Clock, Search, Star, X } from "lucide-react"; import { useMemo, useState } from "react"; import { ConnectionCard } from "./connection-card.component"; -type SortKey = "name" | "lastUsedAt" | "createdAt"; - type Props = { connections: SavedConnection[]; onConnect: (connection: SavedConnection) => void; @@ -27,7 +25,6 @@ export const ConnectionList: React.FC = ({ }) => { const [search, setSearch] = useState(""); const [showFavoritesOnly, setShowFavoritesOnly] = useState(false); - const [sortBy, setSortBy] = useState("lastUsedAt"); const filtered = useMemo(() => { let result = connections; @@ -46,22 +43,11 @@ export const ConnectionList: React.FC = ({ } return [...result].sort((a, b) => { - if (sortBy === "name") return a.name.localeCompare(b.name); - if (sortBy === "lastUsedAt") { - const aTime = a.lastUsedAt - ? new Date(a.lastUsedAt).getTime() - : 0; - const bTime = b.lastUsedAt - ? new Date(b.lastUsedAt).getTime() - : 0; - return bTime - aTime; - } - return ( - new Date(b.createdAt).getTime() - - new Date(a.createdAt).getTime() - ); + const aTime = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0; + const bTime = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0; + return bTime - aTime; }); - }, [connections, search, showFavoritesOnly, sortBy]); + }, [connections, search, showFavoritesOnly]); const favorites = useMemo( () => filtered.filter((c) => c.isFavorite), @@ -91,13 +77,15 @@ export const ConnectionList: React.FC = ({ ) => { if (items.length === 0) return null; return ( -
-

- {icon} - {title} - {items.length} -

-
+
+
+ {icon} + + {title} + + {items.length} +
+
{items.map((conn) => ( = ({ return (
- {/* Search & Filters */} -
-
- - +
+ + setSearch(e.target.value)} + className="w-full h-8 bg-secondary/50 text-sm text-foreground placeholder:text-muted-foreground/40 border border-transparent focus:border-primary/20 focus:outline-none rounded-lg pl-9 pr-8 transition-colors" /> {search && ( - } - label="Clear search" + )}
- - -
+
+ {/* Connection list */} -
+
{filtered.length === 0 ? (
@@ -178,7 +157,7 @@ export const ConnectionList: React.FC = ({

) : showFavoritesOnly ? ( -
+
{filtered.map((conn) => ( = ({ recent, , )} - {renderSection( - "All Connections", - rest, - , - )} + {renderSection("All Connections", rest, null)} )}
diff --git a/apps/compooss/src/lib/components/connection/connection-page.component.tsx b/apps/compooss/src/lib/components/connection/connection-page.component.tsx index 56f83e1..f0424da 100644 --- a/apps/compooss/src/lib/components/connection/connection-page.component.tsx +++ b/apps/compooss/src/lib/components/connection/connection-page.component.tsx @@ -6,7 +6,7 @@ import { apiClient } from "@/lib/config/api.config"; import { ENDPOINTS } from "@/lib/constants"; import type { SavedConnection } from "@compooss/types"; import { ThemeSwitcher } from "@/lib/components/common/theme-switcher.component"; -import { Leaf } from "lucide-react"; +import { Database, Leaf } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; @@ -215,18 +215,21 @@ export const ConnectionPage: React.FC = () => { {/* Right sidebar: Saved connections */} {hasConnections && ( -
-
-
-

+
+
+
+
+ +
+

Saved Connections

- - {connections.length} -
+ + {connections.length} +
-
+