"use client"; import React, { useState, useEffect, useCallback } from "react"; import { useParams } from "next/navigation"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { Button } from "@app/components/ui/button"; import { Switch } from "@app/components/ui/switch"; import { Globe, MoreHorizontal, Plus, AlertCircle, ChevronDown } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { AxiosResponse } from "axios"; import { build } from "@server/build"; import Image from "next/image"; import { StrategySelect, StrategyOption } from "@app/components/StrategySelect"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Destination, HttpDestinationCredenza, parseHttpConfig } from "@app/components/HttpDestinationCredenza"; import { S3DestinationCredenza, parseS3Config } from "@app/components/S3DestinationCredenza"; import { DatadogDestinationCredenza } from "@app/components/DatadogDestinationCredenza"; import { useTranslations } from "next-intl"; // ── Re-export Destination so the rest of the file can use it ────────────────── interface ListDestinationsResponse { destinations: Destination[]; pagination: { total: number; limit: number; offset: number; }; } // ── Destination card ─────────────────────────────────────────────────────────── interface DestinationCardProps { destination: Destination; onToggle: (id: number, enabled: boolean) => void; onEdit: (destination: Destination) => void; onDelete: (destination: Destination) => void; isToggling: boolean; disabled?: boolean; } function getDestinationDisplay(destination: Destination): { name: string; typeLabel: string; detail: string; icon: React.ReactNode; } { if (destination.type === "s3") { const cfg = parseS3Config(destination.config); const detail = cfg.bucket ? `s3://${cfg.bucket}${cfg.prefix ? `/${cfg.prefix.replace(/^\/+/, "")}` : ""}` : ""; return { name: cfg.name, typeLabel: "Amazon S3", detail, icon: ( Amazon S3 ) }; } // Default: HTTP const cfg = parseHttpConfig(destination.config); return { name: cfg.name, typeLabel: "HTTP", detail: cfg.url, icon: }; } function DestinationCard({ destination, onToggle, onEdit, onDelete, isToggling, disabled = false }: DestinationCardProps) { const t = useTranslations(); const { name, typeLabel, detail, icon } = getDestinationDisplay(destination); return (
{/* Top row: icon + name/type + toggle */}
{icon}

{name || t("streamingUnnamedDestination")}

{typeLabel}

onToggle(destination.destinationId, v) } disabled={isToggling || disabled} className="shrink-0 mt-0.5" />
{/* Detail preview (URL for HTTP, s3:// path for S3) */}

{detail || ( {t("streamingNoUrlConfigured")} )}

{/* Error indicator */} {destination.lastError && ( {destination.lastError} )} {/* Footer: edit button + three-dots menu */}
onDelete(destination)} > {t("delete")}
); } // ── Add destination card ─────────────────────────────────────────────────────── function AddDestinationCard({ onClick }: { onClick: () => void }) { const t = useTranslations(); return ( ); } // ── Destination type picker ──────────────────────────────────────────────────── type DestinationType = "http" | "s3" | "datadog"; interface DestinationTypePickerProps { open: boolean; onOpenChange: (open: boolean) => void; onSelect: (type: DestinationType) => void; isPaywalled?: boolean; } function DestinationTypePicker({ open, onOpenChange, onSelect, isPaywalled = false }: DestinationTypePickerProps) { const t = useTranslations(); const [selected, setSelected] = useState("http"); const destinationTypeOptions: ReadonlyArray< StrategyOption > = [ { id: "http", title: t("streamingHttpWebhookTitle"), description: t("streamingHttpWebhookDescription"), icon: }, { id: "s3", title: t("streamingS3Title"), description: t("streamingS3Description"), icon: ( {t("streamingS3Title")} ) }, { id: "datadog", title: t("streamingDatadogTitle"), description: t("streamingDatadogDescription"), icon: ( {t("streamingDatadogTitle")} ) } ]; useEffect(() => { if (open) setSelected("http"); }, [open]); return ( {t("streamingAddDestination")} {t("streamingTypePickerDescription")}
setSelected(type)} cols={1} />
); } // ── Main page ────────────────────────────────────────────────────────────────── export default function StreamingDestinationsPage() { const { orgId } = useParams() as { orgId: string }; const api = createApiClient(useEnvContext()); const { isPaidUser } = usePaidStatus(); const isEnterprise = isPaidUser(tierMatrix[TierFeature.SIEM]); const t = useTranslations(); const [destinations, setDestinations] = useState([]); const [loading, setLoading] = useState(true); const [modalOpen, setModalOpen] = useState(false); const [typePickerOpen, setTypePickerOpen] = useState(false); const [editingDestination, setEditingDestination] = useState(null); const [pickedType, setPickedType] = useState("http"); const [togglingIds, setTogglingIds] = useState>(new Set()); // Delete state const [deleteTarget, setDeleteTarget] = useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleting, setDeleting] = useState(false); const loadDestinations = useCallback(async () => { if (build == "oss") { setDestinations([]); setLoading(false); return; } try { const res = await api.get>( `/org/${orgId}/event-streaming-destinations` ); setDestinations(res.data.data.destinations ?? []); } catch (e) { toast({ variant: "destructive", title: t("streamingFailedToLoad"), description: formatAxiosError(e, t("streamingUnexpectedError")) }); } finally { setLoading(false); } }, [orgId]); useEffect(() => { loadDestinations(); }, [loadDestinations]); const handleToggle = async (destinationId: number, enabled: boolean) => { // Optimistic update setDestinations((prev) => prev.map((d) => d.destinationId === destinationId ? { ...d, enabled } : d ) ); setTogglingIds((prev) => new Set(prev).add(destinationId)); try { await api.post( `/org/${orgId}/event-streaming-destination/${destinationId}`, { enabled } ); } catch (e) { // Revert on failure setDestinations((prev) => prev.map((d) => d.destinationId === destinationId ? { ...d, enabled: !enabled } : d ) ); toast({ variant: "destructive", title: t("streamingFailedToUpdate"), description: formatAxiosError(e, t("streamingUnexpectedError")) }); } finally { setTogglingIds((prev) => { const next = new Set(prev); next.delete(destinationId); return next; }); } }; const handleDeleteCard = (destination: Destination) => { setDeleteTarget(destination); setDeleteDialogOpen(true); }; const handleDeleteConfirm = async () => { if (!deleteTarget) return; setDeleting(true); try { await api.delete( `/org/${orgId}/event-streaming-destination/${deleteTarget.destinationId}` ); toast({ title: t("streamingDeletedSuccess") }); setDeleteDialogOpen(false); setDeleteTarget(null); loadDestinations(); } catch (e) { toast({ variant: "destructive", title: t("streamingFailedToDelete"), description: formatAxiosError(e, t("streamingUnexpectedError")) }); } finally { setDeleting(false); } }; const openCreate = () => { setTypePickerOpen(true); }; const handleTypePicked = (type: DestinationType) => { setPickedType(type); setTypePickerOpen(false); setEditingDestination(null); setModalOpen(true); }; const openEdit = (destination: Destination) => { setEditingDestination(destination); setPickedType((destination.type as DestinationType) ?? "http"); setModalOpen(true); }; return ( <> {loading ? (
{Array.from({ length: 4 }).map((_, i) => (
))}
) : (
{destinations.map((dest) => ( ))} {/* Add card is always clickable - paywall is enforced inside the picker */}
)} {pickedType === "http" && ( )} {pickedType === "s3" && ( )} {pickedType === "datadog" && ( )} {deleteTarget && ( { setDeleteDialogOpen(v); if (!v) setDeleteTarget(null); }} string={ getDestinationDisplay(deleteTarget).name || t("streamingDeleteDialogThisDestination") } title={t("streamingDeleteTitle")} dialog={

{t("streamingDeleteDialogAreYouSure")}{" "} {getDestinationDisplay(deleteTarget).name || t("streamingDeleteDialogThisDestination")} {t("streamingDeleteDialogPermanentlyRemoved")}

} buttonText={t("streamingDeleteButtonText")} onConfirm={handleDeleteConfirm} /> )} ); }