LogDrains.tsx 7.9 KB

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