import { useState, useEffect, useMemo } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { ChevronDown, RefreshCw, Save, ArrowLeft, AlertCircle, KeyRound } from 'lucide-react';
import { connectivityAPI } from '../../services/api';
import {
Toast, useToasts, toastEvent, apiError, typeMeta,
GROUP_TYPES, TypeIcon,
} from './shared';
const inputCls =
'w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500';
const monoCls =
'w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-xs text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 resize-y';
function FieldRow({ label, required, hint, children, error }) {
return (
{label}
{required && * }
{children}
{error &&
{error}
}
{hint && !error &&
{hint}
}
);
}
// A write-only secret control: on a new connection it's a plain editable field;
// on edit it shows "•••• set" with a Replace button that reveals the input.
function SecretField({ label, required, hint, isEdit, hasExisting, value, onChange, multiline, placeholder }) {
const [replacing, setReplacing] = useState(!isEdit || !hasExisting);
if (isEdit && hasExisting && !replacing) {
return (
•••• set
{ setReplacing(true); }}
className="text-sm font-medium text-primary-600 hover:text-primary-700"
>
Replace
);
}
return (
{multiline ? (
);
}
const EMPTY = {
name: '',
// sshuttle
host: '', port: '', user: '', auth: 'key', private_key: '', password: '', known_hosts: '',
// proxy
scheme: 'socks5', proxy_user: '', proxy_password: '',
// wireguard / openvpn
conf: '',
};
export default function ConnectionForm() {
const toasts = useToasts();
const navigate = useNavigate();
const { group, id } = useParams();
const isEdit = id && id !== 'new';
const groupTypes = GROUP_TYPES[group] || [];
const [type, setType] = useState(groupTypes[0] || 'wireguard_ext');
const [form, setForm] = useState(EMPTY);
const [existing, setExisting] = useState(null); // the loaded connection on edit
const [loading, setLoading] = useState(isEdit);
const [saving, setSaving] = useState(false);
const [loadError, setLoadError] = useState(null);
const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
useEffect(() => {
if (!isEdit) return;
let cancelled = false;
(async () => {
try {
const res = await connectivityAPI.listConnections();
const conns = res.data?.connections ?? res.data ?? [];
const conn = (Array.isArray(conns) ? conns : []).find((c) => c.id === id);
if (cancelled) return;
if (!conn) { setLoadError('Connection not found'); setLoading(false); return; }
setExisting(conn);
setType(conn.type);
const cfg = conn.config || {};
setForm({
...EMPTY,
name: conn.name || '',
host: cfg.host || '',
port: cfg.port != null ? String(cfg.port) : '',
user: cfg.user || '',
auth: cfg.auth || 'key',
scheme: cfg.scheme || 'socks5',
proxy_user: cfg.user || '',
});
setLoading(false);
} catch (err) {
if (!cancelled) { setLoadError(apiError(err, 'Failed to load connection')); setLoading(false); }
}
})();
return () => { cancelled = true; };
}, [isEdit, id]);
// Secrets that already exist on the connection (so we can show "•••• set").
const hasSecret = useMemo(() => {
const cfg = existing?.config || {};
return {
conf: !!existing && (type === 'wireguard_ext' || type === 'openvpn'),
private_key: !!existing && type === 'sshuttle' && (cfg.auth || 'key') === 'key',
password: !!existing && ((type === 'sshuttle' && cfg.auth === 'password') || type === 'proxy'),
known_hosts: !!existing && type === 'sshuttle',
};
}, [existing, type]);
const buildPayload = () => {
const config = {};
const secrets = {};
if (type === 'wireguard_ext' || type === 'openvpn') {
if (form.conf.trim()) secrets.conf = form.conf;
} else if (type === 'sshuttle') {
config.host = form.host.trim();
config.port = Number(form.port || 22);
config.user = form.user.trim();
config.auth = form.auth;
if (form.known_hosts.trim()) secrets.known_hosts = form.known_hosts.trim();
if (form.auth === 'key') {
if (form.private_key.trim()) secrets.private_key = form.private_key;
} else if (form.password) {
secrets.password = form.password;
}
} else if (type === 'proxy') {
config.scheme = form.scheme;
config.host = form.host.trim();
config.port = Number(form.port);
config.user = form.proxy_user.trim();
if (form.proxy_password) secrets.password = form.proxy_password;
}
return { config, secrets };
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!form.name.trim()) { toastEvent('Name is required', 'error'); return; }
setSaving(true);
const { config, secrets } = buildPayload();
try {
if (isEdit) {
const fields = { name: form.name.trim(), config };
if (Object.keys(secrets).length) fields.secrets = secrets;
await connectivityAPI.updateConnection(id, fields);
toastEvent(`Saved ${form.name.trim()}`);
} else {
await connectivityAPI.createConnection(type, form.name.trim(), config, secrets);
toastEvent(`Created ${form.name.trim()}`);
}
navigate(`/connectivity/${group}`);
} catch (err) {
toastEvent(apiError(err, 'Failed to save connection'), 'error');
} finally {
setSaving(false);
}
};
if (loading) {
return (
);
}
if (loadError) {
return (
);
}
const meta = typeMeta(type);
return (
Back
{isEdit ? `Edit ${meta.short}` : `Add ${meta.short}`}
);
}