LogDrains.tsx232 lines · main
| 1 | import { IS_PLATFORM, useFlag, useParams } from 'common' |
| 2 | import { MoreHorizontal, TrashIcon } from 'lucide-react' |
| 3 | import { cloneElement, useState } from 'react' |
| 4 | import { toast } from 'sonner' |
| 5 | import { |
| 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' |
| 20 | import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' |
| 21 | import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' |
| 22 | |
| 23 | import { LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants' |
| 24 | import { LogDrainsCard } from './LogDrainsCard' |
| 25 | import { LogDrainsEmpty } from './LogDrainsEmpty' |
| 26 | import { VoteLink } from './VoteLink' |
| 27 | import AlertError from '@/components/ui/AlertError' |
| 28 | import { useDeleteLogDrainMutation } from '@/data/log-drains/delete-log-drain-mutation' |
| 29 | import { LogDrainData, useLogDrainsQuery } from '@/data/log-drains/log-drains-query' |
| 30 | import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' |
| 31 | import { useTrack } from '@/lib/telemetry/track' |
| 32 | |
| 33 | export 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 | } |