import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
getProjectServerLinks, getServers, linkServer, unlinkServer,
type ProjectServerLink, type QuickLink,
} from "../../lib/commands";
function CopyBtn({ value }: { value: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
);
}
function buildSshCmd(link: ProjectServerLink): string {
const port = link.server_port && link.server_port !== "22" ? ` -p ${link.server_port}` : "";
const key = link.ssh_key_path ? ` -i ${link.ssh_key_path}` : "";
const user = link.username ? `${link.username}@` : "";
return `ssh ${user}${link.server_host}${port}${key}`;
}
function buildDeployKeyCmd(link: ProjectServerLink): string {
const keyPub = link.ssh_key_path
? link.ssh_key_path.replace(/(?:\.pub)?$/, ".pub")
: "%USERPROFILE%\\.ssh\\id_ed25519.pub";
const portFlag = link.server_port && link.server_port !== "22" ? ` -p ${link.server_port}` : "";
const user = link.username ? `${link.username}@` : "";
return `type ${keyPub} | ssh${portFlag} ${user}${link.server_host} "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"`;
}
// ── 已关联服务器条目 ────────────────────────────────────────────────────────
function LinkedServerItem({ link, projectId }: { link: ProjectServerLink; projectId: string }) {
const qc = useQueryClient();
const [showPwd, setShowPwd] = useState(false);
const unlinkMut = useMutation({
mutationFn: () => unlinkServer(projectId, link.server_id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["project-servers", projectId] });
qc.invalidateQueries({ queryKey: ["server-projects", link.server_id] });
},
});
const sshCmd = buildSshCmd(link);
const links: QuickLink[] = (() => {
try { return link.quick_links ? JSON.parse(link.quick_links) : []; } catch { return []; }
})();
return (
{/* 标题行 */}
{link.server_name}
{link.server_host}
{link.role && (
{link.role}
)}
{/* 信息行 */}
{link.deploy_type && (
{link.deploy_type === "docker" ? "Docker" : "裸机"}
)}
{link.username && (
用户:
{link.username}
)}
{link.password && (
密码:
{showPwd ? link.password : "••••"}
)}
{link.deploy_path && (
{link.deploy_type === "docker" ? "Compose:" : "路径:"}
{link.deploy_path}
)}
{link.container_name && (
容器:
{link.container_name}
)}
{link.start_command && (
启动:
{link.start_command}
)}
{/* SSH 命令 */}
$ {sshCmd}
{/* 部署公钥命令 */}
▶
部署公钥(CMD)
> {buildDeployKeyCmd(link)}
{/* 快速链接 */}
{links.length > 0 && (
{links.map((l, i) => (
))}
)}
);
}
// ── 关联选择器 ──────────────────────────────────────────────────────────────
function LinkSelector({ projectId, linkedIds }: { projectId: string; linkedIds: Set }) {
const qc = useQueryClient();
const [selectedServerId, setSelectedServerId] = useState("");
const [role, setRole] = useState("production");
const [deployType, setDeployType] = useState<"bare" | "docker">("bare");
const [deployPath, setDeployPath] = useState("");
const [containerName, setContainerName] = useState("");
const [startCommand, setStartCommand] = useState("");
const { data: allServers = [] } = useQuery({
queryKey: ["servers"],
queryFn: getServers,
staleTime: 30000,
});
const available = allServers.filter((s) => !linkedIds.has(s.id));
const linkMut = useMutation({
mutationFn: () => linkServer(projectId, selectedServerId, {
role: role || undefined,
deploy_type: deployType,
deploy_path: deployPath || undefined,
container_name: containerName || undefined,
start_command: startCommand || undefined,
}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["project-servers", projectId] });
qc.invalidateQueries({ queryKey: ["server-projects", selectedServerId] });
setSelectedServerId("");
setRole("production");
setDeployType("bare");
setDeployPath("");
setContainerName("");
setStartCommand("");
},
});
if (available.length === 0) {
return (
{allServers.length === 0 ? "请先在「服务器」Tab 中添加服务器" : "所有服务器已关联"}
);
}
const inputCls = "text-xs px-2.5 py-2 rounded-md border border-gray-200 bg-white focus:outline-none focus:border-blue-300 focus:ring-1 focus:ring-blue-100";
return (
{/* 第一行:服务器 + 角色 + 部署方式 */}
{/* 第二行:根据部署类型显示不同字段 */}
{/* Docker 额外行:启动命令 */}
{deployType === "docker" && (
setStartCommand(e.target.value)}
placeholder="docker compose up -d" className={inputCls} />
)}
);
}
// ── 主面板(嵌入 ProjectCard) ──────────────────────────────────────────────
export function ProjectServerPanel({ projectId }: { projectId: string }) {
const [showLink, setShowLink] = useState(false);
const { data: links = [] } = useQuery({
queryKey: ["project-servers", projectId],
queryFn: () => getProjectServerLinks(projectId),
});
const linkedIds = new Set(links.map((l) => l.server_id));
return (
关联服务器
{showLink &&
}
{links.length === 0 && !showLink && (
暂未关联服务器
)}
{links.map((link) => (
))}
);
}