import { zodResolver } from '@hookform/resolvers/zod' import { ExternalLink, TrashIcon } from 'lucide-react' import { ReactNode, useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import { useParams } from 'common' import { LogDrainData, useLogDrainsQuery } from 'data/log-drains/log-drains-query' import { useFlag } from 'hooks/ui/useFlag' import { Button, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, FormLabel_Shadcn_, FormMessage_Shadcn_, Input_Shadcn_, RadioGroupCard, RadioGroupCardItem, Select_Shadcn_, SelectContent_Shadcn_, SelectGroup_Shadcn_, SelectItem_Shadcn_, SelectLabel_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, Sheet, SheetContent, SheetFooter, SheetHeader, SheetSection, SheetTitle, Switch, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { urlRegex } from '../Auth/Auth.constants' import { DATADOG_REGIONS, LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants' import { DocsButton } from 'components/ui/DocsButton' const FORM_ID = 'log-drain-destination-form' const formUnion = z.discriminatedUnion('type', [ z.object({ type: z.literal('webhook'), url: z.string().regex(urlRegex(), 'Endpoint URL is required and must be a valid URL'), http: z.enum(['http1', 'http2']), gzip: z.boolean(), headers: z.record(z.string(), z.string()).optional(), }), z.object({ type: z.literal('datadog'), api_key: z.string().min(1, { message: 'API key is required' }), region: z.string().min(1, { message: 'Region is required' }), }), z.object({ type: z.literal('elastic'), }), z.object({ type: z.literal('postgres'), }), z.object({ type: z.literal('bigquery'), }), z.object({ type: z.literal('loki'), url: z.string().min(1, { message: 'Loki URL is required' }), headers: z.record(z.string(), z.string()), username: z.string().optional(), password: z.string().optional(), }), ]) const formSchema = z .object({ name: z.string().min(1, { message: 'Destination name is required', }), description: z.string().optional(), }) .and(formUnion) function LogDrainFormItem({ value, label, description, formControl, placeholder, type, defaultValue, }: { value: string label: string formControl: any placeholder?: string description?: ReactNode type?: string defaultValue?: string }) { return ( ( )} /> ) } type DefaultValues = { type: LogDrainType } & Partial export function LogDrainDestinationSheetForm({ open, onOpenChange, defaultValues, onSubmit, isLoading, mode, }: { open: boolean onOpenChange: (v: boolean) => void defaultValues?: DefaultValues isLoading?: boolean onSubmit: (values: z.infer) => void mode: 'create' | 'update' }) { const lokiLogDrainsEnabled = useFlag('lokilogdrains') const CREATE_DEFAULT_HEADERS = { 'Content-Type': 'application/json', } const DEFAULT_HEADERS = mode === 'create' ? CREATE_DEFAULT_HEADERS : defaultValues?.config?.headers || {} const { ref } = useParams() const { data: logDrains } = useLogDrainsQuery({ ref, }) const defaultType = defaultValues?.type || 'webhook' const [newCustomHeader, setNewCustomHeader] = useState({ name: '', value: '' }) const form = useForm>({ resolver: zodResolver(formSchema), values: { name: defaultValues?.name || '', description: defaultValues?.description || '', type: defaultType, http: defaultValues?.config?.http || 'http2', gzip: mode === 'create' ? true : defaultValues?.config?.gzip || false, headers: DEFAULT_HEADERS, url: defaultValues?.config?.url || '', api_key: defaultValues?.config?.api_key || '', region: defaultValues?.config?.region || '', username: defaultValues?.config?.username || '', password: defaultValues?.config?.password || '', }, }) const headers = form.watch('headers') const type = form.watch('type') function removeHeader(key: string) { const newHeaders = { ...headers, } delete newHeaders[key] form.setValue('headers', newHeaders) } function addHeader() { const formHeaders = form.getValues('headers') if (!formHeaders) return const headerKeys = Object.keys(formHeaders) if (headerKeys?.length === 20) { toast.error('You can only have 20 custom headers') return } if (headerKeys?.includes(newCustomHeader.name)) { toast.error('Header name already exists') return } if (!newCustomHeader.name || !newCustomHeader.value) { toast.error('Header name and value are required') return } form.setValue('headers', { ...formHeaders, [newCustomHeader.name]: newCustomHeader.value }) setNewCustomHeader({ name: '', value: '' }) } const hasHeaders = Object.keys(headers || {})?.length > 0 useEffect(() => { if (mode === 'create' && !open) { form.reset() } }, [mode, open, form]) function getHeadersSectionDescription() { if (type === 'webhook') { return 'Set custom headers when draining logs to the Endpoint URL' } if (type === 'loki') { return 'Set custom headers when draining logs to the Loki HTTP(S) endpoint' } return '' } return ( { setNewCustomHeader({ name: '', value: '' }) onOpenChange(v) }} > Add destination
{ e.preventDefault() // Temp check to make sure the name is unique const logDrainName = form.getValues('name') const logDrainExists = logDrains?.find((drain) => drain.name === logDrainName) if (logDrainExists && mode === 'create') { toast.error('Log drain name already exists') return } form.handleSubmit(onSubmit)(e) }} >
{mode === 'create' && ( t.value === type)?.description || ''} > form.setValue('type', v)} > {LOG_DRAIN_TYPES.find((t) => t.value === type)?.name} {LOG_DRAIN_TYPES.map((type) => type.value === 'loki' && !lokiLogDrainsEnabled ? null : ( {type.name} ) )} )}
{type === 'webhook' && ( <>
( )} />
(
Gzip Gzip compresses logs before sending it to the destination.
)} /> )} {type === 'datadog' && (
The API Key obtained from the Datadog dashboard{' '} here } /> ( The Datadog region to send logs to. Read more about Datadog regions{' '} here .

} > Region {DATADOG_REGIONS.map((reg) => ( {reg.label} ))}
)} />
)} {type === 'loki' && (
)}
{/* This form needs to be outside the */} {(type === 'webhook' || type === 'loki') && ( <>

Custom Headers

{getHeadersSectionDescription()}

{hasHeaders && Object.keys(headers || {})?.map((headerKey) => (
{headerKey}
{headers?.[headerKey]}
))}
{ e.preventDefault() e.stopPropagation() addHeader() }} className="flex border-t py-4 gap-4 items-center px-content" > setNewCustomHeader({ ...newCustomHeader, name: e.target.value })} /> setNewCustomHeader({ ...newCustomHeader, value: e.target.value }) } /> )}
Additional drain cost
} >
  • $60 per drain per month
  • $0.20 per million events
  • $0.09 per GB egress
) }