mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
## What kind of change does this PR introduce? Bug fix. Resolves DEPR-539. ## What is the current behavior? When a focused child unmounts, Radix can move focus to the Sheet wrapper and break the expected tab order. Several callsites suppress the wrapper's tabindex individually. ## What is the new behavior? Sheet still focuses its first interactive child when opened, but the wrapper itself is no longer focusable by default. Callers can opt in with an explicit `tabIndex` when needed. ## Additional context ### Testing Compare this Studio experience on both this branch and `master`: 1. Open any project with an Edge Function. 2. Go to **Edge Functions**, open the function, then click **Test**. 3. Under **Headers**, click **Add Headers**. Click the first header key input, then Tab slowly through the header inputs and remove buttons. On `master`, focus can jump to the whole Sheet. On this branch, focus stays on the controls in order. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved keyboard focus behavior across sheets and panels. * Sheets now focus the first available interactive element when opened, without adding unnecessary focus targets. * Preserved support for programmatic focus and prevented focus from unexpectedly moving to the sheet when focused content is removed. * Updated authentication, integrations, connection, logging, storage, and other sheet interfaces consistently. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
208 lines
7.5 KiB
TypeScript
208 lines
7.5 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import Link from 'next/link'
|
|
import { parseAsBoolean, useQueryState } from 'nuqs'
|
|
import { useState } from 'react'
|
|
import { Button, Sheet, SheetContent } from 'ui'
|
|
import { Admonition } from 'ui-patterns/Admonition'
|
|
|
|
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
|
|
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
|
|
import { useAvailableIntegrations } from '../Landing/useAvailableIntegrations'
|
|
import { CreateIcebergWrapperSheet } from './CreateIcebergWrapperSheet'
|
|
import { CreateWrapperSheet } from './CreateWrapperSheet'
|
|
import { WRAPPERS } from './Wrappers.constants'
|
|
import { WrapperTable } from './WrapperTable'
|
|
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
|
import { getServiceVersionsPath } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersions.utils'
|
|
import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
|
|
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
|
|
|
|
const WrapperOverviewContent = () => {
|
|
const { id } = useParams()
|
|
const wrapperMeta = WRAPPERS.find((w) => w.name === id)
|
|
|
|
const [isDirty, setIsDirty] = useState(false)
|
|
const [createWrapperShown, setCreateWrapperShown] = useQueryState(
|
|
'new',
|
|
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
|
|
)
|
|
|
|
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
|
checkIsDirty: () => isDirty,
|
|
onClose: () => {
|
|
setCreateWrapperShown(false)
|
|
setIsDirty(false)
|
|
},
|
|
})
|
|
|
|
// [Joshen] Opting to declare custom wrapper sheets here instead of within Wrappers.constants.ts
|
|
// as we'll easily run into circular dependencies doing so unfortunately
|
|
const CreateWrapperSheetComponent = !wrapperMeta
|
|
? null
|
|
: wrapperMeta.customComponent
|
|
? wrapperMeta.name === 'iceberg_wrapper'
|
|
? CreateIcebergWrapperSheet
|
|
: null
|
|
: CreateWrapperSheet
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-col gap-y-5">
|
|
<p>Recent wrappers</p>
|
|
<WrapperTable />
|
|
</div>
|
|
|
|
{!!CreateWrapperSheetComponent && !!wrapperMeta && (
|
|
<Sheet open={!!createWrapperShown} onOpenChange={handleOpenChange}>
|
|
<SheetContent size="lg">
|
|
<CreateWrapperSheetComponent
|
|
wrapperMeta={wrapperMeta}
|
|
onDirty={setIsDirty}
|
|
onClose={() => setCreateWrapperShown(false)}
|
|
onCloseWithConfirmation={confirmOnClose}
|
|
/>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)}
|
|
|
|
<DiscardChangesConfirmationDialog {...modalProps} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
const AddNewWrapperCTA = () => {
|
|
const { id } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const [, setCreateWrapperShown] = useQueryState(
|
|
'new',
|
|
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
|
|
)
|
|
|
|
const { can: canCreateWrapper } = useAsyncCheckPermissions(
|
|
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
|
'wrappers'
|
|
)
|
|
|
|
const { data } = useDatabaseExtensionsQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
|
|
const wrapperMeta = WRAPPERS.find((w) => w.name === id)
|
|
const wrappersExtension = data?.find((ext) => ext.name === 'wrappers')
|
|
const isWrappersExtensionInstalled = !!wrappersExtension?.installed_version
|
|
const hasRequiredVersion =
|
|
(wrappersExtension?.installed_version ?? '') >= (wrapperMeta?.minimumExtensionVersion ?? '')
|
|
// [Joshen] Default version is what's on the DB, so if the installed version is already the default version
|
|
// but still doesnt meet the minimum extension version, then DB upgrade is required
|
|
const databaseNeedsUpgrading =
|
|
wrappersExtension?.installed_version === wrappersExtension?.default_version
|
|
|
|
if (!!wrapperMeta && isWrappersExtensionInstalled && !hasRequiredVersion) {
|
|
return (
|
|
<Admonition type="warning" title="Your extension version is outdated for this wrapper">
|
|
<div className="flex flex-col gap-y-2 [&>p]:mb-0!">
|
|
<p>
|
|
The {wrapperMeta.label} wrapper requires a minimum extension version of{' '}
|
|
{wrapperMeta.minimumExtensionVersion}. You have version{' '}
|
|
{wrappersExtension?.installed_version} installed. Please{' '}
|
|
{databaseNeedsUpgrading && 'upgrade your database then '}update the extension by
|
|
disabling and enabling the <code className="text-code-inline">wrappers</code> extension
|
|
to create this wrapper.
|
|
</p>
|
|
<p className="text-warning">
|
|
Warning: Before reinstalling the wrapper extension, you must first remove all existing
|
|
wrappers. Afterward, you can recreate the wrappers.
|
|
</p>
|
|
</div>
|
|
<Button asChild variant="default" className="w-min mt-3">
|
|
<Link
|
|
href={
|
|
databaseNeedsUpgrading
|
|
? getServiceVersionsPath(project?.ref)
|
|
: `/project/${project?.ref}/database/extensions?filter=wrappers`
|
|
}
|
|
>
|
|
{databaseNeedsUpgrading ? 'Upgrade database' : 'View wrappers extension'}
|
|
</Link>
|
|
</Button>
|
|
</Admonition>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="py-3 px-5 border rounded-md">
|
|
<ButtonTooltip
|
|
variant="default"
|
|
onClick={() => setCreateWrapperShown(true)}
|
|
disabled={!canCreateWrapper}
|
|
tooltip={{
|
|
content: {
|
|
text: !canCreateWrapper
|
|
? 'You need additional permissions to create a foreign data wrapper'
|
|
: undefined,
|
|
},
|
|
}}
|
|
>
|
|
Add new wrapper
|
|
</ButtonTooltip>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const WrapperContent = () => {
|
|
const { id } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
|
|
const { data: integrations = [] } = useAvailableIntegrations()
|
|
const integration = integrations.find((i) => i.id === id)
|
|
const wrapperMeta = WRAPPERS.find((w) => w.name === id)
|
|
|
|
const { data: extensions } = useDatabaseExtensionsQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
const installableExtensions = (extensions ?? []).filter((ext) =>
|
|
(integration?.requiredExtensions ?? []).includes(ext.name)
|
|
)
|
|
const isInstalled = installableExtensions.every((x) => x.installed_version)
|
|
|
|
if (!wrapperMeta) {
|
|
return (
|
|
<ScaffoldContainer>
|
|
<ScaffoldSection isFullWidth>
|
|
<p className="text-sm text-foreground-light">Unsupported integration type</p>
|
|
</ScaffoldSection>
|
|
</ScaffoldContainer>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<RequiredExtensionsSection />
|
|
<AddNewWrapperCTA />
|
|
{isInstalled && <WrapperOverviewContent />}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export const WrapperOverviewTab = () => {
|
|
const isMarketplaceEnabled = useIsMarketplaceEnabled()
|
|
|
|
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
|
|
|
|
return (
|
|
<IntegrationOverviewTab actions={<AddNewWrapperCTA />}>
|
|
<div className="mx-10">
|
|
<WrapperOverviewContent />
|
|
</div>
|
|
</IntegrationOverviewTab>
|
|
)
|
|
}
|