Extensions.tsx159 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import { isNull, partition } from 'lodash'
4import { AlertCircle, Search } from 'lucide-react'
5import { useEffect, useRef, useState } from 'react'
6import {
7 Card,
8 InputGroup,
9 InputGroupAddon,
10 InputGroupInput,
11 ShadowScrollArea,
12 Table,
13 TableBody,
14 TableCell,
15 TableHead,
16 TableHeader,
17 TableRow,
18} from 'ui'
19import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
20
21import { ExtensionRow } from './ExtensionRow'
22import { HIDDEN_EXTENSIONS, SEARCH_TERMS } from './Extensions.constants'
23import InformationBox from '@/components/ui/InformationBox'
24import { NoSearchResults } from '@/components/ui/NoSearchResults'
25import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
26import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
27import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
28import { onSearchInputEscape } from '@/lib/keyboard'
29import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
30import { useShortcut } from '@/state/shortcuts/useShortcut'
31
32export const Extensions = () => {
33 const { filter } = useParams()
34 const { data: project } = useSelectedProjectQuery()
35 const [filterString, setFilterString] = useState<string>('')
36 const searchInputRef = useRef<HTMLInputElement>(null)
37
38 useShortcut(
39 SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
40 () => {
41 searchInputRef.current?.focus()
42 searchInputRef.current?.select()
43 },
44 { label: 'Search extensions' }
45 )
46
47 useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
48 setFilterString('')
49 })
50
51 const { data = [], isPending: isLoading } = useDatabaseExtensionsQuery({
52 projectRef: project?.ref,
53 connectionString: project?.connectionString,
54 })
55
56 const visibleExtensions = data.filter((ext) => !HIDDEN_EXTENSIONS.includes(ext.name))
57 const extensions =
58 filterString.length === 0
59 ? visibleExtensions
60 : visibleExtensions.filter((ext) => {
61 const nameMatchesSearch = ext.name.toLowerCase().includes(filterString.toLowerCase())
62 const searchTermsMatchesSearch = (SEARCH_TERMS[ext.name] || []).some((x) =>
63 x.includes(filterString.toLowerCase())
64 )
65 return nameMatchesSearch || searchTermsMatchesSearch
66 })
67 const [enabledExtensions, disabledExtensions] = partition(
68 extensions,
69 (ext) => !isNull(ext.installed_version)
70 )
71
72 const { can: canUpdateExtensions, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
73 PermissionAction.TENANT_SQL_ADMIN_WRITE,
74 'extensions'
75 )
76
77 useEffect(() => {
78 if (filter !== undefined) setFilterString(filter as string)
79 }, [filter])
80
81 return (
82 <>
83 <div className="mb-4">
84 <InputGroup className="w-52">
85 <InputGroupInput
86 ref={searchInputRef}
87 size="tiny"
88 placeholder="Search for an extension"
89 value={filterString}
90 onChange={(e) => setFilterString(e.target.value)}
91 onKeyDown={onSearchInputEscape(filterString, setFilterString)}
92 />
93 <InputGroupAddon>
94 <Search />
95 </InputGroupAddon>
96 </InputGroup>
97 </div>
98
99 {isPermissionsLoaded && !canUpdateExtensions && (
100 <InformationBox
101 icon={<AlertCircle className="text-foreground-light" size={18} strokeWidth={2} />}
102 title="You need additional permissions to update database extensions"
103 />
104 )}
105
106 {isLoading ? (
107 <GenericSkeletonLoader />
108 ) : (
109 <Card>
110 <ShadowScrollArea stickyLastColumn>
111 <Table>
112 <TableHeader>
113 <TableRow>
114 <TableHead key="name">Name</TableHead>
115 <TableHead key="version" className="w-28">
116 Version
117 </TableHead>
118 <TableHead key="schema">Schema</TableHead>
119 <TableHead key="description" className="min-w-80">
120 Description
121 </TableHead>
122 <TableHead key="used-by">Used by</TableHead>
123 <TableHead key="links">Links</TableHead>
124 {/*
125 [Joshen] All these classes are just to make the last column sticky
126 I reckon we can pull these out into the Table component where we can declare
127 sticky columns via props, but we can do that if we start to have more tables
128 in the dashboard with sticky columns
129 */}
130 <TableHead key="enabled" className="px-0">
131 <div className="bg-200! px-4 w-full h-full flex items-center border-l">
132 Enabled
133 </div>
134 </TableHead>
135 </TableRow>
136 </TableHeader>
137 <TableBody>
138 {[...enabledExtensions, ...disabledExtensions].map((extension) => (
139 <ExtensionRow key={extension.name} extension={extension} />
140 ))}
141 {extensions.length === 0 && (
142 <TableRow>
143 <TableCell colSpan={7}>
144 <NoSearchResults
145 className="border-none p-0! bg-transparent"
146 searchString={filterString}
147 onResetFilter={() => setFilterString('')}
148 />
149 </TableCell>
150 </TableRow>
151 )}
152 </TableBody>
153 </Table>
154 </ShadowScrollArea>
155 </Card>
156 )}
157 </>
158 )
159}