import { useEffect, useState } from 'react'; import { Eye, EyeOff } from 'lucide-react'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { Button, FormField, Input, Select, SectionHeader, Toggle, useToast } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; import { useOutboundConnection, useCreateOutboundConnection, useUpdateOutboundConnection, useTestOutboundConnection, type OutboundConnectionDto, type OutboundConnectionRequest, type OutboundConnectionTestResult, type OutboundMethod, type OutboundAuthKind, type TrustMode, } from '../../api/queries/admin/outboundConnections'; import { useEnvironments } from '../../api/queries/admin/environments'; import { describeApiError } from '../../api/errors'; import sectionStyles from '../../styles/section-card.module.css'; // ── Form state ────────────────────────────────────────────────────────── interface FormState { name: string; description: string; url: string; method: OutboundMethod; headers: Array<{ key: string; value: string }>; defaultBodyTmpl: string; tlsTrustMode: TrustMode; tlsCaPemPaths: string[]; hmacSecret: string; // empty = keep existing (edit) / no secret (create) authKind: OutboundAuthKind; bearerToken: string; basicUsername: string; basicPassword: string; allowAllEnvs: boolean; allowedEnvIds: string[]; } function initialForm(existing?: OutboundConnectionDto): FormState { return { name: existing?.name ?? '', description: existing?.description ?? '', url: existing?.url ?? 'https://', method: existing?.method ?? 'POST', headers: existing ? Object.entries(existing.defaultHeaders).map(([key, value]) => ({ key, value })) : [], defaultBodyTmpl: existing?.defaultBodyTmpl ?? '', tlsTrustMode: existing?.tlsTrustMode ?? 'SYSTEM_DEFAULT', tlsCaPemPaths: existing?.tlsCaPemPaths ?? [], hmacSecret: '', authKind: existing?.authKind ?? 'NONE', bearerToken: '', basicUsername: '', basicPassword: '', allowAllEnvs: !existing || existing.allowedEnvironmentIds.length === 0, allowedEnvIds: existing?.allowedEnvironmentIds ?? [], }; } function toRequest(f: FormState): OutboundConnectionRequest { const defaultHeaders = Object.fromEntries( f.headers.filter((h) => h.key.trim()).map((h) => [h.key.trim(), h.value]), ); const allowedEnvironmentIds = f.allowAllEnvs ? [] : f.allowedEnvIds; const auth = f.authKind === 'NONE' ? {} : f.authKind === 'BEARER' ? { tokenCiphertext: f.bearerToken } : { username: f.basicUsername, passwordCiphertext: f.basicPassword }; return { name: f.name, description: f.description || null, url: f.url, method: f.method, defaultHeaders, defaultBodyTmpl: f.defaultBodyTmpl || null, tlsTrustMode: f.tlsTrustMode, tlsCaPemPaths: f.tlsCaPemPaths, hmacSecret: f.hmacSecret ? f.hmacSecret : null, auth, allowedEnvironmentIds, }; } // ── Select option arrays ─────────────────────────────────────────────── const METHOD_OPTIONS: Array<{ value: OutboundMethod; label: string }> = [ { value: 'POST', label: 'POST' }, { value: 'PUT', label: 'PUT' }, { value: 'PATCH', label: 'PATCH' }, ]; const TRUST_OPTIONS: Array<{ value: TrustMode; label: string }> = [ { value: 'SYSTEM_DEFAULT', label: 'System default (recommended)' }, { value: 'TRUST_PATHS', label: 'Trust additional CA PEMs' }, { value: 'TRUST_ALL', label: 'Trust all (INSECURE)' }, ]; const AUTH_OPTIONS: Array<{ value: OutboundAuthKind; label: string }> = [ { value: 'NONE', label: 'None' }, { value: 'BEARER', label: 'Bearer token' }, { value: 'BASIC', label: 'Basic' }, ]; // ── Component ────────────────────────────────────────────────────────── export default function OutboundConnectionEditor() { const { id } = useParams<{ id: string }>(); const isNew = !id; const navigate = useNavigate(); const { toast } = useToast(); const existingQ = useOutboundConnection(isNew ? undefined : id); const envQ = useEnvironments(); const createMut = useCreateOutboundConnection(); // Hooks must be called unconditionally; pass placeholder id when unknown. const updateMut = useUpdateOutboundConnection(id ?? 'placeholder'); const testMut = useTestOutboundConnection(); const [form, setForm] = useState(() => initialForm()); const [initialized, setInitialized] = useState(isNew); const [testResult, setTestResult] = useState(null); const [showSecret, setShowSecret] = useState(false); useEffect(() => { if (!initialized && existingQ.data) { setForm(initialForm(existingQ.data)); setInitialized(true); } }, [existingQ.data, initialized]); if (!isNew && existingQ.isLoading) return ; const isSaving = createMut.isPending || updateMut.isPending; const onSave = () => { const payload = toRequest(form); if (isNew) { createMut.mutate(payload, { onSuccess: () => { toast({ title: 'Created', description: form.name, variant: 'success' }); navigate('/admin/outbound-connections'); }, onError: (e) => toast({ title: 'Create failed', description: describeApiError(e), variant: 'error' }), }); } else { updateMut.mutate(payload, { onSuccess: () => toast({ title: 'Updated', description: form.name, variant: 'success' }), onError: (e) => toast({ title: 'Update failed', description: describeApiError(e), variant: 'error' }), }); } }; const onTest = () => { if (!id) return; testMut.mutate(id, { onSuccess: (r) => setTestResult(r), onError: (e) => setTestResult({ status: 0, latencyMs: 0, responseSnippet: null, tlsProtocol: null, tlsCipherSuite: null, peerCertificateSubject: null, peerCertificateExpiresAtEpochMs: null, error: String(e), }), }); }; const envs = envQ.data ?? []; return (
{isNew ? 'New Outbound Connection' : `Edit: ${existingQ.data?.name ?? ''}`}
{/* Name */} setForm({ ...form, name: e.target.value })} placeholder="slack-ops" /> {/* Description */}