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