Files
supabase/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx
T
Danny White 325a5a0da7 fix(studio): polish extension installation dialogs (#51463)
## Problem

The enable-extension dialog in #51440 needs clearer installation copy
and a consistent disclosure for custom-schema guidance. On narrow
screens, footer buttons touch and dialog content can scroll sideways.

## Solution

Polish the dialog on top of #51440:

- Explain fixed-schema requirements and hide custom-schema guidance when
the extension requires a specific schema.
- Share the disclosure interaction with the session-expired and
API-hardening dialogs through `ui-patterns/DialogDisclosure`, documented
with a live design-system example.
- Fix narrow-screen overflow and add vertical spacing to shared dialog
footers.
- Refine inline-code styling and update the PostGIS instructions and E2E
button locator.


| After |
| --- |
| <img width="852" height="710" alt="CleanShot 2026-10-09 at 12 17
47@2x"
src="https://github.com/user-attachments/assets/59f6f1e7-9884-4cdb-b678-62b7f88a8ec5"
/> |
| _Standard extension_ |
| <img width="836" height="1092" alt="52254"
src="https://github.com/user-attachments/assets/dbf53d3e-6583-4c3b-8f43-577e0723d9a7"
/> |
| _Standard extension expanded accordion_ |
| <img width="836" height="604" alt="CleanShot 2026-10-09 at 12 20
04@2x"
src="https://github.com/user-attachments/assets/c8e3961f-2683-440c-a2a2-48ccd53c4a17"
/> |
| _Extension with fixed schema_ |

## Review instructions

1. Open `/project/<ref>/database/extensions`. Turn on the toggle for a
disabled `postgres_fdw` extension. Expand **Install in a different
schema** and check its hover, keyboard focus and full-width click
target. Repeat with `pg_cron` or `pgmq`: the intro must say **must**,
and custom-schema guidance must be absent. Cancel without enabling.
2. At 320 px and 375 px, check the extension dialog's alignment,
inline-code rendering, wrapped SQL and stacked **Enable** / **Cancel**
spacing. Confirm the page does not scroll sideways.
3. On a platform project with the Data API enabled, open
`/project/<ref>/integrations/data_api/settings`. Under **Harden Data
API**, click **Harden Data API**. Expand both sections and check the
same trigger behaviour. Confirm completed sections still show their
check icons. Close without applying either action.
4. The session-expired dialog has no direct route. It is rendered by
`apps/studio/hooks/misc/withAuth.tsx` after authentication loading
exceeds 10 seconds. For repeatable local review, temporarily pass
`visible={true}` to its `SessionTimeoutModal` callsite, then open
`/projects`. Expand **Having trouble?** by clicking the text or chevron
and using Enter / Space. Check the help text and controls. Close the
dialog without signing out or clearing site data, then undo the
temporary prop change.
5. In the design system, open
[`/docs/components/dialog#optional-details`](https://design-system-git-dnywh-choreextension-dialog-polish-supabase.vercel.app/design-system/docs/components/dialog#optional-details).
Click **Export results**, then expand **What’s included** with the mouse
and keyboard. The demo imports the same `ui-patterns/DialogDisclosure`
used by Studio.
6. Open another dialog and an alert dialog at a narrow width. Check the
shared 8 px gap between stacked footer buttons. Review inline-code
examples on `/docs/components/typography` for the shared styling change.
2026-10-12 10:18:45 +11:00

172 lines
5.9 KiB
TypeScript

import { ident, safeSql } from '@supabase/pg-meta'
import { toast } from 'sonner'
import {
Button,
Card,
CardContent,
cn,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { CodeBlock } from 'ui-patterns/CodeBlock'
import {
DialogDisclosure,
DialogDisclosureContent,
DialogDisclosureTrigger,
} from 'ui-patterns/DialogDisclosure'
import { DocsButton } from '@/components/ui/DocsButton'
import { useDatabaseExtensionEnableMutation } from '@/data/database-extensions/database-extension-enable-mutation'
import { type DatabaseExtension } from '@/data/database-extensions/database-extensions-query'
import { useIsOrioleDb, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
const orioleExtCallOuts = ['vector', 'postgis']
interface EnableExtensionModalProps {
visible: boolean
extension: DatabaseExtension
onCancel: () => void
}
export const EnableExtensionModal = ({
visible,
extension,
onCancel,
}: EnableExtensionModalProps) => {
const isOrioleDb = useIsOrioleDb()
const { data: project } = useSelectedProjectQuery()
// [Joshen] Hard-coding pg_cron here as this is enforced on our end (Not via pg_available_extension_versions)
const defaultSchema =
extension.name === 'pg_cron' ? 'pg_catalog' : extension.default_version_schema
const hasFixedSchema = defaultSchema != null
const { mutate: enableExtension, isPending: isEnabling } = useDatabaseExtensionEnableMutation({
onSuccess: () => {
toast.success(`Extension "${extension.name}" is now enabled`)
onCancel()
},
onError: (error) => {
toast.error(`Failed to enable ${extension.name}: ${error.message}`)
},
})
const onConfirmEnable = async () => {
if (project === undefined) return console.error('Project is required')
enableExtension({
projectRef: project.ref,
connectionString: project?.connectionString,
schema: defaultSchema ?? 'extensions',
name: extension.name,
version: extension.default_version,
cascade: true,
})
}
return (
<Dialog
open={visible}
onOpenChange={(open: boolean) => {
if (!open) onCancel()
}}
>
<DialogContent size="small" className="min-w-0 overflow-hidden" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>Enable {extension.name}</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
{isOrioleDb && orioleExtCallOuts.includes(extension.name) && (
<Admonition
type="default"
title="Extension is limited by OrioleDB"
className="border-x-0 border-t-0 rounded-none"
>
<span className="block">
{extension.name} cannot be accelerated by indexes on tables that are using the
OrioleDB access method
</span>
<DocsButton abbrev={false} className="mt-2" href={`${DOCS_URL}`} />
</Admonition>
)}
<DialogSection className="flex flex-col gap-y-4">
<p className="text-sm text-foreground-light">
{hasFixedSchema
? 'This extension must be installed in the schema below.'
: 'This extension will be installed in the schema below.'}
</p>
<Card>
<CardContent className="divide-y text-sm p-0">
<div className="flex items-center justify-between px-3 py-2.5">
<p className="text-foreground-lighter">Extension</p>
<code className="text-code-inline">{extension.name}</code>
</div>
<div className="flex items-center justify-between px-3 py-2.5">
<p className="text-foreground-lighter">Schema</p>
<code data-testid="enable-extension-schema" className="text-code-inline">
{defaultSchema ?? 'extensions'}
</code>
</div>
</CardContent>
</Card>
</DialogSection>
{!hasFixedSchema && (
<>
<DialogSectionSeparator />
<DialogDisclosure>
<DialogDisclosureTrigger className="py-4 px-4 md:px-5">
<span>Install in a different schema</span>
</DialogDisclosureTrigger>
<DialogDisclosureContent>
<DialogSection className="pt-1 min-w-0 flex flex-col gap-y-3">
<div className="text-sm text-foreground-light flex flex-col gap-y-3">
<p>
We recommend using the <code className="text-code-inline">extensions</code>{' '}
schema to keep extension objects separate from your own tables.
</p>
<p>
To use another schema, run the following in the SQL Editor. Replace{' '}
<code className="text-code-inline">target_schema</code> with an existing
schema name.
</p>
</div>
<CodeBlock
language="pgsql"
hideLineNumbers
wrapLongLines
wrapperClassName={cn('min-w-0 max-w-full [&_pre]:px-3 [&_pre]:py-3')}
className="[&_code]:text-xs"
value={safeSql`create extension if not exists ${ident(extension.name)} schema target_schema;`}
/>
</DialogSection>
</DialogDisclosureContent>
</DialogDisclosure>
</>
)}
<DialogFooter>
<Button disabled={isEnabling} onClick={() => onCancel()}>
Cancel
</Button>
<Button variant="primary" loading={isEnabling} onClick={() => onConfirmEnable()}>
Enable
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}