"use client"; import { useState, useEffect } from "react"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { Button } from "@app/components/ui/button"; import { Input } from "@app/components/ui/input"; import { Label } from "@app/components/ui/label"; import { Switch } from "@app/components/ui/switch"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; import { Textarea } from "@app/components/ui/textarea"; import { Checkbox } from "@app/components/ui/checkbox"; import { Plus, X } from "lucide-react"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { build } from "@server/build"; import { useTranslations } from "next-intl"; // ── Types ────────────────────────────────────────────────────────────────────── export type AuthType = "none" | "bearer" | "basic" | "custom"; export type PayloadFormat = "json_array" | "ndjson" | "json_single"; export interface HttpConfig { name: string; url: string; authType: AuthType; bearerToken?: string; basicCredentials?: string; customHeaderName?: string; customHeaderValue?: string; headers: Array<{ key: string; value: string }>; format: PayloadFormat; useBodyTemplate: boolean; bodyTemplate?: string; } export interface Destination { destinationId: number; orgId: string; type: string; config: string; enabled: boolean; sendAccessLogs: boolean; sendActionLogs: boolean; sendConnectionLogs: boolean; sendRequestLogs: boolean; createdAt: number; updatedAt: number; } // ── Helpers ──────────────────────────────────────────────────────────────────── export const defaultHttpConfig = (): HttpConfig => ({ name: "", url: "", authType: "none", bearerToken: "", basicCredentials: "", customHeaderName: "", customHeaderValue: "", headers: [], format: "json_array", useBodyTemplate: false, bodyTemplate: "" }); export function parseHttpConfig(raw: string): HttpConfig { try { return { ...defaultHttpConfig(), ...JSON.parse(raw) }; } catch { return defaultHttpConfig(); } } // ── Headers editor ───────────────────────────────────────────────────────────── interface HeadersEditorProps { headers: Array<{ key: string; value: string }>; onChange: (headers: Array<{ key: string; value: string }>) => void; } function HeadersEditor({ headers, onChange }: HeadersEditorProps) { const t = useTranslations(); const addRow = () => onChange([...headers, { key: "", value: "" }]); const removeRow = (i: number) => onChange(headers.filter((_, idx) => idx !== i)); const updateRow = (i: number, field: "key" | "value", val: string) => { const next = [...headers]; next[i] = { ...next[i], [field]: val }; onChange(next); }; return (
{headers.length === 0 && (

{t("httpDestNoHeadersConfigured")}

)} {headers.map((h, i) => (
updateRow(i, "key", e.target.value)} placeholder={t("httpDestHeaderNamePlaceholder")} className="flex-1" /> updateRow(i, "value", e.target.value) } placeholder={t("httpDestHeaderValuePlaceholder")} className="flex-1" />
))}
); } // ── Component ────────────────────────────────────────────────────────────────── export interface HttpDestinationCredenzaProps { open: boolean; onOpenChange: (open: boolean) => void; editing: Destination | null; orgId: string; onSaved: () => void; } export function HttpDestinationCredenza({ open, onOpenChange, editing, orgId, onSaved }: HttpDestinationCredenzaProps) { const api = createApiClient(useEnvContext()); const t = useTranslations(); const [saving, setSaving] = useState(false); const [cfg, setCfg] = useState(defaultHttpConfig()); const [sendAccessLogs, setSendAccessLogs] = useState(false); const [sendActionLogs, setSendActionLogs] = useState(false); const [sendConnectionLogs, setSendConnectionLogs] = useState(false); const [sendRequestLogs, setSendRequestLogs] = useState(false); useEffect(() => { if (open) { setCfg( editing ? parseHttpConfig(editing.config) : defaultHttpConfig() ); setSendAccessLogs(editing?.sendAccessLogs ?? false); setSendActionLogs(editing?.sendActionLogs ?? false); setSendConnectionLogs(editing?.sendConnectionLogs ?? false); setSendRequestLogs(editing?.sendRequestLogs ?? false); } }, [open, editing]); const update = (patch: Partial) => setCfg((prev) => ({ ...prev, ...patch })); const urlError: string | null = (() => { const raw = cfg.url.trim(); if (!raw) return null; try { const parsed = new URL(raw); if ( parsed.protocol !== "http:" && parsed.protocol !== "https:" ) { return t("httpDestUrlErrorHttpRequired"); } if (build === "saas" && parsed.protocol !== "https:") { return t("httpDestUrlErrorHttpsRequired"); } return null; } catch { return t("httpDestUrlErrorInvalid"); } })(); const isValid = cfg.name.trim() !== "" && cfg.url.trim() !== "" && urlError === null; async function handleSave() { if (!isValid) return; setSaving(true); try { const payload = { type: "http", config: JSON.stringify(cfg), sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs }; if (editing) { await api.post( `/org/${orgId}/event-streaming-destination/${editing.destinationId}`, payload ); toast({ title: t("httpDestUpdatedSuccess") }); } else { await api.put( `/org/${orgId}/event-streaming-destination`, payload ); toast({ title: t("httpDestCreatedSuccess") }); } onSaved(); onOpenChange(false); } catch (e) { toast({ variant: "destructive", title: editing ? t("httpDestUpdateFailed") : t("httpDestCreateFailed"), description: formatAxiosError( e, t("streamingUnexpectedError") ) }); } finally { setSaving(false); } } return ( {editing ? t("httpDestEditTitle") : t("httpDestAddTitle")} {editing ? t("httpDestEditDescription") : t("httpDestAddDescription")} {/* ── Settings tab ────────────────────────────── */}
{/* Name */}
update({ name: e.target.value }) } />
{/* URL */}
update({ url: e.target.value }) } /> {urlError && (

{urlError}

)}
{/* Authentication */}

{t("httpDestAuthDescription")}

update({ authType: v as AuthType }) } className="gap-2" > {/* None */}

{t("httpDestAuthNoneDescription")}

{/* Bearer */}

{t("httpDestAuthBearerDescription")}

{cfg.authType === "bearer" && ( update({ bearerToken: e.target.value }) } /> )}
{/* Basic */}

{t("httpDestAuthBasicDescription")}

{cfg.authType === "basic" && ( update({ basicCredentials: e.target.value }) } /> )}
{/* Custom */}

{t("httpDestAuthCustomDescription")}

{cfg.authType === "custom" && (
update({ customHeaderName: e.target .value }) } className="flex-1" /> update({ customHeaderValue: e.target .value }) } className="flex-1" />
)}
{/* ── Headers tab ──────────────────────────────── */}

{t("httpDestCustomHeadersDescription")}

update({ headers })} />
{/* ── Body tab ─────────────────────────── */}

{t("httpDestBodyTemplateDescription")}

update({ useBodyTemplate: v }) } />
{cfg.useBodyTemplate && (