ExtensionRow.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { AlertTriangle, Book, Github, Loader2 } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { useState } from 'react'
  5. import { extensions } from 'shared-data'
  6. import { toast } from 'sonner'
  7. import { Button, Switch, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  8. import { Admonition } from 'ui-patterns'
  9. import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
  10. import { EnableExtensionModal } from './EnableExtensionModal'
  11. import { EXTENSION_DISABLE_WARNINGS } from './Extensions.constants'
  12. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  13. import { useDatabaseExtensionDisableMutation } from '@/data/database-extensions/database-extension-disable-mutation'
  14. import { DatabaseExtension } from '@/data/database-extensions/database-extensions-query'
  15. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  16. import { useIsOrioleDb, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  17. import { DOCS_URL } from '@/lib/constants'
  18. interface ExtensionRowProps {
  19. extension: DatabaseExtension
  20. }
  21. export const ExtensionRow = ({ extension }: ExtensionRowProps) => {
  22. const { data: project } = useSelectedProjectQuery()
  23. const isOn = extension.installed_version !== null
  24. const isOrioleDb = useIsOrioleDb()
  25. const [isDisableModalOpen, setIsDisableModalOpen] = useState(false)
  26. const [showConfirmEnableModal, setShowConfirmEnableModal] = useState(false)
  27. const { can: canUpdateExtensions } = useAsyncCheckPermissions(
  28. PermissionAction.TENANT_SQL_ADMIN_WRITE,
  29. 'extensions'
  30. )
  31. const orioleDbCheck = isOrioleDb && extension.name === 'orioledb'
  32. const disabled = !canUpdateExtensions || orioleDbCheck
  33. const extensionMeta = extensions.find((item) => item.name === extension.name)
  34. const docsUrl = extensionMeta?.link.startsWith('/guides')
  35. ? `${DOCS_URL}${extensionMeta?.link}`
  36. : (extensionMeta?.link ?? undefined)
  37. const { mutate: disableExtension, isPending: isDisabling } = useDatabaseExtensionDisableMutation({
  38. onSuccess: () => {
  39. toast.success(`${extension.name} is off.`)
  40. setIsDisableModalOpen(false)
  41. },
  42. })
  43. const onConfirmDisable = () => {
  44. if (project === undefined) return console.error('Project is required')
  45. disableExtension({
  46. projectRef: project.ref,
  47. connectionString: project.connectionString,
  48. id: extension.name,
  49. })
  50. }
  51. return (
  52. <>
  53. <TableRow>
  54. <TableCell>
  55. <div className="flex items-center gap-x-2">
  56. <span title={extension.name} className="truncate inline-block max-w-48">
  57. {extension.name}
  58. </span>
  59. {extensionMeta?.deprecated && extensionMeta?.deprecated.length > 0 && (
  60. <ButtonTooltip
  61. type="warning"
  62. icon={<AlertTriangle />}
  63. className="rounded-full"
  64. tooltip={{
  65. content: {
  66. text: `The extension is deprecated and will be removed in ${extensionMeta.deprecated.join(', ')}.`,
  67. },
  68. }}
  69. >
  70. Deprecated
  71. </ButtonTooltip>
  72. )}
  73. </div>
  74. </TableCell>
  75. <TableCell className="w-28 font-mono tracking-tighter">
  76. {extension?.installed_version ?? extension.default_version}
  77. </TableCell>
  78. <TableCell className="truncate">{isOn ? extension.schema : '-'}</TableCell>
  79. <TableCell className="text-foreground-light">
  80. <p className="block" title={extension.comment ?? undefined}>
  81. {extension.comment}
  82. </p>
  83. </TableCell>
  84. <TableCell>
  85. {extensionMeta?.product ? (
  86. <div className="flex flex-col gap-1">
  87. {extensionMeta.product_url ? (
  88. <Link
  89. href={extensionMeta.product_url.replace('{ref}', project?.ref ?? '')}
  90. className="transition hover:text-foreground"
  91. >
  92. {extensionMeta.product}
  93. </Link>
  94. ) : (
  95. <span>{extensionMeta.product}</span>
  96. )}
  97. {!isOn && (
  98. <span className="text-foreground-lighter text-xs">
  99. Install extension to use {extensionMeta.product}
  100. </span>
  101. )}
  102. </div>
  103. ) : (
  104. <span className="text-foreground-lighter">-</span>
  105. )}
  106. </TableCell>
  107. <TableCell>
  108. <div className="flex gap-2 items-center">
  109. {extensionMeta?.github_url && (
  110. <Button asChild type="default" icon={<Github />} className="rounded-full">
  111. <a
  112. target="_blank"
  113. rel="noreferrer"
  114. href={extensionMeta.github_url}
  115. className="font-mono tracking-tighter"
  116. >
  117. {extensionMeta.github_url.split('/').slice(-2).join('/')}
  118. </a>
  119. </Button>
  120. )}
  121. {docsUrl !== undefined && (
  122. <Button asChild type="default" icon={<Book />} className="rounded-full">
  123. <a
  124. target="_blank"
  125. rel="noreferrer"
  126. className="font-mono tracking-tighter"
  127. href={docsUrl}
  128. >
  129. Docs
  130. </a>
  131. </Button>
  132. )}
  133. </div>
  134. </TableCell>
  135. {/*
  136. [Joshen] The div child here and all these classes is to properly add a left border
  137. to make the sticky column more distinct
  138. */}
  139. <TableCell className="w-20 sticky bg-surface-100 right-0 relative">
  140. <div className="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center border-l">
  141. {isDisabling ? (
  142. <Loader2 className="animate-spin" size={16} />
  143. ) : (
  144. <Tooltip>
  145. <TooltipTrigger>
  146. <Switch
  147. disabled={disabled}
  148. checked={isOn}
  149. onCheckedChange={() =>
  150. isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
  151. }
  152. />
  153. </TooltipTrigger>
  154. {disabled && (
  155. <TooltipContent side="bottom">
  156. {!canUpdateExtensions
  157. ? 'You need additional permissions to toggle extensions'
  158. : orioleDbCheck
  159. ? 'Project is using OrioleDB and cannot be disabled'
  160. : null}
  161. </TooltipContent>
  162. )}
  163. </Tooltip>
  164. )}
  165. </div>
  166. </TableCell>
  167. </TableRow>
  168. <EnableExtensionModal
  169. visible={showConfirmEnableModal}
  170. extension={extension}
  171. onCancel={() => setShowConfirmEnableModal(false)}
  172. />
  173. <ConfirmationModal
  174. visible={isDisableModalOpen}
  175. title="Confirm to disable extension"
  176. confirmLabel="Disable"
  177. variant="destructive"
  178. confirmLabelLoading="Disabling"
  179. loading={isDisabling}
  180. onCancel={() => setIsDisableModalOpen(false)}
  181. onConfirm={() => onConfirmDisable()}
  182. >
  183. <div className="flex flex-col gap-y-3">
  184. <p className="text-sm text-foreground-light">
  185. Are you sure you want to turn OFF the "{extension.name}" extension?
  186. </p>
  187. {EXTENSION_DISABLE_WARNINGS[extension.name] && (
  188. <Admonition type="warning">{EXTENSION_DISABLE_WARNINGS[extension.name]}</Admonition>
  189. )}
  190. </div>
  191. </ConfirmationModal>
  192. </>
  193. )
  194. }