Files
supabase/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx
Joshen Lim 66bfc5fdc3 Refactor ConnectSheet + Add unit tests to cover various logic (#47764)
## Context

PR here mainly breaks up the files under `ConnectSheet` to separate the
functional logic so that we can write unit tests.
No behavior changes intended beyond the bug fixes

## Changes involved

- **Test organization:** moved all root-level `ConnectSheet` test files
into `ConnectSheet/__tests__/` for consistency with other parts of the
codebase that use this convention.
- **Bug fix:** read replica label had a stray `}` / missing `)`,
rendering as e.g. `Read Replica (us-east-1 - abc123})` instead of `Read
Replica (us-east-1 - abc123)`.
- **`ConnectSheet.tsx`:** extracted the "hydrate sheet state on open"
`useEffect` logic (mode/field/URL param resolution from URL vs.
localStorage) into a new `ConnectSheet.utils.ts`, with unit tests
- **`useConnectServerEnv.ts`:** fixed two race conditions in the secret
reveal/hide flow:
- `toggle()` and `getValue()` could each fire a separate reveal request
if triggered close together — now deduped to share one in-flight
request.
- `getValue()` could hide a secret that had just been explicitly
revealed by a concurrent `toggle()`, due to reading a stale closure
value — now reads the live state via `useLatest`.
- Also stopped swallowing the original error on reveal failure (now
attached via `cause`).
- Added tests for the above, plus the 10s auto-hide timer (previously
untested).
- **`ConnectStepsSection.tsx`:** extracted `resolveContentPath` and the
three inline "show notice" booleans (IPv4 addon, session pooler,
self-hosted MCP) into `ConnectStepsSection.utils.ts`, matching the
existing pattern for the Data API notice. Added unit tests for all of
them.

## To test
- [ ] Just a basic smoke test of the Connect sheet should do

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved connect setup hydration so saved preferences and URL values
are applied more consistently when opening the sheet, including
automatic URL backfilling where needed.
* Refreshed connection guidance notices (IPv4 add-on, session pooler,
and self-hosted MCP) with more consistent logic.
* **Bug Fixes**
* Fixed secret reveal behavior to keep concurrent reveal actions in
sync, handle failures more safely, and ensure auto-hide works reliably.
  * Corrected the read-replica option label formatting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-10 15:24:34 +08:00

194 lines
6.1 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useEffect, useMemo, useRef } from 'react'
import { cn, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from 'ui'
import type { ConnectMode, ProjectKeys } from './Connect.types'
import { ConnectConfigSection, ModeSelector } from './ConnectConfigSection'
import { resolveConnectSheetHydration } from './ConnectSheet.utils'
import { ConnectStepsSection } from './ConnectStepsSection'
import { useAvailableConnectModes } from './useAvailableConnectModes'
import { useConnectSheetParams } from './useConnectSheetParams'
import { useConnectSheetShortcut } from './useConnectSheetShortcut'
import { useConnectState } from './useConnectState'
import { useAPIKeys } from '@/data/api-keys/api-keys-query'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useTrack } from '@/lib/telemetry/track'
import { useAppStateSnapshot } from '@/state/app-state'
export const ConnectSheet = () => {
const track = useTrack()
const prevShowConnect = useRef(false)
const { ref: projectRef } = useParams()
useConnectSheetShortcut()
const availableModeIds = useAvailableConnectModes()
const { connectSheetSource, setConnectSheetSource } = useAppStateSnapshot()
const { state, activeFields, resolvedSteps, schema, getFieldOptions, setMode, updateField } =
useConnectState()
const [showConnect, setShowConnect] = useQueryState(
'showConnect',
parseAsBoolean.withDefault(false)
)
// URL params have no defaults: a `null` value signals "not in URL" so we can
// fall back to the user's last-used selections from localStorage.
const { params, storedPrefs, setConnectParams, setQueryParams } = useConnectSheetParams()
const {
connectTab,
framework: queryFramework,
using: queryUsing,
method: queryMethod,
type: queryType,
mcpClient: queryMcpClient,
} = params
useEffect(() => {
const justOpened = showConnect && !prevShowConnect.current
prevShowConnect.current = showConnect
if (!justOpened) return
track('connect_sheet_opened', { source: connectSheetSource })
setConnectSheetSource('header_button')
const { mode, fieldUpdates, urlUpdates } = resolveConnectSheetHydration(
{
connectTab,
framework: queryFramework,
using: queryUsing,
method: queryMethod,
type: queryType,
mcpClient: queryMcpClient,
},
storedPrefs,
availableModeIds
)
if (mode) setMode(mode)
fieldUpdates.forEach(({ fieldId, value }) => updateField(fieldId, value))
if (Object.keys(urlUpdates).length > 0) setQueryParams(urlUpdates)
}, [
showConnect,
connectSheetSource,
connectTab,
queryFramework,
queryUsing,
queryMethod,
queryType,
queryMcpClient,
storedPrefs,
availableModeIds,
track,
setConnectSheetSource,
setMode,
updateField,
setQueryParams,
])
const clearAllQueryParams = () => {
setQueryParams({
connectTab: null,
framework: null,
using: null,
method: null,
type: null,
mcpClient: null,
})
}
const handleOpenChange = (sheetOpen: boolean) => {
if (!sheetOpen) clearAllQueryParams()
setShowConnect(sheetOpen)
}
const { data: endpoint = '' } = useProjectApiUrl({ projectRef }, { enabled: showConnect })
const { can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
)
const { data: apiKeysData } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys })
const projectKeys: ProjectKeys = useMemo(() => {
const { anonKey, publishableKey } = apiKeysData ?? {}
return {
apiUrl: endpoint,
anonKey: anonKey?.api_key ?? null,
publishableKey: publishableKey?.api_key ?? null,
}
}, [endpoint, apiKeysData])
const availableModes = useMemo(
() => schema.modes.filter((m) => availableModeIds.includes(m.id)),
[schema.modes, availableModeIds]
)
const handleModeChange = (mode: ConnectMode) => {
setMode(mode)
setConnectParams({
connectTab: mode,
framework: null,
using: null,
method: null,
type: null,
mcpClient: null,
})
}
const handleFieldChange = (fieldId: string, value: string | boolean | string[]) => {
updateField(fieldId, value)
const str = String(value)
if (fieldId === 'framework') {
setConnectParams({ framework: str, using: null })
} else if (fieldId === 'frameworkVariant') {
setConnectParams({ using: str })
} else if (fieldId === 'orm') {
setConnectParams({ framework: str })
} else if (fieldId === 'connectionMethod') {
setConnectParams({ method: str, type: null })
} else if (fieldId === 'connectionType') {
setConnectParams({ type: str })
} else if (fieldId === 'mcpClient') {
setConnectParams({ mcpClient: str })
}
}
return (
<Sheet open={showConnect} onOpenChange={handleOpenChange}>
<SheetContent size="lg" className="flex flex-col gap-0 p-0 space-y-0" tabIndex={undefined}>
<SheetHeader className={cn('text-left border-b shrink-0 py-6 px-8')}>
<SheetTitle>Connect to your project</SheetTitle>
<SheetDescription>Choose how you want to use Supabase</SheetDescription>
</SheetHeader>
<div className="flex flex-1 flex-col overflow-y-auto divide-y">
<div className="p-8">
<ModeSelector
modes={availableModes}
selected={state.mode}
onChange={handleModeChange}
/>
</div>
{activeFields.length > 0 && (
<div className="border-b p-8">
<ConnectConfigSection
state={state}
activeFields={activeFields}
onFieldChange={handleFieldChange}
getFieldOptions={getFieldOptions}
/>
</div>
)}
<ConnectStepsSection steps={resolvedSteps} state={state} projectKeys={projectKeys} />
</div>
</SheetContent>
</Sheet>
)
}