| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import { isNull, partition } from 'lodash'
- import { AlertCircle, Search } from 'lucide-react'
- import { useEffect, useRef, useState } from 'react'
- import {
- Card,
- InputGroup,
- InputGroupAddon,
- InputGroupInput,
- ShadowScrollArea,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- } from 'ui'
- import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
- import { ExtensionRow } from './ExtensionRow'
- import { HIDDEN_EXTENSIONS, SEARCH_TERMS } from './Extensions.constants'
- import InformationBox from '@/components/ui/InformationBox'
- import { NoSearchResults } from '@/components/ui/NoSearchResults'
- import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { onSearchInputEscape } from '@/lib/keyboard'
- import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
- import { useShortcut } from '@/state/shortcuts/useShortcut'
- export const Extensions = () => {
- const { filter } = useParams()
- const { data: project } = useSelectedProjectQuery()
- const [filterString, setFilterString] = useState<string>('')
- const searchInputRef = useRef<HTMLInputElement>(null)
- useShortcut(
- SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
- () => {
- searchInputRef.current?.focus()
- searchInputRef.current?.select()
- },
- { label: 'Search extensions' }
- )
- useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
- setFilterString('')
- })
- const { data = [], isPending: isLoading } = useDatabaseExtensionsQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const visibleExtensions = data.filter((ext) => !HIDDEN_EXTENSIONS.includes(ext.name))
- const extensions =
- filterString.length === 0
- ? visibleExtensions
- : visibleExtensions.filter((ext) => {
- const nameMatchesSearch = ext.name.toLowerCase().includes(filterString.toLowerCase())
- const searchTermsMatchesSearch = (SEARCH_TERMS[ext.name] || []).some((x) =>
- x.includes(filterString.toLowerCase())
- )
- return nameMatchesSearch || searchTermsMatchesSearch
- })
- const [enabledExtensions, disabledExtensions] = partition(
- extensions,
- (ext) => !isNull(ext.installed_version)
- )
- const { can: canUpdateExtensions, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
- PermissionAction.TENANT_SQL_ADMIN_WRITE,
- 'extensions'
- )
- useEffect(() => {
- if (filter !== undefined) setFilterString(filter as string)
- }, [filter])
- return (
- <>
- <div className="mb-4">
- <InputGroup className="w-52">
- <InputGroupInput
- ref={searchInputRef}
- size="tiny"
- placeholder="Search for an extension"
- value={filterString}
- onChange={(e) => setFilterString(e.target.value)}
- onKeyDown={onSearchInputEscape(filterString, setFilterString)}
- />
- <InputGroupAddon>
- <Search />
- </InputGroupAddon>
- </InputGroup>
- </div>
- {isPermissionsLoaded && !canUpdateExtensions && (
- <InformationBox
- icon={<AlertCircle className="text-foreground-light" size={18} strokeWidth={2} />}
- title="You need additional permissions to update database extensions"
- />
- )}
- {isLoading ? (
- <GenericSkeletonLoader />
- ) : (
- <Card>
- <ShadowScrollArea stickyLastColumn>
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead key="name">Name</TableHead>
- <TableHead key="version" className="w-28">
- Version
- </TableHead>
- <TableHead key="schema">Schema</TableHead>
- <TableHead key="description" className="min-w-80">
- Description
- </TableHead>
- <TableHead key="used-by">Used by</TableHead>
- <TableHead key="links">Links</TableHead>
- {/*
- [Joshen] All these classes are just to make the last column sticky
- I reckon we can pull these out into the Table component where we can declare
- sticky columns via props, but we can do that if we start to have more tables
- in the dashboard with sticky columns
- */}
- <TableHead key="enabled" className="px-0">
- <div className="bg-200! px-4 w-full h-full flex items-center border-l">
- Enabled
- </div>
- </TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {[...enabledExtensions, ...disabledExtensions].map((extension) => (
- <ExtensionRow key={extension.name} extension={extension} />
- ))}
- {extensions.length === 0 && (
- <TableRow>
- <TableCell colSpan={7}>
- <NoSearchResults
- className="border-none p-0! bg-transparent"
- searchString={filterString}
- onResetFilter={() => setFilterString('')}
- />
- </TableCell>
- </TableRow>
- )}
- </TableBody>
- </Table>
- </ShadowScrollArea>
- </Card>
- )}
- </>
- )
- }
|