Files
HellreigN/frontend/src/modules/agent/ui/SSHAgentForm.tsx
T
nikitaa_ts 57f12f792c
ci-front / build (push) Successful in 2m8s
feat: add create agents
2026-04-04 01:24:45 +03:00

497 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import {
FiServer,
FiGlobe,
FiKey,
FiLock,
FiPlus,
FiTrash2,
FiSettings,
} from "react-icons/fi";
import { SiDocker } from "react-icons/si";
import { FiPackage, FiUploadCloud } from "react-icons/fi";
type DeployType = "docker" | "binary" | "deploy";
type AuthMethod = "key" | "password";
interface ExtraField {
key: string;
value: string;
}
export interface SSHAgentConfig {
user: string;
ip: string;
authMethod: AuthMethod;
sshKey?: string;
password?: string;
extraFields: ExtraField[];
deployType: DeployType;
}
interface SSHAgentFormProps {
index: number;
config: SSHAgentConfig;
onChange: (index: number, config: SSHAgentConfig) => void;
onRemove: (index: number) => void;
canRemove: boolean;
}
const DEPLOY_OPTIONS: {
value: DeployType;
label: string;
icon: React.ReactNode;
}[] = [
{ value: "docker", label: "Docker", icon: <SiDocker /> },
{ value: "binary", label: "Binary", icon: <FiPackage /> },
];
const inputBaseStyle: React.CSSProperties = {
width: "100%",
padding: "10px 12px",
border: "1px solid var(--border)",
borderRadius: "8px",
backgroundColor: "var(--input-bg)",
color: "var(--text-primary)",
fontSize: "14px",
transition: "border-color 0.2s, box-shadow 0.2s",
};
const labelStyle: React.CSSProperties = {
display: "block",
marginBottom: "8px",
color: "var(--text-secondary)",
fontSize: "14px",
fontWeight: 500,
};
export const SSHAgentForm: React.FC<SSHAgentFormProps> = ({
index,
config,
onChange,
onRemove,
canRemove,
}) => {
const handleChange = (field: keyof SSHAgentConfig, value: unknown) => {
onChange(index, { ...config, [field]: value });
};
const handleExtraFieldChange = (
fieldIndex: number,
field: keyof ExtraField,
value: string,
) => {
const newExtraFields = [...config.extraFields];
newExtraFields[fieldIndex] = {
...newExtraFields[fieldIndex],
[field]: value,
};
handleChange("extraFields", newExtraFields);
};
const addExtraField = () => {
handleChange("extraFields", [
...config.extraFields,
{ key: "", value: "" },
]);
};
const removeExtraField = (fieldIndex: number) => {
const newExtraFields = config.extraFields.filter(
(_, i) => i !== fieldIndex,
);
handleChange("extraFields", newExtraFields);
};
const handleFocus = (
e: React.FocusEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) => {
e.currentTarget.style.borderColor = "var(--border-focus)";
e.currentTarget.style.boxShadow = `0 0 0 3px var(--border-focus)30`;
};
const handleBlur = (
e: React.FocusEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) => {
e.currentTarget.style.borderColor = "var(--border)";
e.currentTarget.style.boxShadow = "none";
};
return (
<div
className="rounded-2xl shadow-lg border"
style={{
backgroundColor: "var(--card-bg)",
borderColor: "var(--border)",
padding: "24px",
marginBottom: "20px",
}}
>
{/* Header */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "24px",
paddingBottom: "16px",
borderBottom: "1px solid var(--border)",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<div
className="w-10 h-10 rounded-lg flex items-center justify-center"
style={{ backgroundColor: "var(--bg-secondary)" }}
>
<FiServer style={{ color: "var(--accent)", fontSize: "20px" }} />
</div>
<h3
style={{
margin: 0,
color: "var(--text-primary)",
fontSize: "18px",
fontWeight: 600,
}}
>
SSH сервер #{index + 1}
</h3>
</div>
{canRemove && (
<button
type="button"
onClick={() => onRemove(index)}
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-all"
style={{
background: "var(--error-bg)",
color: "var(--error-text)",
border: "1px solid var(--error-border)",
cursor: "pointer",
fontSize: "14px",
fontWeight: 500,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "var(--error-text)";
e.currentTarget.style.color = "#fff";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "var(--error-bg)";
e.currentTarget.style.color = "var(--error-text)";
}}
>
<FiTrash2 size={14} />
Удалить
</button>
)}
</div>
<div style={{ display: "grid", gap: "20px" }}>
{/* User и IP */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "16px",
}}
>
<div>
<label style={labelStyle}>
<span
style={{ display: "flex", alignItems: "center", gap: "6px" }}
>
<FiServer size={14} />
Пользователь *
</span>
</label>
<input
type="text"
value={config.user}
onChange={(e) => handleChange("user", e.target.value)}
required
style={inputBaseStyle}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="username"
/>
</div>
<div>
<label style={labelStyle}>
<span
style={{ display: "flex", alignItems: "center", gap: "6px" }}
>
<FiGlobe size={14} />
IP адрес *
</span>
</label>
<input
type="text"
value={config.ip}
onChange={(e) => handleChange("ip", e.target.value)}
required
style={inputBaseStyle}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="192.168.1.1"
/>
</div>
</div>
{/* Метод аутентификации */}
<div>
<label style={labelStyle}>
<span style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<FiKey size={14} />
Метод аутентификации *
</span>
</label>
<div style={{ display: "flex", gap: "8px" }}>
{(["key", "password"] as const).map((method) => (
<button
key={method}
type="button"
onClick={() => handleChange("authMethod", method)}
className="flex-1 py-2.5 px-4 rounded-lg border transition-all font-medium"
style={{
backgroundColor:
config.authMethod === method
? "var(--accent)"
: "var(--input-bg)",
color:
config.authMethod === method
? "var(--accent-text)"
: "var(--text-primary)",
borderColor:
config.authMethod === method
? "var(--accent)"
: "var(--border)",
cursor: "pointer",
fontSize: "14px",
}}
>
{method === "key" ? "SSH ключ" : "Пароль"}
</button>
))}
</div>
</div>
{/* SSH Key или Password */}
{config.authMethod === "key" ? (
<div>
<label style={labelStyle}>
<span
style={{ display: "flex", alignItems: "center", gap: "6px" }}
>
<FiKey size={14} />
SSH ключ *
</span>
</label>
<textarea
value={config.sshKey || ""}
onChange={(e) => handleChange("sshKey", e.target.value)}
required
rows={4}
style={{
...inputBaseStyle,
fontFamily: "ui-monospace, SFMono-Regular, monospace",
resize: "vertical",
}}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
/>
</div>
) : (
<div>
<label style={labelStyle}>
<span
style={{ display: "flex", alignItems: "center", gap: "6px" }}
>
<FiLock size={14} />
Пароль *
</span>
</label>
<input
type="password"
value={config.password || ""}
onChange={(e) => handleChange("password", e.target.value)}
required
style={inputBaseStyle}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="••••••••"
/>
</div>
)}
{/* Дополнительные поля */}
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "12px",
}}
>
<label style={{ ...labelStyle, marginBottom: 0 }}>
<span
style={{ display: "flex", alignItems: "center", gap: "6px" }}
>
<FiSettings size={14} />
Дополнительные параметры
</span>
</label>
<button
type="button"
onClick={addExtraField}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all"
style={{
background: "var(--accent)",
color: "var(--accent-text)",
border: "none",
cursor: "pointer",
fontSize: "13px",
fontWeight: 500,
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = "0.85";
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = "1";
}}
>
<FiPlus size={14} />
Добавить
</button>
</div>
{config.extraFields.length === 0 && (
<div
className="text-center py-6 rounded-lg border border-dashed"
style={{
color: "var(--text-muted)",
borderColor: "var(--border)",
}}
>
<FiSettings
size={20}
style={{ margin: "0 auto 8px", opacity: 0.5 }}
/>
<p style={{ margin: 0, fontSize: "13px" }}>
Нет дополнительных параметров
</p>
</div>
)}
{config.extraFields.map((extra, fieldIndex) => (
<div
key={fieldIndex}
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr auto",
gap: "8px",
marginBottom: "8px",
}}
>
<input
type="text"
value={extra.key}
onChange={(e) =>
handleExtraFieldChange(fieldIndex, "key", e.target.value)
}
style={inputBaseStyle}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="Параметр"
/>
<input
type="text"
value={extra.value}
onChange={(e) =>
handleExtraFieldChange(fieldIndex, "value", e.target.value)
}
style={inputBaseStyle}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="Значение"
/>
<button
type="button"
onClick={() => removeExtraField(fieldIndex)}
className="flex items-center justify-center rounded-lg border transition-all"
style={{
background: "var(--error-bg)",
color: "var(--error-text)",
borderColor: "var(--error-border)",
cursor: "pointer",
fontSize: "18px",
padding: "8px 12px",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "var(--error-text)";
e.currentTarget.style.color = "#fff";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "var(--error-bg)";
e.currentTarget.style.color = "var(--error-text)";
}}
>
×
</button>
</div>
))}
</div>
{/* Тип развертывания */}
<div>
<label style={labelStyle}>
<span style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<FiUploadCloud size={16} />
Тип развертывания *
</span>
</label>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: "8px",
}}
>
{DEPLOY_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
onClick={() => handleChange("deployType", option.value)}
className="flex items-center justify-center gap-2 py-3 px-4 rounded-lg border transition-all font-medium"
style={{
backgroundColor:
config.deployType === option.value
? "var(--accent)"
: "var(--input-bg)",
color:
config.deployType === option.value
? "var(--accent-text)"
: "var(--text-primary)",
borderColor:
config.deployType === option.value
? "var(--accent)"
: "var(--border)",
cursor: "pointer",
fontSize: "14px",
}}
>
<span style={{ fontSize: "18px" }}>{option.icon}</span>
{option.label}
</button>
))}
</div>
</div>
</div>
</div>
);
};