mirror of
https://github.com/supabase/supabase.git
synced 2026-07-05 07:14:28 +08:00
Log Drains UI (#28044)
* add main log drain form and navigation * fix submit and select * add mutation and update form * fix submit issue * fix issues with validation * fix typeerrs * add log drain table * update create log drain mutation * add delete log drain mutation * update get log drains query * fix new log drain click in cards * add delete mutation * add delete log drain * refactor gzip switch * refactor radiogroup to use formfield * add headers form * fix validation, custom headers errors * refactor to support update in form * format * add log drains to nav * update api spec and list logdrains query * wire backend * fix datadog region values * make api input password * fix url elastic * fix typerr * rm unnecessary value setter * fix state issue log drains update form * Add default values setting in useEffect to fix form issues * format * Update LogDrains table header width to 250px * add upgrade plan card when free plan * fix dumb if statement * fix the freaking headers * fix upgrade to team state * fix plan check and loading state * disable type update * Add link to documentation in Log Drains settings * show add destination only after empty state * fix bug with inputs not resetting * add gzip tooltip * rm command * defaultValue to value * add defaultValue * rm consolelog * rm anys
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
import { useParams } from 'common'
|
||||
import { DATADOG_REGIONS, LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants'
|
||||
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import {
|
||||
Button,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormMessage_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
RadioGroupStacked,
|
||||
RadioGroupStackedItem,
|
||||
Switch,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetSection,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
Select_Shadcn_,
|
||||
SelectContent_Shadcn_,
|
||||
SelectGroup_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectLabel_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
SelectValue_Shadcn_,
|
||||
FormItem_Shadcn_,
|
||||
FormLabel_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
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 { TrashIcon } from 'lucide-react'
|
||||
import { LogDrainData } from 'data/log-drains/log-drains-query'
|
||||
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||||
|
||||
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'),
|
||||
httpVersion: z.enum(['HTTP1', 'HTTP2']),
|
||||
gzip: z.boolean(),
|
||||
headers: z.record(z.string(), z.string()),
|
||||
}),
|
||||
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'),
|
||||
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' }),
|
||||
}),
|
||||
])
|
||||
|
||||
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,
|
||||
}: {
|
||||
value: string
|
||||
label: string
|
||||
formControl: any
|
||||
placeholder?: string
|
||||
description?: string
|
||||
type?: string
|
||||
}) {
|
||||
return (
|
||||
<FormField_Shadcn_
|
||||
name={value}
|
||||
control={formControl}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout layout="horizontal" label={label} description={description || ''}>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ type={type || 'text'} placeholder={placeholder} {...field} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function LogDrainDestinationSheetForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
defaultValues,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
mode,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (v: boolean) => void
|
||||
defaultValues?: Partial<LogDrainData> & { type: LogDrainType }
|
||||
isLoading?: boolean
|
||||
onSubmit: (values: z.infer<typeof formSchema>) => void
|
||||
mode: 'create' | 'update'
|
||||
}) {
|
||||
const defaultType = defaultValues?.type || 'webhook'
|
||||
const [newCustomHeader, setNewCustomHeader] = useState({ name: '', value: '' })
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
type: defaultType,
|
||||
...defaultValues,
|
||||
},
|
||||
})
|
||||
|
||||
const headers = form.watch('headers')
|
||||
|
||||
// IDK why but this useEffect is needed to set the default values
|
||||
// Suppossedly if you call form.reset() defaultValues is not cached, but I have found it to be very unreliable, and sometimes the form will open with the wrong default values.
|
||||
// - Jordi
|
||||
useEffect(() => {
|
||||
if (form.formState.isDirty) return
|
||||
|
||||
if (mode === 'update') {
|
||||
form.setValue('type', defaultType)
|
||||
form.setValue('name', defaultValues?.name || '')
|
||||
form.setValue('url', defaultValues?.config?.url || '')
|
||||
form.setValue('api_key', defaultValues?.config?.api_key || '')
|
||||
form.setValue('region', defaultValues?.config?.region || '')
|
||||
form.setValue('username', defaultValues?.config?.username || '')
|
||||
form.setValue('password', defaultValues?.config?.password || '')
|
||||
form.setValue('httpVersion', defaultValues?.config?.httpVersion || 'HTTP2')
|
||||
form.setValue('gzip', defaultValues?.config?.gzip || true)
|
||||
form.setValue('headers', defaultValues?.config?.headers || {})
|
||||
form.setValue('description', defaultValues?.description || '')
|
||||
} else {
|
||||
form.reset()
|
||||
}
|
||||
}, [defaultType, form, defaultValues, mode])
|
||||
|
||||
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
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
form.reset()
|
||||
onOpenChange(v)
|
||||
}}
|
||||
>
|
||||
<SheetContent tabIndex={undefined} showClose={false} size="lg" className="overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Add destination</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetSection>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={FORM_ID}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
form.handleSubmit(onSubmit)(e)
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<LogDrainFormItem
|
||||
value="name"
|
||||
placeholder="My Destination"
|
||||
label="Name"
|
||||
formControl={form.control}
|
||||
/>
|
||||
<LogDrainFormItem
|
||||
value="description"
|
||||
placeholder="My Destination"
|
||||
label="Description"
|
||||
formControl={form.control}
|
||||
/>
|
||||
<div>
|
||||
{mode === 'update' && (
|
||||
<div className="flex items-center gap-2 text-sm text-foreground-light">
|
||||
<InfoTooltip align="start">
|
||||
You cannot change the type of an existing log drain. Please, create a new
|
||||
log drain with the new type.
|
||||
</InfoTooltip>
|
||||
Can't update log drain type
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<RadioGroupStacked
|
||||
className={cn(mode === 'update' && 'opacity-50')}
|
||||
value={type}
|
||||
onValueChange={(v: LogDrainType) => form.setValue('type', v)}
|
||||
disabled={mode === 'update'}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mt-6">
|
||||
{type === 'webhook' && (
|
||||
<>
|
||||
<LogDrainFormItem
|
||||
value="url"
|
||||
label="Webhook URL"
|
||||
formControl={form.control}
|
||||
placeholder="https://example.com/webhooks/log-drain"
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="httpVersion"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_>
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStacked
|
||||
defaultValue="HTTP2"
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormItem_Shadcn_ asChild>
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStackedItem value="HTTP1" label="HTTP1" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItem_Shadcn_>
|
||||
<FormItem_Shadcn_ asChild>
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStackedItem value="HTTP2" label="HTTP2" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItem_Shadcn_>
|
||||
</RadioGroupStacked>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="gzip"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="space-y-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<FormControl_Shadcn_>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl_Shadcn_>
|
||||
<FormLabel_Shadcn_ className="text-base">Gzip</FormLabel_Shadcn_>
|
||||
<InfoTooltip align="start">
|
||||
Gzip can reduce the size of the payload and increase the speed of the
|
||||
request,
|
||||
<br /> but it will increase the CPU usage of the destination.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<Button
|
||||
className="justify-self-end opacity-0 group-hover:opacity-100"
|
||||
type="text"
|
||||
title="Remove"
|
||||
icon={<TrashIcon />}
|
||||
onClick={() => removeHeader(headerKey)}
|
||||
></Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{type === 'datadog' && (
|
||||
<div className="grid gap-4">
|
||||
<LogDrainFormItem
|
||||
type="password"
|
||||
value="api_key"
|
||||
label="API Key"
|
||||
formControl={form.control}
|
||||
description="The API Key obtained from the Datadog dashboard."
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
name="region"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout layout="horizontal" label={'Region'}>
|
||||
<FormControl_Shadcn_>
|
||||
<Select_Shadcn_ value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger_Shadcn_ className="col-span-3">
|
||||
<SelectValue_Shadcn_ placeholder="Select a region" />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
<SelectGroup_Shadcn_>
|
||||
<SelectLabel_Shadcn_>Region</SelectLabel_Shadcn_>
|
||||
{DATADOG_REGIONS.map((reg) => (
|
||||
<SelectItem_Shadcn_ key={reg.value} value={reg.value}>
|
||||
{reg.label}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectGroup_Shadcn_>
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</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_>
|
||||
|
||||
{/* This form needs to be outside the <Form_Shadcn_> */}
|
||||
{type === 'webhook' && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
addHeader()
|
||||
}}
|
||||
className="flex gap-2 mt-2 items-center"
|
||||
>
|
||||
<Input_Shadcn_
|
||||
size={'tiny'}
|
||||
type="text"
|
||||
placeholder="x-header-name"
|
||||
value={newCustomHeader.name}
|
||||
onChange={(e) => setNewCustomHeader({ ...newCustomHeader, name: e.target.value })}
|
||||
/>
|
||||
<Input_Shadcn_
|
||||
size={'tiny'}
|
||||
type="text"
|
||||
placeholder="Header value"
|
||||
value={newCustomHeader.value}
|
||||
onChange={(e) => setNewCustomHeader({ ...newCustomHeader, value: e.target.value })}
|
||||
/>
|
||||
|
||||
<Button htmlType="submit" type="outline">
|
||||
Add
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</SheetSection>
|
||||
|
||||
<SheetFooter className="p-4">
|
||||
<Button form={FORM_ID} loading={isLoading} htmlType="submit" type="primary">
|
||||
Save destination
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Computer, DogIcon, WebhookIcon } 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} />,
|
||||
},
|
||||
{
|
||||
value: 'datadog',
|
||||
name: 'Data dog',
|
||||
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)
|
||||
export type LogDrainType = (typeof LOG_DRAIN_TYPES)[number]['value']
|
||||
|
||||
export const DATADOG_REGIONS = [
|
||||
{
|
||||
label: 'AP1',
|
||||
value: 'AP1',
|
||||
},
|
||||
{
|
||||
label: 'EU',
|
||||
value: 'EU',
|
||||
},
|
||||
{
|
||||
label: 'US1',
|
||||
value: 'US1',
|
||||
},
|
||||
{
|
||||
label: 'US1-FED',
|
||||
value: 'US1-FED',
|
||||
},
|
||||
{
|
||||
label: 'US3',
|
||||
value: 'US3',
|
||||
},
|
||||
{
|
||||
label: 'US5',
|
||||
value: 'US5',
|
||||
},
|
||||
] as const
|
||||
|
||||
export type LogDrainDatadogConfig = {
|
||||
api_key: string
|
||||
region: string
|
||||
}
|
||||
|
||||
export type LogDrainElasticConfig = {
|
||||
url: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export type LogDrainWebhookConfig = {
|
||||
url: string
|
||||
}
|
||||
185
apps/studio/components/interfaces/LogDrains/LogDrains.tsx
Normal file
185
apps/studio/components/interfaces/LogDrains/LogDrains.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { LogDrainData, useLogDrainsQuery } from 'data/log-drains/log-drains-query'
|
||||
import { LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants'
|
||||
import { useParams } from 'common'
|
||||
import CardButton from 'components/ui/CardButton'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from 'ui'
|
||||
import { MoreHorizontal, Pencil, TrashIcon } from 'lucide-react'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { useState } from 'react'
|
||||
import { useDeleteLogDrainMutation } from 'data/log-drains/delete-log-drain-mutation'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
|
||||
import Link from 'next/link'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
|
||||
export function LogDrains({
|
||||
onNewDrainClick,
|
||||
onUpdateDrainClick,
|
||||
}: {
|
||||
onNewDrainClick: (src: LogDrainType) => void
|
||||
onUpdateDrainClick: (drain: LogDrainData) => void
|
||||
}) {
|
||||
const org = useSelectedOrganization()
|
||||
|
||||
const { isLoading: orgPlanLoading, plan } = useCurrentOrgPlan()
|
||||
const logDrainsEnabled = !orgPlanLoading && (plan?.id === 'team' || plan?.id === 'enterprise')
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||
const [selectedLogDrain, setSelectedLogDrain] = useState<LogDrainData | null>(null)
|
||||
const { ref } = useParams()
|
||||
const {
|
||||
data: logDrains,
|
||||
isLoading,
|
||||
refetch,
|
||||
error,
|
||||
isError,
|
||||
} = useLogDrainsQuery(
|
||||
{ ref },
|
||||
{
|
||||
enabled: logDrainsEnabled,
|
||||
}
|
||||
)
|
||||
const { mutate: deleteLogDrain } = useDeleteLogDrainMutation({
|
||||
onSuccess: () => {
|
||||
setIsDeleteModalOpen(false)
|
||||
setSelectedLogDrain(null)
|
||||
},
|
||||
onError: () => {
|
||||
setIsDeleteModalOpen(false)
|
||||
setSelectedLogDrain(null)
|
||||
toast.error('Failed to delete log drain')
|
||||
},
|
||||
})
|
||||
|
||||
if (!orgPlanLoading && !logDrainsEnabled) {
|
||||
return (
|
||||
<CardButton title="Upgrade to Pro" description="Upgrade to a Team Plan to use Log Drains">
|
||||
<Button className="mt-2" asChild>
|
||||
<Link href={`/org/${org?.slug}/billing`}>Upgrade to Pro</Link>
|
||||
</Button>
|
||||
</CardButton>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading || orgPlanLoading) {
|
||||
return (
|
||||
<div>
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isLoading && logDrains?.length === 0) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{LOG_DRAIN_TYPES.map((src) => (
|
||||
<CardButton
|
||||
key={src.value}
|
||||
title={src.name}
|
||||
description={src.description}
|
||||
icon={src.icon}
|
||||
onClick={() => {
|
||||
onNewDrainClick(src.value)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <AlertError error={error}></AlertError>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel className="">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="max-w-[200px]">Name</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead className="text-right">Source</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<div className="sr-only">Actions</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{logDrains?.map((drain) => (
|
||||
<TableRow key={drain.id}>
|
||||
<TableCell className="font-medium">{drain.name}</TableCell>
|
||||
<TableCell>{drain.description}</TableCell>
|
||||
<TableCell className="text-right font-mono">{drain.type}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1 opacity-50 hover:opacity-100 !bg-transparent"
|
||||
icon={<MoreHorizontal />}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="max-w-[140px]" align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
onUpdateDrainClick(drain)
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
Update
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedLogDrain(drain)
|
||||
setIsDeleteModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
<ConfirmationModal
|
||||
title="Delete Log Drain"
|
||||
visible={isDeleteModalOpen}
|
||||
onConfirm={() => {
|
||||
if (selectedLogDrain && ref) {
|
||||
deleteLogDrain({ token: selectedLogDrain.token, projectRef: ref })
|
||||
}
|
||||
}}
|
||||
onCancel={() => setIsDeleteModalOpen(false)}
|
||||
>
|
||||
<div className="text-foreground-light">
|
||||
<p>
|
||||
Are you sure you want to delete{' '}
|
||||
<span className="text-foreground">{selectedLogDrain?.name}</span>?
|
||||
</p>
|
||||
<p>This action cannot be undone.</p>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
</Table>
|
||||
</Panel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -47,6 +47,7 @@ const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutPro
|
||||
])
|
||||
|
||||
const warehouseEnabled = useFlag('warehouse')
|
||||
const logDrainsEnabled = useFlag('logdrains')
|
||||
|
||||
const menuRoutes = generateSettingsMenu(ref, project, organization, {
|
||||
auth: authEnabled,
|
||||
@@ -54,6 +55,7 @@ const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutPro
|
||||
storage: storageEnabled,
|
||||
invoices: invoicesEnabled,
|
||||
warehouse: warehouseEnabled,
|
||||
logDrains: logDrainsEnabled,
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,6 +13,7 @@ export const generateSettingsMenu = (
|
||||
storage?: boolean
|
||||
invoices?: boolean
|
||||
warehouse?: boolean
|
||||
logDrains?: boolean
|
||||
}
|
||||
): ProductMenuGroup[] => {
|
||||
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
|
||||
@@ -22,6 +23,7 @@ export const generateSettingsMenu = (
|
||||
const edgeFunctionsEnabled = features?.edgeFunctions ?? true
|
||||
const storageEnabled = features?.storage ?? true
|
||||
const warehouseEnabled = features?.warehouse ?? false
|
||||
const logDrainsEnabled = features?.logDrains ?? false
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -121,6 +123,16 @@ export const generateSettingsMenu = (
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(IS_PLATFORM && logDrainsEnabled
|
||||
? [
|
||||
{
|
||||
name: `Log Drains`,
|
||||
key: `log-drains`,
|
||||
url: `/project/${ref}/settings/log-drains`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
62
apps/studio/data/log-drains/create-log-drain-mutation.ts
Normal file
62
apps/studio/data/log-drains/create-log-drain-mutation.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { logDrainsKeys } from './keys'
|
||||
import { LogDrainType } from 'components/interfaces/LogDrains/LogDrains.constants'
|
||||
|
||||
export type LogDrainCreateVariables = {
|
||||
projectRef: string
|
||||
name: string
|
||||
config: Record<string, never>
|
||||
type: LogDrainType
|
||||
}
|
||||
|
||||
export async function createLogDrain(payload: LogDrainCreateVariables) {
|
||||
const { data, error } = await post('/platform/projects/{ref}/analytics/log-drains', {
|
||||
params: { path: { ref: payload.projectRef } },
|
||||
body: {
|
||||
name: payload.name,
|
||||
type: payload.type,
|
||||
config: payload.config,
|
||||
},
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type LogDrainCreateData = Awaited<ReturnType<typeof createLogDrain>>
|
||||
|
||||
export const useCreateLogDrainMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<LogDrainCreateData, ResponseError, LogDrainCreateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<LogDrainCreateData, ResponseError, LogDrainCreateVariables>(
|
||||
(vars) => createLogDrain(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await queryClient.invalidateQueries(logDrainsKeys.list(projectRef))
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to mutate: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
55
apps/studio/data/log-drains/delete-log-drain-mutation.ts
Normal file
55
apps/studio/data/log-drains/delete-log-drain-mutation.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { handleError, del } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { logDrainsKeys } from './keys'
|
||||
|
||||
export type LogDrainDeleteVariables = {
|
||||
projectRef: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export async function deleteLogDrain({ projectRef, token }: LogDrainDeleteVariables) {
|
||||
// @ts-ignore Just sample, TS lint will validate if the endpoint is valid
|
||||
const { data, error } = await del('/platform/projects/{ref}/analytics/log-drains/{token}', {
|
||||
params: { path: { ref: projectRef, token } },
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type LogDrainDeleteData = Awaited<ReturnType<typeof deleteLogDrain>>
|
||||
|
||||
export const useDeleteLogDrainMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<LogDrainDeleteData, ResponseError, LogDrainDeleteVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<LogDrainDeleteData, ResponseError, LogDrainDeleteVariables>(
|
||||
(vars) => deleteLogDrain(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await queryClient.invalidateQueries(logDrainsKeys.list(projectRef))
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to mutate: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
3
apps/studio/data/log-drains/keys.ts
Normal file
3
apps/studio/data/log-drains/keys.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const logDrainsKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'log-drains'] as const,
|
||||
}
|
||||
43
apps/studio/data/log-drains/log-drains-query.ts
Normal file
43
apps/studio/data/log-drains/log-drains-query.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { logDrainsKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type LogDrainsVariables = {
|
||||
ref?: string
|
||||
}
|
||||
|
||||
export async function getLogDrains({ ref }: LogDrainsVariables, signal?: AbortSignal) {
|
||||
if (!ref) {
|
||||
throw new Error('ref is required')
|
||||
}
|
||||
|
||||
const { data, error } = await get(`/platform/projects/{ref}/analytics/log-drains`, {
|
||||
params: { path: { ref: ref } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
handleError(error)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export type LogDrainsData = Awaited<ReturnType<typeof getLogDrains>>
|
||||
export type LogDrainData = LogDrainsData[number]
|
||||
export type LogDrainsyError = ResponseError
|
||||
|
||||
export const useLogDrainsQuery = <TData = LogDrainsData>(
|
||||
{ ref }: LogDrainsVariables,
|
||||
{ enabled, ...options }: UseQueryOptions<LogDrainsData, LogDrainsyError, TData> = {}
|
||||
) =>
|
||||
useQuery<LogDrainsData, LogDrainsyError, TData>(
|
||||
logDrainsKeys.list(ref),
|
||||
({ signal }) => getLogDrains({ ref }, signal),
|
||||
{
|
||||
enabled: enabled && !!ref,
|
||||
refetchOnMount: false,
|
||||
...options,
|
||||
}
|
||||
)
|
||||
68
apps/studio/data/log-drains/update-log-drain-mutation.ts
Normal file
68
apps/studio/data/log-drains/update-log-drain-mutation.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { handleError, put } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { logDrainsKeys } from './keys'
|
||||
import { LogDrainType } from 'components/interfaces/LogDrains/LogDrains.constants'
|
||||
|
||||
export type LogDrainUpdateVariables = {
|
||||
projectRef: string
|
||||
token?: string
|
||||
name: string
|
||||
description?: string
|
||||
type: LogDrainType
|
||||
config: Record<string, never>
|
||||
}
|
||||
|
||||
export async function updateLogDrain(payload: LogDrainUpdateVariables) {
|
||||
if (!payload.token) {
|
||||
throw new Error('Token is required')
|
||||
}
|
||||
|
||||
const { data, error } = await put('/platform/projects/{ref}/analytics/log-drains/{token}', {
|
||||
params: { path: { ref: payload.projectRef, token: payload.token } },
|
||||
body: {
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
config: payload.config,
|
||||
},
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type LogDrainUpdateData = Awaited<ReturnType<typeof updateLogDrain>>
|
||||
|
||||
export const useUpdateLogDrainMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<LogDrainUpdateData, ResponseError, LogDrainUpdateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<LogDrainUpdateData, ResponseError, LogDrainUpdateVariables>(
|
||||
(vars) => updateLogDrain(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await queryClient.invalidateQueries(logDrainsKeys.list(projectRef))
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to mutate: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
23
apps/studio/hooks/misc/useCurrentOrgPlan.ts
Normal file
23
apps/studio/hooks/misc/useCurrentOrgPlan.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useSelectedOrganization } from './useSelectedOrganization'
|
||||
|
||||
export function useCurrentOrgPlan() {
|
||||
const currentOrg = useSelectedOrganization()
|
||||
const { data, isLoading, isSuccess } = useOrgSubscriptionQuery({
|
||||
orgSlug: currentOrg?.slug,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return {
|
||||
plan: null,
|
||||
isLoading,
|
||||
isSuccess: false,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
plan: data?.plan,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
}
|
||||
}
|
||||
}
|
||||
153
apps/studio/pages/project/[ref]/settings/log-drains.tsx
Normal file
153
apps/studio/pages/project/[ref]/settings/log-drains.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDescription,
|
||||
ScaffoldHeader,
|
||||
ScaffoldTitle,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { LogDrains } from 'components/interfaces/LogDrains/LogDrains'
|
||||
import { LogDrainDestinationSheetForm } from 'components/interfaces/LogDrains/LogDrainDestinationSheetForm'
|
||||
import { Button } from 'ui'
|
||||
import { useState } from 'react'
|
||||
import { LogDrainType } from 'components/interfaces/LogDrains/LogDrains.constants'
|
||||
import { LogDrainData, useLogDrainsQuery } from 'data/log-drains/log-drains-query'
|
||||
import { useCreateLogDrainMutation } from 'data/log-drains/create-log-drain-mutation'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useUpdateLogDrainMutation } from 'data/log-drains/update-log-drain-mutation'
|
||||
import { useParams } from 'common'
|
||||
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
|
||||
import Link from 'next/link'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
|
||||
const LogDrainsSettings: NextPageWithLayout = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { ref } = useParams() as { ref: string }
|
||||
const [selectedLogDrain, setSelectedLogDrain] = useState<Partial<LogDrainData> | null>(null)
|
||||
const [mode, setMode] = useState<'create' | 'update'>('create')
|
||||
|
||||
const { plan, isLoading: planLoading } = useCurrentOrgPlan()
|
||||
|
||||
const logDrainsEnabled = !planLoading && (plan?.id === 'team' || plan?.id === 'enterprise')
|
||||
|
||||
const { data: logDrains } = useLogDrainsQuery(
|
||||
{ ref },
|
||||
{
|
||||
enabled: logDrainsEnabled,
|
||||
}
|
||||
)
|
||||
|
||||
const { mutate: createLogDrain, isLoading: createLoading } = useCreateLogDrainMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('Log drain destination created')
|
||||
setOpen(false)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to create log drain')
|
||||
setOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: updateLogDrain, isLoading: updateLoading } = useUpdateLogDrainMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('Log drain destination updated')
|
||||
setOpen(false)
|
||||
},
|
||||
onError: () => {
|
||||
setOpen(false)
|
||||
toast.error('Failed to update log drain')
|
||||
},
|
||||
})
|
||||
|
||||
const isLoading = createLoading || updateLoading
|
||||
|
||||
function handleUpdateClick(drain: LogDrainData) {
|
||||
setSelectedLogDrain(drain)
|
||||
setMode('update')
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
function handleNewClick(src: LogDrainType) {
|
||||
setSelectedLogDrain({ type: src })
|
||||
setMode('create')
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldHeader className="flex flex-row justify-between">
|
||||
<div>
|
||||
<ScaffoldTitle>Log Drains</ScaffoldTitle>
|
||||
<ScaffoldDescription>
|
||||
Send your project logs to third party destinations
|
||||
</ScaffoldDescription>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button type="default" icon={<ExternalLink strokeWidth={1.5} />} asChild>
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://supabase.com/docs/guides/platform/log-drains"
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
</Button>
|
||||
{!(logDrains?.length === 0) && (
|
||||
<Button
|
||||
disabled={!logDrainsEnabled}
|
||||
onClick={() => {
|
||||
setSelectedLogDrain(null)
|
||||
setMode('create')
|
||||
setOpen(true)
|
||||
}}
|
||||
type="primary"
|
||||
>
|
||||
Add destination
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</ScaffoldHeader>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldContainer className="flex flex-col gap-10" bottomPadding>
|
||||
<LogDrainDestinationSheetForm
|
||||
mode={mode}
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
if (!v) {
|
||||
setSelectedLogDrain(null)
|
||||
}
|
||||
setOpen(v)
|
||||
}}
|
||||
defaultValues={selectedLogDrain as any}
|
||||
isLoading={isLoading}
|
||||
onSubmit={({ name, type, ...values }) => {
|
||||
const logDrainValues = {
|
||||
name,
|
||||
type,
|
||||
config: values as any, // TODO: fix generated API types from backend
|
||||
id: selectedLogDrain?.id,
|
||||
projectRef: ref,
|
||||
token: selectedLogDrain?.token,
|
||||
}
|
||||
|
||||
if (mode === 'create') {
|
||||
createLogDrain(logDrainValues)
|
||||
} else {
|
||||
if (!logDrainValues.id || !selectedLogDrain?.token) {
|
||||
throw new Error('Log drain ID and token is required')
|
||||
} else {
|
||||
console.log('logDrainValues', logDrainValues)
|
||||
updateLogDrain(logDrainValues)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<LogDrains onUpdateDrainClick={handleUpdateClick} onNewDrainClick={handleNewClick} />
|
||||
</ScaffoldContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
LogDrainsSettings.getLayout = (page) => <SettingsLayout title="Log Drains">{page}</SettingsLayout>
|
||||
export default LogDrainsSettings
|
||||
246
packages/api-types/types/api.d.ts
vendored
246
packages/api-types/types/api.d.ts
vendored
@@ -665,6 +665,18 @@ export interface paths {
|
||||
/** Gets project's usage api requests count */
|
||||
get: operations['UsageApiController_getApiRequestsCount']
|
||||
}
|
||||
'/platform/projects/{ref}/analytics/log-drains': {
|
||||
/** Lists all log drains */
|
||||
get: operations['LogDrainController_listLogDrains']
|
||||
/** Create a log drain */
|
||||
post: operations['LogDrainController_createLogDrain']
|
||||
}
|
||||
'/platform/projects/{ref}/analytics/log-drains/{token}': {
|
||||
/** Update a log drain */
|
||||
put: operations['LogDrainController_updateLogDrain']
|
||||
/** Delete a log drain */
|
||||
delete: operations['LogDrainController_deleteLogDrain']
|
||||
}
|
||||
'/platform/projects/{ref}/analytics/warehouse/access-tokens': {
|
||||
/** Lists project's warehouse access tokens from logflare */
|
||||
get: operations['v1-list-all-warehouse-tokens']
|
||||
@@ -983,10 +995,6 @@ export interface paths {
|
||||
/** Sets up a payment method */
|
||||
post: operations['SetupIntentController_setUpPaymentMethod']
|
||||
}
|
||||
'/platform/telemetry/activity': {
|
||||
/** Sends server activity */
|
||||
post: operations['TelemetryActivityController_sendServerActivity']
|
||||
}
|
||||
'/platform/telemetry/event': {
|
||||
/** Sends analytics server event */
|
||||
post: operations['TelemetryEventController_sendServerEvent']
|
||||
@@ -999,10 +1007,6 @@ export interface paths {
|
||||
/** Send server page event */
|
||||
post: operations['TelemetryPageController_sendServerPage']
|
||||
}
|
||||
'/platform/telemetry/pageview': {
|
||||
/** Send pageview event */
|
||||
post: operations['TelemetryPageviewController_sendServerPageViewed']
|
||||
}
|
||||
'/platform/tos/fly': {
|
||||
/** Redirects to Fly sso flow */
|
||||
get: operations['TermsOfServiceController_flyTosAccepted']
|
||||
@@ -1492,6 +1496,18 @@ export interface paths {
|
||||
/** Gets project's usage api requests count */
|
||||
get: operations['UsageApiController_getApiRequestsCount']
|
||||
}
|
||||
'/v0/projects/{ref}/analytics/log-drains': {
|
||||
/** Lists all log drains */
|
||||
get: operations['LogDrainController_listLogDrains']
|
||||
/** Create a log drain */
|
||||
post: operations['LogDrainController_createLogDrain']
|
||||
}
|
||||
'/v0/projects/{ref}/analytics/log-drains/{token}': {
|
||||
/** Update a log drain */
|
||||
put: operations['LogDrainController_updateLogDrain']
|
||||
/** Delete a log drain */
|
||||
delete: operations['LogDrainController_deleteLogDrain']
|
||||
}
|
||||
'/v0/projects/{ref}/analytics/warehouse/access-tokens': {
|
||||
/** Lists project's warehouse access tokens from logflare */
|
||||
get: operations['v1-list-all-warehouse-tokens']
|
||||
@@ -2509,6 +2525,13 @@ export interface components {
|
||||
partner_billing: components['schemas']['AwsPartnerBillingBody']
|
||||
primary_email: string
|
||||
}
|
||||
CreateBackendParams: {
|
||||
config: Record<string, never>
|
||||
description?: string
|
||||
name: string
|
||||
/** @enum {string} */
|
||||
type: 'postgres' | 'bigquery' | 'webhook' | 'datadog' | 'elastic'
|
||||
}
|
||||
CreateBranchBody: {
|
||||
branch_name: string
|
||||
git_branch?: string
|
||||
@@ -2852,16 +2875,8 @@ export interface components {
|
||||
custom_origin_server: string
|
||||
hostname: string
|
||||
id: string
|
||||
ownership_verification: {
|
||||
name?: string
|
||||
type?: string
|
||||
value?: string
|
||||
}
|
||||
ssl: {
|
||||
status?: string
|
||||
validation_errors?: components['schemas']['ValidationError'][]
|
||||
validation_records?: components['schemas']['ValidationRecord'][]
|
||||
}
|
||||
ownership_verification: components['schemas']['OwnershipVerification']
|
||||
ssl: components['schemas']['SslValidation']
|
||||
status: string
|
||||
verification_errors?: string[]
|
||||
}
|
||||
@@ -3614,6 +3629,20 @@ export interface components {
|
||||
scopes: string
|
||||
token: string
|
||||
}
|
||||
LFBackend: {
|
||||
config: Record<string, never>
|
||||
description?: string
|
||||
id: number
|
||||
metadata: {
|
||||
project_ref?: string
|
||||
type?: string
|
||||
}
|
||||
name: string
|
||||
token: string
|
||||
/** @enum {string} */
|
||||
type: 'postgres' | 'bigquery' | 'webhook' | 'datadog' | 'elastic'
|
||||
user_id: number
|
||||
}
|
||||
LFEndpoint: {
|
||||
cache_duration_seconds: number
|
||||
description: string
|
||||
@@ -3948,11 +3977,10 @@ export interface components {
|
||||
organization_id: number
|
||||
overdue_invoice_count: number
|
||||
}
|
||||
PageBody: {
|
||||
location: string
|
||||
path: string
|
||||
referrer?: string
|
||||
title?: string
|
||||
OwnershipVerification: {
|
||||
name: string
|
||||
type: string
|
||||
value: string
|
||||
}
|
||||
PasswordCheckBody: {
|
||||
password: string
|
||||
@@ -4755,9 +4783,11 @@ export interface components {
|
||||
}
|
||||
RestrictionData: {
|
||||
grace_period_end?: string
|
||||
report_date?: string
|
||||
/** @enum {string} */
|
||||
restrictions?: 'drop_requests_402'
|
||||
usage_stats?: components['schemas']['UsageStats']
|
||||
violation_data?: Record<string, never>
|
||||
violations?: (
|
||||
| 'exceed_db_size_quota'
|
||||
| 'exceed_egress_quota'
|
||||
@@ -5035,6 +5065,11 @@ export interface components {
|
||||
SslEnforcements: {
|
||||
database: boolean
|
||||
}
|
||||
SslValidation: {
|
||||
status: string
|
||||
validation_errors?: components['schemas']['ValidationError'][]
|
||||
validation_records: components['schemas']['ValidationRecord'][]
|
||||
}
|
||||
StorageBucket: {
|
||||
created_at: string
|
||||
id: string
|
||||
@@ -5210,14 +5245,6 @@ export interface components {
|
||||
TaxIdV2Response: {
|
||||
tax_id: components['schemas']['TaxIdV2'] | null
|
||||
}
|
||||
TelemetryActivityBody: {
|
||||
activity: string
|
||||
data?: Record<string, never>
|
||||
orgSlug?: string
|
||||
page: components['schemas']['PageBody']
|
||||
projectRef?: string
|
||||
source: string
|
||||
}
|
||||
TelemetryEventBody: {
|
||||
action: string
|
||||
category: string
|
||||
@@ -5238,14 +5265,6 @@ export interface components {
|
||||
route?: string
|
||||
title: string
|
||||
}
|
||||
TelemetryPageviewBody: {
|
||||
location: string
|
||||
orgSlug?: string
|
||||
path: string
|
||||
projectRef?: string
|
||||
referrer: string
|
||||
title: string
|
||||
}
|
||||
ThirdPartyAuth: {
|
||||
custom_jwks?: unknown
|
||||
id: string
|
||||
@@ -5447,6 +5466,11 @@ export interface components {
|
||||
smtp_user?: string
|
||||
uri_allow_list?: string
|
||||
}
|
||||
UpdateBackendParams: {
|
||||
config?: Record<string, never>
|
||||
description?: string
|
||||
name?: string
|
||||
}
|
||||
UpdateBranchBody: {
|
||||
branch_name?: string
|
||||
git_branch?: string
|
||||
@@ -10725,6 +10749,110 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Lists all log drains */
|
||||
LogDrainController_listLogDrains: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['LFBackend'][]
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to fetch log drains */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Create a log drain */
|
||||
LogDrainController_createLogDrain: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateBackendParams']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': components['schemas']['LFBackend']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to create a log drain */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Update a log drain */
|
||||
LogDrainController_updateLogDrain: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
/** @description Log drains token */
|
||||
token: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UpdateBackendParams']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['LFBackend']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to update log drain */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Delete a log drain */
|
||||
LogDrainController_deleteLogDrain: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
/** @description Log drains token */
|
||||
token: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
204: {
|
||||
content: never
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to delete a log drain */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Lists project's warehouse access tokens from logflare */
|
||||
'v1-list-all-warehouse-tokens': {
|
||||
parameters: {
|
||||
@@ -12680,23 +12808,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Sends server activity */
|
||||
TelemetryActivityController_sendServerActivity: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['TelemetryActivityBody']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to send server activity */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Sends analytics server event */
|
||||
TelemetryEventController_sendServerEvent: {
|
||||
requestBody: {
|
||||
@@ -12748,23 +12859,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Send pageview event */
|
||||
TelemetryPageviewController_sendServerPageViewed: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['TelemetryPageviewBody']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to send pageview event */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Redirects to Fly sso flow */
|
||||
TermsOfServiceController_flyTosAccepted: {
|
||||
parameters: {
|
||||
@@ -14742,6 +14836,9 @@ export interface operations {
|
||||
201: {
|
||||
content: never
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to remove read replica */
|
||||
500: {
|
||||
content: never
|
||||
@@ -14765,6 +14862,9 @@ export interface operations {
|
||||
201: {
|
||||
content: never
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to set up read replica */
|
||||
500: {
|
||||
content: never
|
||||
|
||||
@@ -186,6 +186,10 @@
|
||||
"label": "Storage settings",
|
||||
"url": "/project/_/settings/storage"
|
||||
},
|
||||
{
|
||||
"label": "Log Drains",
|
||||
"url": "/project/_/settings/log-drains"
|
||||
},
|
||||
{
|
||||
"label": "Custom Domains",
|
||||
"url": "/project/_/settings/general#custom-domains"
|
||||
|
||||
Reference in New Issue
Block a user