Files
supabase/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx
T
Joshen Lim 09a1f431bc Mass update for all lucide icon imports to use the component with Icon suffix (#51659)
## Context

This is largely just a mechanical change here:
Opting to use lucide icons with the `Icon` suffix across all icon
imports within `studio`
- so that it’s clearer which components are icons + reduces the
possibility of name collision with our own components
- e.g <User /> vs <UserIcon />

Also added a eslint rule for this:
<img width="1069" height="167" alt="image"
src="https://github.com/user-attachments/assets/7f36f443-5ca2-4bf6-8d22-86e774513bfc"
/>

And updated `AGENTS.md` as such:
```
Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced).
```
2026-10-14 12:56:55 +02:00

151 lines
5.1 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { isNull, partition } from 'lodash'
import { AlertCircleIcon, SearchIcon } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import {
Card,
InputGroup,
InputGroupAddon,
InputGroupInput,
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>
<SearchIcon />
</InputGroupAddon>
</InputGroup>
</div>
{isPermissionsLoaded && !canUpdateExtensions && (
<InformationBox
icon={<AlertCircleIcon className="text-foreground-light" size={18} strokeWidth={2} />}
title="You need additional permissions to update database extensions"
/>
)}
{isLoading ? (
<GenericSkeletonLoader />
) : (
<Card>
<Table containerProps={{ stickyLastColumn: true }}>
<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>
<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>
</Card>
)}
</>
)
}