mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
log drain fixes 2: the return of the fix (#28392)
This commit is contained in:
1 parent
daebf486c1
commit
4671c91c8a
2 files changed
+137
-120
No files matched your search
@@ -34,17 +34,19 @@ import { z } from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { TrashIcon } from 'lucide-react'
|
||||
import { LogDrainData, useLogDrainsQuery } from 'data/log-drains/log-drains-query'
|
||||
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||||
import Link from 'next/link'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@ui/components/shadcn/ui/select'
|
||||
import { FormDescription } from '@ui/components/shadcn/ui/form'
|
||||
|
||||
const FORM_ID = 'log-drain-destination-form'
|
||||
|
||||
const formUnion = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('webhook'),
|
||||
url: z.string().url('Webhook URL is required and must be a valid URL'),
|
||||
url: z.string().url('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(),
|
||||
@@ -56,9 +58,6 @@ const formUnion = z.discriminatedUnion('type', [
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('elastic'),
|
||||
url: z.string().url({ message: 'URL is required and must be a valid URL' }),
|
||||
username: z.string().min(1, { message: 'Username is required' }),
|
||||
password: z.string().min(1, { message: 'Password is required' }),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('postgres'),
|
||||
@@ -90,7 +89,7 @@ function LogDrainFormItem({
|
||||
label: string
|
||||
formControl: any
|
||||
placeholder?: string
|
||||
description?: string
|
||||
description?: ReactNode
|
||||
type?: string
|
||||
defaultValue?: string
|
||||
}) {
|
||||
@@ -131,6 +130,13 @@ export function LogDrainDestinationSheetForm({
|
||||
onSubmit: (values: z.infer<typeof formSchema>) => void
|
||||
mode: 'create' | 'update'
|
||||
}) {
|
||||
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,
|
||||
@@ -147,12 +153,10 @@ export function LogDrainDestinationSheetForm({
|
||||
type: defaultType,
|
||||
http: defaultValues?.config?.http || 'http2',
|
||||
gzip: defaultValues?.config?.gzip || true,
|
||||
headers: defaultValues?.config?.headers || {},
|
||||
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 || '',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -207,7 +211,7 @@ export function LogDrainDestinationSheetForm({
|
||||
<SheetHeader>
|
||||
<SheetTitle>Add destination</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetSection>
|
||||
<SheetSection className="!px-0">
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={FORM_ID}
|
||||
@@ -225,7 +229,7 @@ export function LogDrainDestinationSheetForm({
|
||||
form.handleSubmit(onSubmit)(e)
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 px-content">
|
||||
<LogDrainFormItem
|
||||
value="name"
|
||||
placeholder="My Destination"
|
||||
@@ -239,57 +243,69 @@ export function LogDrainDestinationSheetForm({
|
||||
formControl={form.control}
|
||||
/> */}
|
||||
{mode === 'create' && (
|
||||
<RadioGroupStacked
|
||||
defaultValue={defaultType}
|
||||
value={form.getValues('type')}
|
||||
onValueChange={(v: LogDrainType) => form.setValue('type', v)}
|
||||
>
|
||||
{LOG_DRAIN_TYPES.map((type) => (
|
||||
<RadioGroupStackedItem
|
||||
value={type.value}
|
||||
key={type.value}
|
||||
id={type.value}
|
||||
label={type.name}
|
||||
description={type.description}
|
||||
className="text-left"
|
||||
/>
|
||||
))}
|
||||
</RadioGroupStacked>
|
||||
<FormItemLayout layout="horizontal" label="Type">
|
||||
<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.map((type) => (
|
||||
<SelectItem
|
||||
value={type.value}
|
||||
key={type.value}
|
||||
id={type.value}
|
||||
className="text-left"
|
||||
>
|
||||
{type.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
<FormDescription className="mt-2">
|
||||
{LOG_DRAIN_TYPES.find((t) => t.value === type)?.description}
|
||||
</FormDescription>
|
||||
</Select>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mt-6">
|
||||
<div className="space-y-6 mt-4">
|
||||
{type === 'webhook' && (
|
||||
<>
|
||||
<LogDrainFormItem
|
||||
value="url"
|
||||
label="Webhook URL"
|
||||
formControl={form.control}
|
||||
placeholder="https://example.com/webhooks/log-drain"
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="http"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_>
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStacked onValueChange={field.onChange} value={field.value}>
|
||||
<FormItem_Shadcn_ asChild>
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStackedItem value="http1" label="HTTP/1" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItem_Shadcn_>
|
||||
<FormItem_Shadcn_ asChild>
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStackedItem value="http2" label="HTTP/2" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItem_Shadcn_>
|
||||
</RadioGroupStacked>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
<div className="px-content space-y-6">
|
||||
<LogDrainFormItem
|
||||
value="url"
|
||||
label="Endpoint URL"
|
||||
formControl={form.control}
|
||||
placeholder="https://example.com/log-drain"
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="http"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout layout="horizontal" label="HTTP Version">
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStacked onValueChange={field.onChange} value={field.value}>
|
||||
<FormItem_Shadcn_ asChild>
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStackedItem value="http1" label="HTTP/1" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItem_Shadcn_>
|
||||
<FormItem_Shadcn_ asChild>
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStackedItem value="http2" label="HTTP/2" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItem_Shadcn_>
|
||||
</RadioGroupStacked>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <FormField_Shadcn_
|
||||
control={form.control}
|
||||
@@ -309,38 +325,57 @@ export function LogDrainDestinationSheetForm({
|
||||
)}
|
||||
/> */}
|
||||
|
||||
<div>
|
||||
<FormLabel_Shadcn_>Custom Headers</FormLabel_Shadcn_>
|
||||
{hasHeaders &&
|
||||
Object.keys(headers || {})?.map((headerKey) => (
|
||||
<div
|
||||
className="flex hover:bg-background-alternative text-sm text-foreground items-center font-mono border-b p-1.5 group"
|
||||
key={headerKey}
|
||||
>
|
||||
<div className="w-full px-1">{headerKey}</div>
|
||||
<div className="w-full px-1 truncate" title={headers?.[headerKey]}>
|
||||
{headers?.[headerKey]}
|
||||
<div className="space-y-2">
|
||||
<div className="px-content">
|
||||
<FormLabel_Shadcn_>Custom Headers</FormLabel_Shadcn_>
|
||||
<p className="text-xs text-foreground-lighter">
|
||||
Set custom headers when draining logs to the Endpoint URL
|
||||
</p>
|
||||
</div>
|
||||
<div className="divide-y border-y">
|
||||
{hasHeaders &&
|
||||
Object.keys(headers || {})?.map((headerKey) => (
|
||||
<div
|
||||
className="flex text-sm px-content text-foreground items-center font-mono py-1.5 group"
|
||||
key={headerKey}
|
||||
>
|
||||
<div className="w-full">{headerKey}</div>
|
||||
<div className="w-full truncate" title={headers?.[headerKey]}>
|
||||
{headers?.[headerKey]}
|
||||
</div>
|
||||
<Button
|
||||
className="justify-self-end opacity-0 group-hover:opacity-100 w-7"
|
||||
type="text"
|
||||
title="Remove"
|
||||
icon={<TrashIcon />}
|
||||
onClick={() => removeHeader(headerKey)}
|
||||
></Button>
|
||||
</div>
|
||||
<Button
|
||||
className="justify-self-end opacity-0 group-hover:opacity-100"
|
||||
type="text"
|
||||
title="Remove"
|
||||
icon={<TrashIcon />}
|
||||
onClick={() => removeHeader(headerKey)}
|
||||
></Button>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{type === 'datadog' && (
|
||||
<div className="grid gap-4">
|
||||
<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."
|
||||
description={
|
||||
<>
|
||||
The API Key obtained from the Datadog dashboard{' '}
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm underline text-foreground"
|
||||
href="https://app.datadoghq.com/organization-settings/api-keys"
|
||||
>
|
||||
here
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
name="region"
|
||||
@@ -369,22 +404,6 @@ export function LogDrainDestinationSheetForm({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{type === 'elastic' && (
|
||||
<div className="grid gap-4">
|
||||
<LogDrainFormItem value="url" label="Filebeat URL" formControl={form.control} />
|
||||
<LogDrainFormItem
|
||||
value="username"
|
||||
label="Username"
|
||||
formControl={form.control}
|
||||
/>
|
||||
<LogDrainFormItem
|
||||
type="password"
|
||||
value="password"
|
||||
label="Password"
|
||||
formControl={form.control}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
@@ -397,17 +416,23 @@ export function LogDrainDestinationSheetForm({
|
||||
e.stopPropagation()
|
||||
addHeader()
|
||||
}}
|
||||
className="flex gap-2 mt-2 items-center"
|
||||
className="flex gap-2 mt-4 items-center px-content"
|
||||
>
|
||||
<label className="sr-only" htmlFor="header-name">
|
||||
Header name
|
||||
</label>
|
||||
<Input_Shadcn_
|
||||
size={'tiny'}
|
||||
id="header-name"
|
||||
type="text"
|
||||
placeholder="x-header-name"
|
||||
value={newCustomHeader.name}
|
||||
onChange={(e) => setNewCustomHeader({ ...newCustomHeader, name: e.target.value })}
|
||||
/>
|
||||
<label className="sr-only" htmlFor="header-value">
|
||||
Header value
|
||||
</label>
|
||||
<Input_Shadcn_
|
||||
size={'tiny'}
|
||||
id="header-value"
|
||||
type="text"
|
||||
placeholder="Header value"
|
||||
value={newCustomHeader.value}
|
||||
@@ -421,7 +446,9 @@ export function LogDrainDestinationSheetForm({
|
||||
)}
|
||||
</SheetSection>
|
||||
|
||||
<SheetFooter className="p-4">
|
||||
<SheetSection></SheetSection>
|
||||
|
||||
<SheetFooter className="p-content">
|
||||
<Button form={FORM_ID} loading={isLoading} htmlType="submit" type="primary">
|
||||
Save destination
|
||||
</Button>
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
import { Computer, DogIcon, WebhookIcon } from 'lucide-react'
|
||||
import { BracesIcon, DogIcon } from 'lucide-react'
|
||||
|
||||
const iconProps = { size: 24, strokeWidth: 1.5, className: 'text-foreground-light' }
|
||||
|
||||
export const LOG_DRAIN_TYPES = [
|
||||
{
|
||||
value: 'webhook',
|
||||
name: 'Webhook',
|
||||
description: 'Forward logs to a custom HTTP endpoint',
|
||||
icon: <WebhookIcon {...iconProps} />,
|
||||
name: 'Endpoint',
|
||||
description: 'Forward logs as a POST request to a custom HTTP endpoint',
|
||||
icon: <BracesIcon {...iconProps} />,
|
||||
},
|
||||
{
|
||||
value: 'datadog',
|
||||
name: 'Data dog',
|
||||
name: 'Datadog',
|
||||
description: 'Datadog is a monitoring service for cloud-scale applications',
|
||||
icon: <DogIcon {...iconProps} />,
|
||||
},
|
||||
{
|
||||
value: 'elastic',
|
||||
name: 'Elastic Filebeat',
|
||||
description: 'Filebeat is a lightweight shipper for forwarding and centralizing log data',
|
||||
icon: <Computer {...iconProps} />,
|
||||
},
|
||||
] as const
|
||||
|
||||
export const LOG_DRAIN_SOURCE_VALUES = LOG_DRAIN_TYPES.map((source) => source.value)
|
||||
|
||||
// Added postgres and bigquery here because the generated api types require them but they're not available yet.
|
||||
// Once we add them they can be removed from this type and added to the LOG_DRAIN_TYPES array.
|
||||
export type LogDrainType = (typeof LOG_DRAIN_TYPES)[number]['value'] | 'postgres' | 'bigquery'
|
||||
export type LogDrainType =
|
||||
| (typeof LOG_DRAIN_TYPES)[number]['value']
|
||||
| 'postgres'
|
||||
| 'bigquery'
|
||||
| 'elastic'
|
||||
|
||||
export const DATADOG_REGIONS = [
|
||||
{
|
||||
@@ -61,12 +57,6 @@ export type LogDrainDatadogConfig = {
|
||||
region: string
|
||||
}
|
||||
|
||||
export type LogDrainElasticConfig = {
|
||||
url: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export type LogDrainWebhookConfig = {
|
||||
url: string
|
||||
}
|
||||
Reference in new issue
Block a user