Files
supabase/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx
Gildas Garcia 96d43099bb chore: refactor Button API so that it can be used a standard button (#46880)
## Problem

Our `<Button>` component breaks the default `button` contract by
redefining the `type` prop to set its variant (`primary`, `default`,
etc) instead of the button type (`submit`, `button`, etc).
This is confusing and forces to write more code when using it with
shadcn components that expect/inject the standard button props.

## Solution

- rename the `type` prop to `variant`
- rename the `htmlType` prop to `type`
- propagate the changes where necessary
- format code

## How to test

As this is just prop renaming, if it builds it's ok

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
2026-06-16 23:59:58 +02:00

1055 lines
40 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { zodResolver } from '@hookform/resolvers/zod'
import { IS_PLATFORM, useFlag } from 'common'
import Link from 'next/link'
import { ReactNode, useEffect, useMemo, useRef } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
cn,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
Input,
RadioGroupCard,
RadioGroupCardItem,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetSection,
SheetTitle,
Switch,
TextArea,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import { z } from 'zod'
import {
DATADOG_REGIONS,
LAST9_REGIONS,
LOG_DRAIN_TYPES,
LogDrainType,
OTLP_PROTOCOLS,
} from './LogDrains.constants'
import {
getDefaultHeadersByType,
getHeadersSectionDescription as getHeadersDescription,
headerRecordToRows,
headerRowsToRecord,
logDrainHeaderEntriesSchema,
type LogDrainHeaderRow,
} from './LogDrains.utils'
import { TaxDisclaimer } from '@/components/interfaces/Billing/TaxDisclaimer'
import { Shortcut } from '@/components/ui/Shortcut'
import { LogDrainData } from '@/data/log-drains/log-drains-query'
import { DOCS_URL } from '@/lib/constants'
import { httpEndpointUrlSchema } from '@/lib/validation/http-url'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
const FORM_ID = 'log-drain-destination-form'
const headerRecordSchema = z.record(z.string(), z.string())
const webhookFields = {
type: z.literal('webhook'),
url: httpEndpointUrlSchema({
requiredMessage: 'Endpoint URL is required',
invalidMessage: 'Endpoint URL must be a valid URL',
prefixMessage: 'Endpoint URL must start with http:// or https://',
}),
http: z.enum(['http1', 'http2']),
gzip: z.boolean(),
}
const webhookFormSchema = z.object({
...webhookFields,
headerEntries: logDrainHeaderEntriesSchema.optional(),
})
const webhookSubmitSchema = z.object({
...webhookFields,
headers: headerRecordSchema.optional(),
})
const datadogSchema = 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' }),
})
const lokiFields = {
type: z.literal('loki'),
url: httpEndpointUrlSchema({
requiredMessage: 'Loki URL is required',
invalidMessage: 'Loki URL must be a valid URL',
prefixMessage: 'Loki URL must start with http:// or https://',
}),
username: z.string().optional(),
password: z.string().optional(),
}
const lokiFormSchema = z.object({
...lokiFields,
headerEntries: logDrainHeaderEntriesSchema.optional(),
})
const lokiSubmitSchema = z.object({
...lokiFields,
headers: headerRecordSchema,
})
const postgresSchema = z.object({
type: z.literal('postgres'),
})
const bigquerySchema = z.object({
type: z.literal('bigquery'),
})
const clickhouseSchema = z.object({
type: z.literal('clickhouse'),
})
const s3Schema = z.object({
type: z.literal('s3'),
s3_bucket: z.string().min(1, { message: 'Bucket name is required' }),
storage_region: z.string().min(1, { message: 'Region is required' }),
access_key_id: z.string().min(1, { message: 'Access Key ID is required' }),
secret_access_key: z.string().min(1, { message: 'Secret Access Key is required' }),
batch_timeout: z.coerce
.number()
.int({ message: 'Batch timeout must be an integer' })
.min(1, { message: 'Batch timeout must be a positive integer' }),
})
const sentrySchema = z.object({
type: z.literal('sentry'),
dsn: z
.string()
.min(1, { message: 'Sentry DSN is required' })
.refine((dsn) => dsn.startsWith('https://'), 'Sentry DSN must start with https://'),
})
const axiomSchema = z.object({
type: z.literal('axiom'),
api_token: z.string().min(1, { message: 'API token is required' }),
dataset_name: z.string().min(1, { message: 'Dataset name is required' }),
})
const last9Schema = z.object({
type: z.literal('last9'),
region: z.string().min(1, { message: 'Region is required' }),
username: z.string().min(1, { message: 'Username is required' }),
password: z.string().min(1, { message: 'Password is required' }),
})
const otlpFields = {
type: z.literal('otlp'),
endpoint: httpEndpointUrlSchema({
requiredMessage: 'OTLP endpoint is required',
invalidMessage: 'OTLP endpoint must be a valid URL',
prefixMessage: 'OTLP endpoint must start with http:// or https://',
}),
protocol: z.string().optional().default('http/protobuf'),
gzip: z.boolean().optional().default(true),
}
const otlpFormSchema = z.object({
...otlpFields,
headerEntries: logDrainHeaderEntriesSchema.optional(),
})
const otlpSubmitSchema = z.object({
...otlpFields,
headers: headerRecordSchema.optional(),
})
const syslogSchema = z.object({
type: z.literal('syslog'),
host: z.string().min(1, { message: 'Host is required' }),
port: z.coerce
.number()
.int({ message: 'Port must be an integer' })
.min(0, { message: 'Port must be between 0 and 65535' })
.max(65535, { message: 'Port must be between 0 and 65535' }),
tls: z.boolean().optional().default(false),
structured_data: z.string().optional(),
cipher_key: z.string().optional(),
ca_cert: z.string().optional(),
client_cert: z.string().optional(),
client_key: z.string().optional(),
})
const formUnion = z.discriminatedUnion('type', [
webhookFormSchema,
datadogSchema,
lokiFormSchema,
postgresSchema,
bigquerySchema,
clickhouseSchema,
s3Schema,
sentrySchema,
axiomSchema,
last9Schema,
otlpFormSchema,
syslogSchema,
])
const submitUnion = z.discriminatedUnion('type', [
webhookSubmitSchema,
datadogSchema,
lokiSubmitSchema,
postgresSchema,
bigquerySchema,
clickhouseSchema,
s3Schema,
sentrySchema,
axiomSchema,
last9Schema,
otlpSubmitSchema,
syslogSchema,
])
const formSchema = z
.object({
name: z.string().min(1, {
message: 'Destination name is required',
}),
description: z.string().optional(),
})
.and(formUnion)
.superRefine((data, ctx) => {
if (data.type !== 'syslog') return
if (data.client_cert && !data.client_key) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Client key is required when a client certificate is provided',
path: ['client_key'],
})
}
if (data.client_key && !data.client_cert) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Client certificate is required when a client key is provided',
path: ['client_cert'],
})
}
})
const submitSchema = z
.object({
name: z.string().min(1, {
message: 'Destination name is required',
}),
description: z.string().optional(),
})
.and(submitUnion)
type LogDrainDestinationFormValues = z.infer<typeof formSchema>
type LogDrainDestinationSubmitValues = z.infer<typeof submitSchema>
const HEADER_ENABLED_TYPES = ['webhook', 'loki', 'otlp'] as const
export function toSubmitValues(
values: LogDrainDestinationFormValues
): LogDrainDestinationSubmitValues {
if (!HEADER_ENABLED_TYPES.includes(values.type as (typeof HEADER_ENABLED_TYPES)[number])) {
return submitSchema.parse(values)
}
const { headerEntries = [], ...rest } = values as LogDrainDestinationFormValues & {
headerEntries?: LogDrainHeaderRow[]
}
const headers = headerRowsToRecord(headerEntries)
const transformedValues =
rest.type === 'loki'
? { ...rest, headers }
: Object.keys(headers).length > 0
? { ...rest, headers }
: rest
return submitSchema.parse(transformedValues)
}
function LogDrainFormItem({
value,
label,
description,
formControl,
placeholder,
type,
}: {
value: string
label: string
formControl: any
placeholder?: string
description?: ReactNode
type?: string
}) {
return (
<FormField
name={value}
control={formControl}
render={({ field }) => (
<FormItemLayout layout="horizontal" label={label} description={description || ''}>
<FormControl>
<Input type={type || 'text'} placeholder={placeholder} {...field} />
</FormControl>
</FormItemLayout>
)}
/>
)
}
type DefaultValues = { type: LogDrainType } & Partial<LogDrainData>
export function LogDrainDestinationSheetForm({
open,
onOpenChange,
defaultValues,
onSubmit,
isLoading,
mode,
existingDrainNames = [],
onSaveClick,
}: {
open: boolean
onOpenChange: (v: boolean) => void
defaultValues?: DefaultValues
isLoading?: boolean
onSubmit: (values: LogDrainDestinationSubmitValues) => void
mode: 'create' | 'update'
existingDrainNames?: string[]
onSaveClick?: (type: LogDrainType) => void
}) {
// NOTE(kamil): This used to be `any` for a long long time, but after moving to Zod,
// it produces a correct union type of all possible configs. Unfortunately, this type was not designed correctly
// and it does not include `type` inside the config itself, so it's not trivial to create `discriminatedUnion`
// out of it, therefore for an ease of use now, we bail to `any` until the better time come.
const defaultType = defaultValues?.type || 'webhook'
const defaultHeaderEntries = useMemo(() => {
const config = (defaultValues?.config || {}) as any
const type = defaultValues?.type || 'webhook'
return headerRecordToRows(
mode === 'create' ? getDefaultHeadersByType(type) : config?.headers || {}
)
}, [defaultValues, mode])
const sentryEnabled = useFlag('SentryLogDrain')
const s3Enabled = useFlag('S3logdrain')
const axiomEnabled = useFlag('axiomLogDrain')
const otlpEnabled = useFlag('otlpLogDrain')
const last9Enabled = useFlag('Last9LogDrain')
const syslogEnabled = useFlag('syslogLogDrain')
const formRef = useRef<HTMLFormElement>(null)
const formValues = useMemo(() => {
const config = (defaultValues?.config || {}) as any
const type = defaultValues?.type || 'webhook'
return {
name: defaultValues?.name || '',
description: defaultValues?.description || '',
type,
http: config?.http || 'http2',
gzip: mode === 'create' ? true : config?.gzip || false,
headerEntries: defaultHeaderEntries,
url: config?.url || '',
api_key: config?.api_key || '',
region: config?.region || '',
username: config?.username || '',
password: config?.password || '',
dsn: config?.dsn || '',
s3_bucket: config?.s3_bucket || '',
storage_region: config?.storage_region || '',
access_key_id: config?.access_key_id || '',
secret_access_key: config?.secret_access_key || '',
batch_timeout: config?.batch_timeout ?? 3000,
dataset_name: config?.dataset_name || '',
api_token: config?.api_token || '',
endpoint: config?.endpoint || '',
protocol: config?.protocol || 'http/protobuf',
host: config?.host || '',
port: (config?.port ?? '') as number,
tls: config?.tls ?? false,
structured_data: config?.structured_data || '',
cipher_key: config?.cipher_key || '',
ca_cert: config?.ca_cert || '',
client_cert: config?.client_cert || '',
client_key: config?.client_key || '',
}
}, [defaultValues, mode, defaultHeaderEntries])
const form = useForm<LogDrainDestinationFormValues>({
resolver: zodResolver(formSchema),
values: formValues,
})
const type = form.watch('type')
const tls = form.watch('tls')
useEffect(() => {
if (mode === 'create' && !open) {
form.reset()
}
}, [mode, open, form])
useEffect(() => {
if (!open || mode !== 'create') return
form.setValue('headerEntries', headerRecordToRows(getDefaultHeadersByType(type)))
form.clearErrors('headerEntries')
}, [form, mode, open, type])
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
tabIndex={undefined}
showClose={false}
size="lg"
className="overflow-y-auto flex flex-col"
>
<SheetHeader>
<SheetTitle>Add destination</SheetTitle>
</SheetHeader>
<SheetSection className="px-0! pb-0!">
<Form {...form}>
<form
ref={formRef}
id={FORM_ID}
onSubmit={(e) => {
e.preventDefault()
// Temp check to make sure the name is unique
const logDrainName = form.getValues('name')
const logDrainExists = existingDrainNames.includes(logDrainName)
if (logDrainExists && mode === 'create') {
toast.error('Log drain name already exists')
return
}
form.handleSubmit((values) => {
onSubmit(toSubmitValues(values))
onSaveClick?.(values.type)
})(e)
}}
>
<div className="space-y-8 px-content">
<LogDrainFormItem
value="name"
placeholder="My Destination"
label="Name"
formControl={form.control}
/>
<LogDrainFormItem
value="description"
placeholder="Optional description"
label="Description"
formControl={form.control}
/>
{mode === 'create' && (
<FormItemLayout
layout="horizontal"
label="Type"
description={LOG_DRAIN_TYPES.find((t) => t.value === type)?.description || ''}
>
<Select
defaultValue={defaultType}
value={form.getValues('type')}
onValueChange={(v: LogDrainType) => form.setValue('type', v)}
>
<SelectTrigger>
{LOG_DRAIN_TYPES.find((t) => t.value === type)?.name}
</SelectTrigger>
<SelectContent>
{LOG_DRAIN_TYPES.filter((t) => {
if (t.value === 'sentry') return sentryEnabled
if (t.value === 's3') return s3Enabled
if (t.value === 'axiom') return axiomEnabled
if (t.value === 'otlp') return otlpEnabled
if (t.value === 'last9') return last9Enabled
if (t.value === 'syslog') return syslogEnabled
return true
}).map((type) => (
<SelectItem
value={type.value}
key={type.value}
id={type.value}
className="text-left"
>
{type.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItemLayout>
)}
</div>
<div className="space-y-8 mt-4">
{type === 'webhook' && (
<>
<div className="px-content space-y-8">
<LogDrainFormItem
value="url"
label="Endpoint URL"
formControl={form.control}
placeholder="https://example.com/log-drain"
/>
<FormField
control={form.control}
name="http"
render={({ field }) => (
<FormItemLayout layout="horizontal" label="HTTP Version">
<FormControl>
<RadioGroupCard
className="flex gap-2"
onValueChange={field.onChange}
value={field.value}
>
<FormItem asChild>
<FormControl>
<RadioGroupCardItem value="http1" label="HTTP/1" />
</FormControl>
</FormItem>
<FormItem asChild>
<FormControl>
<RadioGroupCardItem value="http2" label="HTTP/2" />
</FormControl>
</FormItem>
</RadioGroupCard>
</FormControl>
</FormItemLayout>
)}
/>
</div>
<FormField
control={form.control}
name="gzip"
render={({ field }) => (
<FormItem className="space-y-2 px-4">
<div className="flex gap-2 items-center">
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormLabel className="text-base">Gzip</FormLabel>
<InfoTooltip align="start">
Gzip compresses logs before sending it to the destination.
</InfoTooltip>
</div>
</FormItem>
)}
/>
</>
)}
{type === 'datadog' && (
<div className="grid gap-4 px-content">
<LogDrainFormItem
type="password"
value="api_key"
label="API Key"
formControl={form.control}
description={
<>
The API Key obtained from the Datadog dashboard{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="text-sm underline transition hover:text-foreground"
href="https://app.datadoghq.com/organization-settings/api-keys"
>
here
</a>
</>
}
/>
<FormField
name="region"
control={form.control}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label={'Region'}
description={
<p>
The Datadog region to send logs to. Read more about Datadog regions{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground transition"
href="https://docs.datadoghq.com/getting_started/site/#access-the-datadog-site"
>
here
</a>
.
</p>
}
>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select a region" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Region</SelectLabel>
{DATADOG_REGIONS.map((reg) => (
<SelectItem key={reg.value} value={reg.value}>
{reg.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItemLayout>
)}
/>
</div>
)}
{type === 'loki' && (
<div className="grid gap-4 px-content">
<LogDrainFormItem
type="url"
value="url"
placeholder="https://my-logs-endpoint.grafana.net/loki/api/v1/push"
label="Loki URL"
formControl={form.control}
description="The Loki HTTP(S) endpoint to send events."
/>
<LogDrainFormItem
value="username"
label="Username"
placeholder="123456789"
formControl={form.control}
/>
<LogDrainFormItem
type="password"
value="password"
label="Password"
placeholder="glc_ABCD1234567890"
formControl={form.control}
/>
</div>
)}
{type === 'sentry' && (
<div className="grid gap-4 px-content">
<LogDrainFormItem
type="text"
value="dsn"
label="DSN"
placeholder="https://<project_id>@o<organization_id>.ingest.sentry.io/<project_id>"
formControl={form.control}
description={
<>
The DSN obtained from the Sentry dashboard. Read more about DSNs{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="text-sm underline transition hover:text-foreground"
href="https://docs.sentry.io/concepts/key-terms/dsn-explainer/"
>
here
</a>
.
</>
}
/>
</div>
)}
{type === 's3' && (
<div className="grid gap-4 px-content">
<LogDrainFormItem
value="s3_bucket"
label="S3 Bucket"
placeholder="my-log-bucket"
formControl={form.control}
description="The name of an existing S3 bucket."
/>
<LogDrainFormItem
value="storage_region"
label="Region"
placeholder="us-east-1"
formControl={form.control}
description="AWS region where the bucket is located."
/>
<LogDrainFormItem
value="access_key_id"
label="Access Key ID"
placeholder="AKIA..."
formControl={form.control}
/>
<LogDrainFormItem
type="password"
value="secret_access_key"
label="Secret Access Key"
placeholder="••••••••••••••••"
formControl={form.control}
/>
<LogDrainFormItem
type="number"
value="batch_timeout"
label="Batch Timeout (ms)"
placeholder="3000"
formControl={form.control}
description="Recommended 20005000ms."
/>
<p className="text-xs text-foreground-lighter">
Ensure the account tied to the Access Key ID can write to the specified
bucket.
</p>
</div>
)}
{type === 'axiom' && (
<div className="grid gap-4 px-content">
<LogDrainFormItem
type="text"
value="dataset_name"
label="Dataset name"
placeholder="dataset"
formControl={form.control}
description="Name of the dataset in Axiom where the logs will be sent."
/>
<LogDrainFormItem
type="text"
value="api_token"
label="API Token"
placeholder="xaat-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
formControl={form.control}
description="Token allowing ingest access to the specified dataset"
/>
</div>
)}
{type === 'otlp' && (
<>
<div className="grid gap-4 px-content">
<LogDrainFormItem
type="url"
value="endpoint"
label="OTLP Endpoint"
placeholder="https://otlp.example.com:4318/v1/logs"
formControl={form.control}
description="The HTTP endpoint for OTLP log ingestion (typically ends with /v1/logs)"
/>
<FormField
name="protocol"
control={form.control}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Protocol"
description="Only HTTP with Protocol Buffers is currently supported"
>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select protocol" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Protocol</SelectLabel>
{OTLP_PROTOCOLS.map((proto) => (
<SelectItem key={proto.value} value={proto.value}>
{proto.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItemLayout>
)}
/>
</div>
<FormField
control={form.control}
name="gzip"
render={({ field }) => (
<FormItem className="space-y-2 px-4">
<div className="flex gap-2 items-center">
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormLabel className="text-base">Gzip Compression</FormLabel>
<InfoTooltip align="start">
Enable gzip compression for log data sent to the OTLP endpoint.
</InfoTooltip>
</div>
</FormItem>
)}
/>
</>
)}
{type === 'last9' && (
<div className="grid gap-4 px-content">
<FormField
name="region"
control={form.control}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label={'Region'}
description={
<p>
The Last9 region to send logs to. Credentials can be obtained from the
Last9 OTEL integration panel.
</p>
}
>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select a region" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Region</SelectLabel>
{LAST9_REGIONS.map((reg) => (
<SelectItem key={reg.value} value={reg.value}>
{reg.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItemLayout>
)}
/>
<LogDrainFormItem
type="text"
value="username"
label="Username"
placeholder="username"
formControl={form.control}
description="Username for authentication from Last9 OTEL integration."
/>
<LogDrainFormItem
type="password"
value="password"
label="Password"
placeholder="••••••••••••••••"
formControl={form.control}
description="Password for authentication from Last9 OTEL integration."
/>
</div>
)}
{type === 'syslog' && (
<>
<div className="grid gap-4 px-content">
<LogDrainFormItem
value="host"
label="Host"
placeholder="logs.example.com"
formControl={form.control}
description="Hostname or IP address of the syslog receiver."
/>
<LogDrainFormItem
type="number"
value="port"
label="Port"
placeholder="514"
formControl={form.control}
description="Port of the syslog receiver (065535)."
/>
<LogDrainFormItem
value="structured_data"
label="Structured Data"
placeholder='[exampleSDID@32473 iut="3" eventSource="Application"]'
formControl={form.control}
description="Static RFC 5424 Structured Data included in every log frame."
/>
<LogDrainFormItem
type="password"
value="cipher_key"
label="Cipher Key"
placeholder="••••••••••••••••"
formControl={form.control}
description="Base64-encoded 32-byte key for AES-256-GCM encryption of the log body."
/>
</div>
<FormField
control={form.control}
name="tls"
render={({ field }) => (
<FormItem className="space-y-2 px-4">
<div className="flex gap-2 items-center">
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormLabel className="text-base">TLS</FormLabel>
<InfoTooltip align="start">
Connect via SSL/TLS instead of plain TCP.
</InfoTooltip>
</div>
</FormItem>
)}
/>
{tls && (
<div className="grid gap-4 px-content">
<FormField
name="ca_cert"
control={form.control}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="CA Certificate"
description="PEM encoded CA certificate for verifying the server. Falls back to the system CA bundle if omitted."
>
<FormControl>
<TextArea
className="font-mono text-xs"
placeholder="-----BEGIN CERTIFICATE-----"
rows={4}
{...field}
/>
</FormControl>
</FormItemLayout>
)}
/>
<FormField
name="client_cert"
control={form.control}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Client Certificate"
description="PEM encoded client certificate for mTLS."
>
<FormControl>
<TextArea
className="font-mono text-xs"
placeholder="-----BEGIN CERTIFICATE-----"
rows={4}
{...field}
/>
</FormControl>
</FormItemLayout>
)}
/>
<FormField
name="client_key"
control={form.control}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Client Key"
description="PEM encoded client private key for mTLS. Required when a client certificate is provided."
>
<FormControl>
<TextArea
className="font-mono text-xs"
placeholder="-----BEGIN PRIVATE KEY-----"
rows={4}
{...field}
/>
</FormControl>
</FormItemLayout>
)}
/>
</div>
)}
</>
)}
{HEADER_ENABLED_TYPES.includes(type as (typeof HEADER_ENABLED_TYPES)[number]) && (
<div className="px-content">
<FormField
control={form.control}
name="headerEntries"
render={({ fieldState }) => (
<FormItemLayout
layout="horizontal"
label="Custom Headers"
description={getHeadersDescription(type)}
hideMessage={!fieldState.error?.message}
>
<KeyValueFieldArray
control={form.control}
name="headerEntries"
keyFieldName="key"
valueFieldName="value"
createEmptyRow={() => ({ key: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add a new header"
removeLabel="Remove header"
/>
</FormItemLayout>
)}
/>
</div>
)}
</div>
</form>
</Form>
</SheetSection>
<div className="mt-auto">
<SheetSection
className={cn(
`border-t bg-background-alternative-200 mt-auto py-1.5 ${!IS_PLATFORM && 'hidden'}`
)}
>
<ul className="text-right text-foreground-light divide-y divide-dashed text-sm">
<li className="flex items-center justify-between gap-2 py-2" translate="no">
<span className="text-foreground-lighter">Additional drain cost</span>
<span className="text-foreground">$60 per month</span>
</li>
<li className="flex items-center justify-between gap-2 py-2" translate="no">
<span className="text-foreground-lighter">Per million events</span>
<span>+$0.20</span>
</li>
<li className="flex items-center justify-between gap-2 py-2" translate="no">
<span className="text-foreground-lighter">Per GB egress</span>
<span>+$0.09</span>
</li>
</ul>
</SheetSection>
<SheetFooter className="p-content mt-0! justify-between! flex-row! w-full items-center">
<div className="flex flex-col gap-0.5">
<span className="text-sm text-foreground-light">
<span>See full pricing breakdown</span>{' '}
<Link
href={`${DOCS_URL}/guides/platform/manage-your-usage/log-drains`}
target="_blank"
className="text-foreground underline underline-offset-2 decoration-foreground-muted hover:decoration-foreground transition-all"
>
here
</Link>
</span>
<TaxDisclaimer />
</div>
<Shortcut
id={SHORTCUT_IDS.LOG_DRAINS_SAVE_DESTINATION}
onTrigger={() => formRef.current?.requestSubmit()}
options={{ enabled: open && !isLoading }}
side="top"
>
<Button form={FORM_ID} loading={isLoading} type="submit" variant="primary">
Save destination
</Button>
</Shortcut>
</SheetFooter>
</div>
</SheetContent>
</Sheet>
)
}