"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 (
);
}
// ── 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 */}
{/* ── Headers tab ──────────────────────────────── */}
{t("httpDestCustomHeadersDescription")}
update({ headers })}
/>
{/* ── Body tab ─────────────────────────── */}
{t("httpDestBodyTemplateDescription")}
update({ useBodyTemplate: v })
}
/>
{cfg.useBodyTemplate && (
)}
{/* Payload Format */}
{t("httpDestPayloadFormatDescription")}
update({
format: v as PayloadFormat
})
}
className="gap-2"
>
{/* JSON Array */}
{t("httpDestFormatJsonArrayDescription")}
{/* NDJSON */}
{t("httpDestFormatNdjsonDescription")}
{/* Single event per request */}
{t("httpDestFormatSingleDescription")}
{/* ── Logs tab ──────────────────────────────────── */}
{t("httpDestLogTypesDescription")}
setSendAccessLogs(v === true)
}
className="mt-0.5"
/>
{t("httpDestAccessLogsDescription")}
setSendActionLogs(v === true)
}
className="mt-0.5"
/>
{t("httpDestActionLogsDescription")}
setSendConnectionLogs(v === true)
}
className="mt-0.5"
/>
{t("httpDestConnectionLogsDescription")}
setSendRequestLogs(v === true)
}
className="mt-0.5"
/>
{t("httpDestRequestLogsDescription")}
);
}