feat: add connection management UI

Add TypeScript types, typed Tauri invoke wrappers, Zustand store,
TanStack Query hooks, and connection components: ConnectionDialog
(create/edit/test), ConnectionList (sheet panel), ConnectionSelector
(toolbar dropdown).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 19:06:35 +03:00
parent 9b675babd5
commit 734b84b525
10 changed files with 878 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useSaveConnection, useTestConnection } from "@/hooks/use-connections";
import { toast } from "sonner";
import type { ConnectionConfig } from "@/types";
import { Loader2 } from "lucide-react";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
connection?: ConnectionConfig | null;
}
const emptyConfig: ConnectionConfig = {
id: "",
name: "",
host: "localhost",
port: 5432,
user: "postgres",
password: "",
database: "postgres",
ssl_mode: "prefer",
color: undefined,
};
export function ConnectionDialog({ open, onOpenChange, connection }: Props) {
const [form, setForm] = useState<ConnectionConfig>(emptyConfig);
const saveMutation = useSaveConnection();
const testMutation = useTestConnection();
useEffect(() => {
if (open) {
setForm(
connection ?? { ...emptyConfig, id: crypto.randomUUID() }
);
}
}, [open, connection]);
const update = (field: keyof ConnectionConfig, value: string | number) => {
setForm((f) => ({ ...f, [field]: value }));
};
const handleTest = () => {
testMutation.mutate(form, {
onSuccess: (version) => {
toast.success("Connection successful", { description: version });
},
onError: (err) => {
toast.error("Connection failed", { description: String(err) });
},
});
};
const handleSave = () => {
if (!form.name.trim()) {
toast.error("Name is required");
return;
}
saveMutation.mutate(form, {
onSuccess: () => {
toast.success("Connection saved");
onOpenChange(false);
},
onError: (err) => {
toast.error("Save failed", { description: String(err) });
},
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[480px]">
<DialogHeader>
<DialogTitle>
{connection ? "Edit Connection" : "New Connection"}
</DialogTitle>
</DialogHeader>
<div className="grid gap-3 py-4">
<div className="grid grid-cols-4 items-center gap-3">
<label className="text-right text-sm text-muted-foreground">
Name
</label>
<Input
className="col-span-3"
value={form.name}
onChange={(e) => update("name", e.target.value)}
placeholder="My Database"
/>
</div>
<div className="grid grid-cols-4 items-center gap-3">
<label className="text-right text-sm text-muted-foreground">
Host
</label>
<Input
className="col-span-3"
value={form.host}
onChange={(e) => update("host", e.target.value)}
/>
</div>
<div className="grid grid-cols-4 items-center gap-3">
<label className="text-right text-sm text-muted-foreground">
Port
</label>
<Input
className="col-span-3"
type="number"
value={form.port}
onChange={(e) => update("port", parseInt(e.target.value) || 5432)}
/>
</div>
<div className="grid grid-cols-4 items-center gap-3">
<label className="text-right text-sm text-muted-foreground">
User
</label>
<Input
className="col-span-3"
value={form.user}
onChange={(e) => update("user", e.target.value)}
/>
</div>
<div className="grid grid-cols-4 items-center gap-3">
<label className="text-right text-sm text-muted-foreground">
Password
</label>
<Input
className="col-span-3"
type="password"
value={form.password}
onChange={(e) => update("password", e.target.value)}
/>
</div>
<div className="grid grid-cols-4 items-center gap-3">
<label className="text-right text-sm text-muted-foreground">
Database
</label>
<Input
className="col-span-3"
value={form.database}
onChange={(e) => update("database", e.target.value)}
/>
</div>
<div className="grid grid-cols-4 items-center gap-3">
<label className="text-right text-sm text-muted-foreground">
SSL Mode
</label>
<Select
value={form.ssl_mode ?? "prefer"}
onValueChange={(v) => update("ssl_mode", v)}
>
<SelectTrigger className="col-span-3">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="disable">Disable</SelectItem>
<SelectItem value="prefer">Prefer</SelectItem>
<SelectItem value="require">Require</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={handleTest}
disabled={testMutation.isPending}
>
{testMutation.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Test
</Button>
<Button onClick={handleSave} disabled={saveMutation.isPending}>
{saveMutation.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}