log-drains.tsx268 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { IS_PLATFORM, useFlag, useParams } from 'common'
3import { ChevronDown } from 'lucide-react'
4import { cloneElement, useState } from 'react'
5import { toast } from 'sonner'
6import {
7 Alert,
8 Button,
9 DropdownMenu,
10 DropdownMenuContent,
11 DropdownMenuItem,
12 DropdownMenuTrigger,
13} from 'ui'
14import { GenericSkeletonLoader } from 'ui-patterns'
15import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
16
17import { LogDrainDestinationSheetForm } from '@/components/interfaces/LogDrains/LogDrainDestinationSheetForm'
18import { LogDrains } from '@/components/interfaces/LogDrains/LogDrains'
19import {
20 LOG_DRAIN_TYPES,
21 LogDrainType,
22} from '@/components/interfaces/LogDrains/LogDrains.constants'
23import DefaultLayout from '@/components/layouts/DefaultLayout'
24import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
25import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout'
26import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
27import { DocsButton } from '@/components/ui/DocsButton'
28import {
29 LogDrainCreateVariables,
30 useCreateLogDrainMutation,
31} from '@/data/log-drains/create-log-drain-mutation'
32import { LogDrainData, useLogDrainsQuery } from '@/data/log-drains/log-drains-query'
33import { useUpdateLogDrainMutation } from '@/data/log-drains/update-log-drain-mutation'
34import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
35import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
36import { DOCS_URL } from '@/lib/constants'
37import type { NextPageWithLayout } from '@/types'
38
39const LogDrainsSettings: NextPageWithLayout = () => {
40 const { can: canManageLogDrains, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
41 PermissionAction.ANALYTICS_ADMIN_WRITE,
42 'logflare'
43 )
44
45 const [open, setOpen] = useState(false)
46 const { ref } = useParams() as { ref: string }
47 const [selectedLogDrain, setSelectedLogDrain] = useState<Partial<LogDrainData> | null>(null)
48 const [isCreateConfirmModalOpen, setIsCreateConfirmModalOpen] = useState(false)
49 const [pendingLogDrainValues, setPendingLogDrainValues] =
50 useState<LogDrainCreateVariables | null>(null)
51 const [mode, setMode] = useState<'create' | 'update'>('create')
52
53 const { hasAccess: hasAccessToLogDrains, isLoading: isLoadingEntitlement } =
54 useCheckEntitlements('log_drains')
55
56 const sentryEnabled = useFlag('SentryLogDrain')
57 const s3Enabled = useFlag('S3logdrain')
58 const axiomEnabled = useFlag('axiomLogDrain')
59 const otlpEnabled = useFlag('otlpLogDrain')
60 const last9Enabled = useFlag('Last9LogDrain')
61 const syslogEnabled = useFlag('syslogLogDrain')
62
63 const { data: logDrains } = useLogDrainsQuery(
64 { ref },
65 { enabled: !isLoadingEntitlement && hasAccessToLogDrains }
66 )
67
68 const { mutate: createLogDrain, isPending: createLoading } = useCreateLogDrainMutation({
69 onSuccess: () => {
70 toast.success('Log drain destination created')
71 setIsCreateConfirmModalOpen(false)
72 setOpen(false)
73 },
74 onError: () => {
75 toast.error('Failed to create log drain')
76 setIsCreateConfirmModalOpen(false)
77 setOpen(false)
78 },
79 })
80
81 const { mutate: updateLogDrain, isPending: updateLoading } = useUpdateLogDrainMutation({
82 onSuccess: () => {
83 toast.success('Log drain updated')
84 setOpen(false)
85 },
86 onError: () => {
87 setOpen(false)
88 toast.error('Failed to update log drain')
89 },
90 })
91
92 const isLoading = createLoading || updateLoading
93
94 function handleUpdateClick(drain: LogDrainData) {
95 setSelectedLogDrain(drain)
96 setMode('update')
97 setOpen(true)
98 }
99
100 function handleNewClick(src: LogDrainType) {
101 setSelectedLogDrain({ type: src })
102 setMode('create')
103 setOpen(true)
104 }
105
106 const content = (
107 <ScaffoldSection isFullWidth id="log-drains" className="gap-6">
108 <ScaffoldContainer className="flex flex-col gap-10" bottomPadding>
109 <LogDrainDestinationSheetForm
110 mode={mode}
111 open={open}
112 onOpenChange={(v) => {
113 if (!v) {
114 setSelectedLogDrain(null)
115 }
116 setOpen(v)
117 }}
118 defaultValues={{
119 ...selectedLogDrain,
120 type: selectedLogDrain?.type ? selectedLogDrain.type : 'webhook',
121 }}
122 isLoading={isLoading}
123 onSubmit={({ name, description, type, ...values }) => {
124 const logDrainValues = {
125 name,
126 description: description || '',
127 type,
128 config: values as any, // TODO: fix generated API types from backend
129 id: selectedLogDrain?.id,
130 projectRef: ref,
131 token: selectedLogDrain?.token,
132 }
133
134 if (mode === 'create') {
135 setPendingLogDrainValues(logDrainValues)
136 setIsCreateConfirmModalOpen(true)
137 } else {
138 if (!logDrainValues.id || !selectedLogDrain?.token) {
139 throw new Error('Log drain ID and token is required')
140 } else {
141 updateLogDrain(logDrainValues)
142 }
143 }
144 }}
145 />
146
147 {isLoadingPermissions ? (
148 <GenericSkeletonLoader />
149 ) : !canManageLogDrains ? (
150 <Alert variant="default">You do not have permission to manage log drains</Alert>
151 ) : (
152 <LogDrains onUpdateDrainClick={handleUpdateClick} onNewDrainClick={handleNewClick} />
153 )}
154 </ScaffoldContainer>
155
156 <ConfirmationModal
157 confirmLabel="Add destination"
158 variant="default"
159 title="Confirm Log Drain Creation"
160 visible={isCreateConfirmModalOpen}
161 onConfirm={() => {
162 if (pendingLogDrainValues) {
163 createLogDrain(pendingLogDrainValues)
164 setPendingLogDrainValues(null)
165 }
166 setIsCreateConfirmModalOpen(false)
167 }}
168 onCancel={() => {
169 setIsCreateConfirmModalOpen(false)
170 setPendingLogDrainValues(null)
171 }}
172 >
173 <div className="text-foreground-light text-sm space-y-2">
174 <p>
175 You are about to create a new log drain destination:{' '}
176 <span className="text-foreground">{pendingLogDrainValues?.name}</span>
177 </p>
178 {IS_PLATFORM && (
179 <p>
180 This will incur an additional <span className="text-foreground">$60 per month</span>{' '}
181 charge to your subscription.
182 </p>
183 )}
184 <p>Are you sure you want to proceed?</p>
185 </div>
186 </ConfirmationModal>
187 </ScaffoldSection>
188 )
189
190 // [kemal]: Ordinarily <PageLayout /> would be bundled with the getLayout function below, however in this case we need access to some bits for the "Add destination" button to render as part of the in-built page header in <PageLayout />.
191 if (!isLoadingEntitlement && hasAccessToLogDrains) {
192 return (
193 <PageLayout
194 title="log drains"
195 subtitle="send your project logs to third party destinations"
196 primaryActions={
197 <>
198 {!(logDrains?.length === 0) && (
199 <div className="flex items-center">
200 <Button
201 disabled={!hasAccessToLogDrains || !canManageLogDrains}
202 onClick={() => {
203 setSelectedLogDrain(null)
204 setMode('create')
205 setOpen(true)
206 }}
207 type="primary"
208 className="rounded-r-none px-3"
209 >
210 Add destination
211 </Button>
212 <DropdownMenu>
213 <DropdownMenuTrigger asChild>
214 <Button
215 type="primary"
216 title="Choose token scope"
217 className="rounded-l-none px-[4px] py-[5px]"
218 icon={<ChevronDown />}
219 />
220 </DropdownMenuTrigger>
221 <DropdownMenuContent align="end" side="bottom">
222 {LOG_DRAIN_TYPES.filter((t) => {
223 if (t.value === 'sentry') return sentryEnabled
224 if (t.value === 's3') return s3Enabled
225 if (t.value === 'axiom') return axiomEnabled
226 if (t.value === 'otlp') return otlpEnabled
227 if (t.value === 'last9') return last9Enabled
228 if (t.value === 'syslog') return syslogEnabled
229 return true
230 }).map((drainType) => (
231 <DropdownMenuItem
232 key={drainType.value}
233 onClick={() => handleNewClick(drainType.value)}
234 >
235 <div className="flex items-center gap-3">
236 {cloneElement(drainType.icon, { height: 16, width: 16 })}
237 <div className="space-y-1">
238 <p className="block text-foreground">{drainType.name}</p>
239 {IS_PLATFORM && (
240 <p className="text-xs text-foreground-lighter">Additional $60</p>
241 )}
242 </div>
243 </div>
244 </DropdownMenuItem>
245 ))}
246 </DropdownMenuContent>
247 </DropdownMenu>
248 </div>
249 )}
250 </>
251 }
252 secondaryActions={<DocsButton href={`${DOCS_URL}/guides/platform/log-drains`} />}
253 >
254 {content}
255 </PageLayout>
256 )
257 }
258
259 return content
260}
261
262LogDrainsSettings.getLayout = (page) => (
263 <DefaultLayout>
264 <SettingsLayout title="log drains">{page}</SettingsLayout>
265 </DefaultLayout>
266)
267
268export default LogDrainsSettings