import { useMemo, useState, type ReactNode } from "react"; import { driverById } from "./catalog"; import { DriverIcon } from "./DriverIcon"; import { useWorkspaceStore, type SavedConnection } from "./store"; function hostSummary(c: SavedConnection) { const d = driverById(c.driver); if (c.driver === "sqlite") { return c.database || "本地文件"; } const host = c.host || "127.0.0.1"; const port = c.port ? `:${c.port}` : ""; const db = c.database ? ` / ${c.database}` : ""; return `${host}${port}${db}`; } function matchesQuery(c: SavedConnection, needle: string): boolean { if (!needle) return true; const d = driverById(c.driver); const hay = [c.name, c.host || "", c.driver, d?.name ?? "", c.database || "", String(c.port || "")] .join(" ") .toLowerCase(); return hay.includes(needle); } function ConnRow({ c, onOpen, onDelete, setDefaultConnection, editConnection, notify, }: { c: SavedConnection; onOpen: (c: SavedConnection) => void; onDelete: (c: SavedConnection) => void; setDefaultConnection: (id: string) => void; editConnection: (c: SavedConnection) => void; notify: ReturnType["notify"]; }) { const d = driverById(c.driver); return (
{!c.isDefault && ( )}
); } function SectionLabel({ children }: { children: ReactNode }) { return (
{children}
); } const RECENT_N = 5; export function ConnectionList() { const connections = useWorkspaceStore((s) => s.connections); const openPicker = useWorkspaceStore((s) => s.openPicker); const editConnection = useWorkspaceStore((s) => s.editConnection); const removeConnection = useWorkspaceStore((s) => s.removeConnection); const setDefaultConnection = useWorkspaceStore((s) => s.setDefaultConnection); const askConfirm = useWorkspaceStore((s) => s.askConfirm); const set = useWorkspaceStore((s) => s.set); const notify = useWorkspaceStore((s) => s.notify); const [search, setSearch] = useState(""); const needle = search.trim().toLowerCase(); const filtered = useMemo( () => [...connections] .filter((c) => matchesQuery(c, needle)) .sort((a, b) => { if (a.isDefault && !b.isDefault) return -1; if (!a.isDefault && b.isDefault) return 1; return (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0); }), [connections, needle], ); const recent = useMemo(() => { const ids = new Set(filtered.map((c) => c.id)); return [...connections] .filter((c) => (c.lastUsedAt ?? 0) > 0 && ids.has(c.id)) .sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0)) .slice(0, RECENT_N); }, [connections, filtered]); /** 「全部」list: filtered, with recent still shown (pinned section above). */ const allList = filtered; const onDelete = async (c: SavedConnection) => { const ok = await askConfirm({ title: "删除连接", message: `确认删除「${c.name}」?此操作不可撤销。`, okLabel: "删除", danger: true, }); if (!ok) return; removeConnection(c.id); notify({ kind: "success", title: "已删除", message: `「${c.name}」已移除。`, mode: "toast" }); }; const onOpen = (c: SavedConnection) => { set("activeConnectionId", c.id); set("view", "workbench"); set("status", `已选择 ${c.name}`); }; const rowProps = { onOpen, onDelete, setDefaultConnection, editConnection, notify }; return (
连接
选择或新建数据库连接
{connections.length > 0 && ( )}
{connections.length > 0 && (
setSearch(e.target.value)} aria-label="搜索连接" /> {search && ( )}
)} {connections.length === 0 ? (

还没有连接

新建一个连接,保存后会出现在这里。

) : filtered.length === 0 ? (

没有匹配的连接

换个关键词试试,或清除搜索查看全部。

) : (
{recent.length > 0 && ( <> 最近 {recent.map((c) => ( ))} )} 全部{needle ? ` · ${allList.length}` : ""} {allList.map((c) => ( ))}
)}
); }