211 lines
6.3 KiB
TypeScript
211 lines
6.3 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { toast } from "sonner";
|
||
import { api, type Connection } from "@/components/api";
|
||
import { keyValueLinesFromJson, parseKeyValueLines } from "@/components/kv";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
|
||
interface ConnectionDialogProps {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
/** 传入则为编辑,否则为新建 */
|
||
initial?: Connection | null;
|
||
onSaved: () => void;
|
||
}
|
||
|
||
const EMPTY = {
|
||
name: "",
|
||
type: "mysql",
|
||
host: "127.0.0.1",
|
||
port: "3306",
|
||
user: "root",
|
||
password: "",
|
||
database: "",
|
||
};
|
||
|
||
export function ConnectionDialog({
|
||
open,
|
||
onOpenChange,
|
||
initial,
|
||
onSaved,
|
||
}: ConnectionDialogProps) {
|
||
const [form, setForm] = useState(() =>
|
||
initial
|
||
? {
|
||
name: initial.name,
|
||
type: initial.type,
|
||
host: initial.host,
|
||
port: String(initial.port),
|
||
user: initial.user,
|
||
password: initial.password,
|
||
database: initial.database,
|
||
}
|
||
: EMPTY
|
||
);
|
||
const [saving, setSaving] = useState(false);
|
||
const [paramsText, setParamsText] = useState(() =>
|
||
initial ? keyValueLinesFromJson(initial.params) : ""
|
||
);
|
||
|
||
const set = (k: keyof typeof EMPTY) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
||
setForm((f) => ({ ...f, [k]: e.target.value }));
|
||
|
||
async function submit() {
|
||
if (!form.name.trim() || !form.host.trim() || !form.database.trim()) {
|
||
toast.error("请填写名称、host 和 database");
|
||
return;
|
||
}
|
||
const port = Number(form.port) || 3306;
|
||
setSaving(true);
|
||
try {
|
||
const body = JSON.stringify({
|
||
name: form.name.trim(),
|
||
type: form.type,
|
||
host: form.host.trim(),
|
||
port,
|
||
user: form.user.trim(),
|
||
password: form.password,
|
||
database: form.database.trim(),
|
||
params: parseKeyValueLines(paramsText),
|
||
});
|
||
if (initial) {
|
||
await api(`/api/connections/${initial.id}`, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body,
|
||
});
|
||
toast.success("连接已更新");
|
||
} else {
|
||
await api("/api/connections", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body,
|
||
});
|
||
toast.success("连接已创建");
|
||
}
|
||
onOpenChange(false);
|
||
onSaved();
|
||
} catch (e) {
|
||
toast.error(e instanceof Error ? e.message : "保存失败");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>{initial ? "编辑连接" : "新建连接"}</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="grid gap-3">
|
||
<div className="grid gap-1.5">
|
||
<Label htmlFor="conn-name">名称</Label>
|
||
<Input
|
||
id="conn-name"
|
||
value={form.name}
|
||
onChange={set("name")}
|
||
placeholder="例如:测试环境 MySQL"
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="grid gap-1.5">
|
||
<Label>类型</Label>
|
||
<Select
|
||
value={form.type}
|
||
onValueChange={(v) => setForm((f) => ({ ...f, type: String(v) }))}
|
||
>
|
||
<SelectTrigger className="w-full">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="mysql">mysql</SelectItem>
|
||
<SelectItem value="mariadb">mariadb</SelectItem>
|
||
<SelectItem value="tidb">tidb</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="grid gap-1.5">
|
||
<Label htmlFor="conn-port">端口</Label>
|
||
<Input
|
||
id="conn-port"
|
||
type="number"
|
||
value={form.port}
|
||
onChange={set("port")}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="grid gap-1.5">
|
||
<Label htmlFor="conn-host">Host</Label>
|
||
<Input id="conn-host" value={form.host} onChange={set("host")} />
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="grid gap-1.5">
|
||
<Label htmlFor="conn-user">用户名</Label>
|
||
<Input id="conn-user" value={form.user} onChange={set("user")} />
|
||
</div>
|
||
<div className="grid gap-1.5">
|
||
<Label htmlFor="conn-password">密码</Label>
|
||
<Input
|
||
id="conn-password"
|
||
type="password"
|
||
value={form.password}
|
||
onChange={set("password")}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="grid gap-1.5">
|
||
<Label htmlFor="conn-db">数据库</Label>
|
||
<Input
|
||
id="conn-db"
|
||
value={form.database}
|
||
onChange={set("database")}
|
||
/>
|
||
</div>
|
||
<div className="grid gap-1.5">
|
||
<Label htmlFor="conn-params">连接参数</Label>
|
||
<Textarea
|
||
id="conn-params"
|
||
className="font-mono text-xs"
|
||
rows={3}
|
||
value={paramsText}
|
||
onChange={(e) => setParamsText(e.target.value)}
|
||
placeholder={"tidb_skip_isolation_level_check=1"}
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
每行一个 key=value,作为 URL 查询参数传递,sling / atlas 均生效;TiDB
|
||
目标库建议设置 tidb_skip_isolation_level_check=1。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={submit} disabled={saving}>
|
||
{saving ? "保存中…" : "保存"}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|