LogDrains.tsx232 lines · main
1import { IS_PLATFORM, useFlag, useParams } from 'common'
2import { MoreHorizontal, TrashIcon } from 'lucide-react'
3import { cloneElement, useState } from 'react'
4import { toast } from 'sonner'
5import {
6 Button,
7 Card,
8 cn,
9 DropdownMenu,
10 DropdownMenuContent,
11 DropdownMenuItem,
12 DropdownMenuTrigger,
13 Table,
14 TableBody,
15 TableCell,
16 TableHead,
17 TableHeader,
18 TableRow,
19} from 'ui'
20import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
21import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
22
23import { LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants'
24import { LogDrainsCard } from './LogDrainsCard'
25import { LogDrainsEmpty } from './LogDrainsEmpty'
26import { VoteLink } from './VoteLink'
27import AlertError from '@/components/ui/AlertError'
28import { useDeleteLogDrainMutation } from '@/data/log-drains/delete-log-drain-mutation'
29import { LogDrainData, useLogDrainsQuery } from '@/data/log-drains/log-drains-query'
30import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
31import { useTrack } from '@/lib/telemetry/track'
32
33export function LogDrains({
34 onNewDrainClick,
35 onUpdateDrainClick: _onUpdateDrainClick,
36}: {
37 onNewDrainClick: (src: LogDrainType) => void
38 onUpdateDrainClick: (drain: LogDrainData) => void
39}) {
40 const { hasAccess: hasAccessToLogDrains, isLoading: isLoadingEntitlement } =
41 useCheckEntitlements('log_drains')
42 const track = useTrack()
43 const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
44 const [selectedLogDrain, setSelectedLogDrain] = useState<LogDrainData | null>(null)
45 const { ref } = useParams()
46 const {
47 data: logDrains,
48 isPending: isLoading,
49 error,
50 isError,
51 } = useLogDrainsQuery(
52 { ref },
53 {
54 enabled: hasAccessToLogDrains,
55 }
56 )
57 const sentryEnabled = useFlag('SentryLogDrain')
58 const s3Enabled = useFlag('S3logdrain')
59 const axiomEnabled = useFlag('axiomLogDrain')
60 const otlpEnabled = useFlag('otlpLogDrain')
61 const last9Enabled = useFlag('Last9LogDrain')
62 const syslogEnabled = useFlag('syslogLogDrain')
63 const hasLogDrains = !!logDrains?.length
64
65 const { mutate: deleteLogDrain } = useDeleteLogDrainMutation({
66 onSuccess: () => {
67 setIsDeleteModalOpen(false)
68 setSelectedLogDrain(null)
69 },
70 onError: () => {
71 setIsDeleteModalOpen(false)
72 setSelectedLogDrain(null)
73 toast.error('Failed to delete log drain')
74 },
75 })
76
77 if (isLoading || isLoadingEntitlement) {
78 return (
79 <div>
80 <GenericSkeletonLoader />
81 </div>
82 )
83 }
84
85 if (!isLoadingEntitlement && !hasAccessToLogDrains) {
86 return <LogDrainsEmpty />
87 }
88
89 if (isError) {
90 return <AlertError subject="Failed to load log drains" error={error}></AlertError>
91 }
92
93 if (!isLoading && !hasLogDrains) {
94 return (
95 <>
96 <div className="grid lg:grid-cols-3 gap-4">
97 {LOG_DRAIN_TYPES.filter((t) => {
98 if (t.value === 'sentry') return sentryEnabled
99 if (t.value === 's3') return s3Enabled
100 if (t.value === 'axiom') return axiomEnabled
101 if (t.value === 'otlp') return otlpEnabled
102 if (t.value === 'last9') return last9Enabled
103 if (t.value === 'syslog') return syslogEnabled
104 return true
105 }).map((src) => (
106 <LogDrainsCard
107 key={src.value}
108 title={src.name}
109 description={src.description}
110 icon={src.icon}
111 rightLabel={IS_PLATFORM ? 'Additional $60' : undefined}
112 onClick={() => {
113 onNewDrainClick(src.value)
114 }}
115 />
116 ))}
117 </div>
118 <VoteLink />
119 </>
120 )
121 }
122
123 return (
124 <>
125 <Card>
126 <Table>
127 <TableHeader>
128 <TableRow>
129 <TableHead className="max-w-[200px]">Name</TableHead>
130 <TableHead className="w-96">Description</TableHead>
131 <TableHead className="w-48">Destination</TableHead>
132 <TableHead className="text-right">
133 <div className="sr-only">Actions</div>
134 </TableHead>
135 </TableRow>
136 </TableHeader>
137 <TableBody>
138 {logDrains
139 ?.slice()
140 .sort((a, b) => b.id - a.id)
141 .map((drain) => (
142 <TableRow key={drain.id}>
143 <TableCell className="font-medium truncate max-w-72" title={drain.name}>
144 {drain.name}
145 </TableCell>
146 <TableCell
147 className={cn(
148 'truncate max-w-96',
149 drain.description ? 'text-foreground-light' : 'text-foreground-muted'
150 )}
151 title={drain.description}
152 >
153 {drain.description || '-'}
154 </TableCell>
155 <TableCell className="text-foreground-light">
156 <div className="flex items-center gap-2">
157 {LOG_DRAIN_TYPES.find((t) => t.value === drain.type)?.icon && (
158 <span className="text-foreground-light">
159 {cloneElement(LOG_DRAIN_TYPES.find((t) => t.value === drain.type)!.icon, {
160 height: 16,
161 width: 16,
162 })}
163 </span>
164 )}
165 <span className="truncate max-w-40">
166 {LOG_DRAIN_TYPES.find((t) => t.value === drain.type)?.name ?? drain.type}
167 </span>
168 </div>
169 </TableCell>
170 <TableCell className="text-right">
171 <DropdownMenu>
172 <DropdownMenuTrigger asChild>
173 <Button
174 type="text"
175 className="px-1 opacity-50 hover:opacity-100 bg-transparent! shrink-0"
176 icon={<MoreHorizontal />}
177 />
178 </DropdownMenuTrigger>
179 <DropdownMenuContent className="max-w-[140px]" align="end">
180 {/* Jordi: Updating log drains is disabled temporarily.
181 <DropdownMenuItem
182 onClick={() => {
183 onUpdateDrainClick(drain)
184 }}
185 >
186 <Pencil className="h-4 w-4 mr-2" />
187 Update
188 </DropdownMenuItem> */}
189 <DropdownMenuItem
190 onClick={() => {
191 setSelectedLogDrain(drain)
192 setIsDeleteModalOpen(true)
193 }}
194 >
195 <TrashIcon className="h-4 w-4 mr-2" />
196 Delete
197 </DropdownMenuItem>
198 </DropdownMenuContent>
199 </DropdownMenu>
200 </TableCell>
201 </TableRow>
202 ))}
203 </TableBody>
204
205 <ConfirmationModal
206 confirmLabel="Delete"
207 variant="destructive"
208 title="Delete Log Drain"
209 visible={isDeleteModalOpen}
210 onConfirm={() => {
211 if (selectedLogDrain && ref) {
212 deleteLogDrain({ token: selectedLogDrain.token, projectRef: ref })
213 track('log_drain_removed', {
214 destination: selectedLogDrain.type,
215 })
216 }
217 }}
218 onCancel={() => setIsDeleteModalOpen(false)}
219 >
220 <div className="text-foreground-light text-sm">
221 <p>
222 Are you sure you want to delete{' '}
223 <span className="text-foreground">{selectedLogDrain?.name}</span>?
224 </p>
225 <p>This action cannot be undone.</p>
226 </div>
227 </ConfirmationModal>
228 </Table>
229 </Card>
230 </>
231 )
232}