mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 23:29:08 +03:00
remove other modes for review
This commit is contained in:
1 parent
993fe45aee
commit
bc9afa9d47
17 files changed
+47
-1367
No files matched your search
@@ -23,25 +23,17 @@ export const ConnectSheet = ({ open, onOpenChange, connectTab }: ConnectSheetPro
|
||||
const {
|
||||
projectConnectionShowAppFrameworks: showAppFrameworks,
|
||||
projectConnectionShowMobileFrameworks: showMobileFrameworks,
|
||||
projectConnectionShowOrms: showOrms,
|
||||
} = useIsFeatureEnabled([
|
||||
'project_connection:show_app_frameworks',
|
||||
'project_connection:show_mobile_frameworks',
|
||||
'project_connection:show_orms',
|
||||
])
|
||||
|
||||
// Filter available modes based on feature flags
|
||||
const availableModeIds = useMemo(() => {
|
||||
const modes: string[] = []
|
||||
const showFrameworks = showAppFrameworks || showMobileFrameworks
|
||||
|
||||
if (showFrameworks) modes.push('framework')
|
||||
modes.push('direct')
|
||||
if (showOrms) modes.push('orm')
|
||||
modes.push('mcp')
|
||||
|
||||
return modes
|
||||
}, [showAppFrameworks, showMobileFrameworks, showOrms])
|
||||
return showFrameworks ? ['framework'] : []
|
||||
}, [showAppFrameworks, showMobileFrameworks])
|
||||
|
||||
const { state, updateField, setMode, activeFields, resolvedSteps, getFieldOptions, schema } =
|
||||
useConnectState()
|
||||
@@ -90,10 +82,7 @@ export const ConnectSheet = ({ open, onOpenChange, connectTab }: ConnectSheetPro
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
if (typeof connectTab === 'string') {
|
||||
if (connectTab === 'mcp') setMode('mcp')
|
||||
else if (connectTab === 'orms') setMode('orm')
|
||||
else if (connectTab === 'direct') setMode('direct')
|
||||
else if (connectTab === 'frameworks' || connectTab === 'mobiles') setMode('framework')
|
||||
if (connectTab === 'frameworks' || connectTab === 'mobiles') setMode('framework')
|
||||
}
|
||||
}, [connectTab, open, setMode])
|
||||
|
||||
@@ -123,7 +112,9 @@ export const ConnectSheet = ({ open, onOpenChange, connectTab }: ConnectSheetPro
|
||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||
{/* Configuration Section */}
|
||||
<div className="space-y-6 border-b p-8 shrink-0">
|
||||
<ModeSelector modes={availableModes} selected={state.mode} onChange={setMode} />
|
||||
{availableModes.length > 1 && (
|
||||
<ModeSelector modes={availableModes} selected={state.mode} onChange={setMode} />
|
||||
)}
|
||||
<div className="border-t pt-8">
|
||||
<ConnectConfigSection
|
||||
activeFields={activeFields}
|
||||
|
||||
@@ -10,10 +10,7 @@ import type { ConnectState } from './Connect.types'
|
||||
describe('connect.schema:structure', () => {
|
||||
test('should have all required modes', () => {
|
||||
const modeIds = connectSchema.modes.map((m) => m.id)
|
||||
expect(modeIds).toContain('framework')
|
||||
expect(modeIds).toContain('direct')
|
||||
expect(modeIds).toContain('orm')
|
||||
expect(modeIds).toContain('mcp')
|
||||
expect(modeIds).toEqual(['framework'])
|
||||
})
|
||||
|
||||
test('each mode should have required properties', () => {
|
||||
@@ -34,23 +31,19 @@ describe('connect.schema:structure', () => {
|
||||
expect(frameworkMode?.fields).toContain('frameworkUi')
|
||||
})
|
||||
|
||||
test('direct mode should have correct fields', () => {
|
||||
test('direct mode should be removed', () => {
|
||||
const directMode = connectSchema.modes.find((m) => m.id === 'direct')
|
||||
expect(directMode?.fields).toContain('connectionMethod')
|
||||
expect(directMode?.fields).toContain('useSharedPooler')
|
||||
expect(directMode?.fields).toContain('connectionType')
|
||||
expect(directMode).toBeUndefined()
|
||||
})
|
||||
|
||||
test('orm mode should have correct fields', () => {
|
||||
test('orm mode should be removed', () => {
|
||||
const ormMode = connectSchema.modes.find((m) => m.id === 'orm')
|
||||
expect(ormMode?.fields).toContain('orm')
|
||||
expect(ormMode).toBeUndefined()
|
||||
})
|
||||
|
||||
test('mcp mode should have correct fields', () => {
|
||||
test('mcp mode should be removed', () => {
|
||||
const mcpMode = connectSchema.modes.find((m) => m.id === 'mcp')
|
||||
expect(mcpMode?.fields).toContain('mcpClient')
|
||||
expect(mcpMode?.fields).toContain('mcpReadonly')
|
||||
expect(mcpMode?.fields).toContain('mcpFeatures')
|
||||
expect(mcpMode).toBeUndefined()
|
||||
})
|
||||
|
||||
test('all mode fields should exist in fields definition', () => {
|
||||
@@ -89,37 +82,29 @@ describe('connect.schema:fields', () => {
|
||||
expect(field.dependsOn).toEqual({ framework: ['nextjs', 'react'] })
|
||||
})
|
||||
|
||||
test('connectionMethod field should have radio-list type', () => {
|
||||
test('connectionMethod field should be removed', () => {
|
||||
const field = connectSchema.fields.connectionMethod
|
||||
expect(field.type).toBe('radio-list')
|
||||
expect(field.options).toEqual({ source: 'connectionMethods' })
|
||||
expect(field.defaultValue).toBe('direct')
|
||||
expect(field).toBeUndefined()
|
||||
})
|
||||
|
||||
test('useSharedPooler field should depend on transaction connection method', () => {
|
||||
test('useSharedPooler field should be removed', () => {
|
||||
const field = connectSchema.fields.useSharedPooler
|
||||
expect(field.type).toBe('switch')
|
||||
expect(field.dependsOn).toEqual({ connectionMethod: ['transaction'] })
|
||||
expect(field).toBeUndefined()
|
||||
})
|
||||
|
||||
test('orm field should have radio-list type', () => {
|
||||
test('orm field should be removed', () => {
|
||||
const field = connectSchema.fields.orm
|
||||
expect(field.type).toBe('radio-list')
|
||||
expect(field.options).toEqual({ source: 'orms' })
|
||||
expect(field.defaultValue).toBe('prisma')
|
||||
expect(field).toBeUndefined()
|
||||
})
|
||||
|
||||
test('mcpClient field should have select type', () => {
|
||||
test('mcpClient field should be removed', () => {
|
||||
const field = connectSchema.fields.mcpClient
|
||||
expect(field.type).toBe('select')
|
||||
expect(field.options).toEqual({ source: 'mcpClients' })
|
||||
expect(field.defaultValue).toBe('cursor')
|
||||
expect(field).toBeUndefined()
|
||||
})
|
||||
|
||||
test('mcpFeatures field should have multi-select type', () => {
|
||||
test('mcpFeatures field should be removed', () => {
|
||||
const field = connectSchema.fields.mcpFeatures
|
||||
expect(field.type).toBe('multi-select')
|
||||
expect(field.options).toEqual({ source: 'mcpFeatures' })
|
||||
expect(field).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -198,101 +183,35 @@ describe('connect.schema:steps resolution', () => {
|
||||
})
|
||||
|
||||
describe('direct mode steps', () => {
|
||||
test('should resolve connection step for default direct mode', () => {
|
||||
test('should not resolve steps for direct mode', () => {
|
||||
const state: ConnectState = { mode: 'direct' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'connection')).toBeDefined()
|
||||
})
|
||||
|
||||
test('should resolve install and files steps for nodejs connection type', () => {
|
||||
const state: ConnectState = { mode: 'direct', connectionType: 'nodejs' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'direct-files')).toBeDefined()
|
||||
})
|
||||
|
||||
test('should resolve install and files steps for golang connection type', () => {
|
||||
const state: ConnectState = { mode: 'direct', connectionType: 'golang' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'direct-files')).toBeDefined()
|
||||
})
|
||||
|
||||
test('should resolve install and files steps for python connection type', () => {
|
||||
const state: ConnectState = { mode: 'direct', connectionType: 'python' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
|
||||
})
|
||||
|
||||
test('should resolve install and files steps for dotnet connection type', () => {
|
||||
const state: ConnectState = { mode: 'direct', connectionType: 'dotnet' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
|
||||
expect(steps.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('orm mode steps', () => {
|
||||
test('should resolve install and configure steps for prisma', () => {
|
||||
test('should not resolve steps for orm mode', () => {
|
||||
const state: ConnectState = { mode: 'orm', orm: 'prisma' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'install')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'configure')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'install-skills')).toBeDefined()
|
||||
})
|
||||
|
||||
test('should resolve install and configure steps for drizzle', () => {
|
||||
const state: ConnectState = { mode: 'orm', orm: 'drizzle' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'install')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'configure')).toBeDefined()
|
||||
expect(steps.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mcp mode steps', () => {
|
||||
test('should resolve configure step for cursor client', () => {
|
||||
test('should not resolve steps for mcp mode', () => {
|
||||
const state: ConnectState = { mode: 'mcp', mcpClient: 'cursor' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'configure-mcp')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'install-skills')).toBeDefined()
|
||||
})
|
||||
|
||||
test('should resolve codex-specific steps for codex client', () => {
|
||||
const state: ConnectState = { mode: 'mcp', mcpClient: 'codex' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'codex-add-server')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'codex-enable-remote')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'codex-authenticate')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'codex-verify')).toBeDefined()
|
||||
})
|
||||
|
||||
test('should resolve claude-code-specific steps for claude-code client', () => {
|
||||
const state: ConnectState = { mode: 'mcp', mcpClient: 'claude-code' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'claude-add-server')).toBeDefined()
|
||||
expect(steps.find((s) => s.id === 'claude-authenticate')).toBeDefined()
|
||||
})
|
||||
|
||||
test('should resolve default mcp steps for other clients', () => {
|
||||
const state: ConnectState = { mode: 'mcp', mcpClient: 'unknown-client' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'configure-mcp')).toBeDefined()
|
||||
expect(steps.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('skills install step', () => {
|
||||
test('should include skills install step in all modes', () => {
|
||||
const modes: ConnectState['mode'][] = ['framework', 'direct', 'orm', 'mcp']
|
||||
const modes: ConnectState['mode'][] = ['framework']
|
||||
|
||||
modes.forEach((mode) => {
|
||||
const state: ConnectState = { mode }
|
||||
@@ -336,12 +255,11 @@ describe('connect.schema:step content paths', () => {
|
||||
expect(exploreStep?.content).toBe('steps/shadcn/explore')
|
||||
})
|
||||
|
||||
test('direct connection step should have valid content path', () => {
|
||||
test('direct connection step should not be resolved', () => {
|
||||
const state: ConnectState = { mode: 'direct' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
const connectionStep = steps.find((s) => s.id === 'connection')
|
||||
|
||||
expect(connectionStep?.content).toBe('steps/direct-connection')
|
||||
expect(steps.length).toBe(0)
|
||||
})
|
||||
|
||||
test('skills install step should have valid content path', () => {
|
||||
@@ -352,50 +270,31 @@ describe('connect.schema:step content paths', () => {
|
||||
expect(skillsStep?.content).toBe('steps/skills-install')
|
||||
})
|
||||
|
||||
test('orm configure step should use template content path', () => {
|
||||
// The ORM configure step uses a template {{orm}} that gets resolved
|
||||
// by the dynamic import system, not the resolver
|
||||
test('orm configure step should not be resolved', () => {
|
||||
const state: ConnectState = { mode: 'orm', orm: 'prisma' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
const configureStep = steps.find((s) => s.id === 'configure')
|
||||
|
||||
// The content path uses template syntax for the component loader
|
||||
expect(configureStep?.content).toBe('{{orm}}')
|
||||
expect(steps.length).toBe(0)
|
||||
})
|
||||
|
||||
test('mcp cursor configure step should have valid content path', () => {
|
||||
test('mcp cursor configure step should not be resolved', () => {
|
||||
const state: ConnectState = { mode: 'mcp', mcpClient: 'cursor' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
const configureStep = steps.find((s) => s.id === 'configure-mcp')
|
||||
|
||||
expect(configureStep?.content).toBe('steps/mcp/cursor')
|
||||
expect(steps.length).toBe(0)
|
||||
})
|
||||
|
||||
test('codex steps should have valid content paths', () => {
|
||||
test('codex steps should not be resolved', () => {
|
||||
const state: ConnectState = { mode: 'mcp', mcpClient: 'codex' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'codex-add-server')?.content).toBe(
|
||||
'steps/mcp/codex/add-server'
|
||||
)
|
||||
expect(steps.find((s) => s.id === 'codex-enable-remote')?.content).toBe(
|
||||
'steps/mcp/codex/enable-remote'
|
||||
)
|
||||
expect(steps.find((s) => s.id === 'codex-authenticate')?.content).toBe(
|
||||
'steps/mcp/codex/authenticate'
|
||||
)
|
||||
expect(steps.find((s) => s.id === 'codex-verify')?.content).toBe('steps/mcp/codex/verify')
|
||||
expect(steps.length).toBe(0)
|
||||
})
|
||||
|
||||
test('claude-code steps should have valid content paths', () => {
|
||||
test('claude-code steps should not be resolved', () => {
|
||||
const state: ConnectState = { mode: 'mcp', mcpClient: 'claude-code' }
|
||||
const steps = resolveSteps(connectSchema, state)
|
||||
|
||||
expect(steps.find((s) => s.id === 'claude-add-server')?.content).toBe(
|
||||
'steps/mcp/claude-code/add-server'
|
||||
)
|
||||
expect(steps.find((s) => s.id === 'claude-authenticate')?.content).toBe(
|
||||
'steps/mcp/claude-code/authenticate'
|
||||
)
|
||||
expect(steps.length).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -60,92 +60,6 @@ const frameworkShadcnExploreStep: StepDefinition = {
|
||||
content: 'steps/shadcn/explore',
|
||||
}
|
||||
|
||||
const directConnectionStep: StepDefinition = {
|
||||
id: 'connection',
|
||||
title: 'Connection string',
|
||||
description: 'Copy the connection details for your database.',
|
||||
content: 'steps/direct-connection',
|
||||
}
|
||||
|
||||
const directInstallStep: StepDefinition = {
|
||||
id: 'direct-install',
|
||||
title: 'Install dependencies',
|
||||
description: 'Run this command to install the required dependencies.',
|
||||
content: 'steps/direct-install',
|
||||
}
|
||||
|
||||
const directFilesStep: StepDefinition = {
|
||||
id: 'direct-files',
|
||||
title: 'Add files',
|
||||
description: 'Add the following files to your project.',
|
||||
content: 'steps/direct-files',
|
||||
}
|
||||
|
||||
const mcpConfigureStep: StepDefinition = {
|
||||
id: 'configure-mcp',
|
||||
title: 'Configure MCP',
|
||||
description: 'Set up your MCP client.',
|
||||
content: 'steps/mcp/cursor',
|
||||
}
|
||||
|
||||
// Codex-specific MCP steps
|
||||
const codexAddServerStep: StepDefinition = {
|
||||
id: 'codex-add-server',
|
||||
title: 'Add the Supabase MCP server to Codex',
|
||||
description: 'Run this command to add the server.',
|
||||
content: 'steps/mcp/codex/add-server',
|
||||
}
|
||||
|
||||
const codexEnableRemoteStep: StepDefinition = {
|
||||
id: 'codex-enable-remote',
|
||||
title: 'Enable remote MCP client support',
|
||||
description: 'Add this to your ~/.codex/config.toml file.',
|
||||
content: 'steps/mcp/codex/enable-remote',
|
||||
}
|
||||
|
||||
const codexAuthenticateStep: StepDefinition = {
|
||||
id: 'codex-authenticate',
|
||||
title: 'Authenticate',
|
||||
description: 'Run the authentication command.',
|
||||
content: 'steps/mcp/codex/authenticate',
|
||||
}
|
||||
|
||||
const codexVerifyStep: StepDefinition = {
|
||||
id: 'codex-verify',
|
||||
title: 'Verify authentication',
|
||||
description: 'Run /mcp inside Codex to verify.',
|
||||
content: 'steps/mcp/codex/verify',
|
||||
}
|
||||
|
||||
const claudeAddServerStep: StepDefinition = {
|
||||
id: 'claude-add-server',
|
||||
title: 'Add MCP server',
|
||||
description: 'Add the MCP server to your project config using the command line.',
|
||||
content: 'steps/mcp/claude-code/add-server',
|
||||
}
|
||||
|
||||
const claudeAuthenticateStep: StepDefinition = {
|
||||
id: 'claude-authenticate',
|
||||
title: 'Authenticate',
|
||||
description:
|
||||
'After configuring the MCP server, you need to authenticate. In a regular terminal (not the IDE extension) run:',
|
||||
content: 'steps/mcp/claude-code/authenticate',
|
||||
}
|
||||
|
||||
const ormInstallStep: StepDefinition = {
|
||||
id: 'install',
|
||||
title: 'Install ORM',
|
||||
description: 'Add the ORM to your project.',
|
||||
content: 'steps/orm-install',
|
||||
}
|
||||
|
||||
const ormConfigureStep: StepDefinition = {
|
||||
id: 'configure',
|
||||
title: 'Configure ORM',
|
||||
description: 'Set up your ORM configuration.',
|
||||
content: '{{orm}}',
|
||||
}
|
||||
|
||||
const skillsInstallStep: StepDefinition = {
|
||||
id: 'install-skills',
|
||||
title: 'Install Agent Skills (Optional)',
|
||||
@@ -169,24 +83,6 @@ export const connectSchema: ConnectSchema = {
|
||||
description: 'Use a client library',
|
||||
fields: ['framework', 'frameworkVariant', 'library', 'frameworkUi'],
|
||||
},
|
||||
{
|
||||
id: 'direct',
|
||||
label: 'Direct',
|
||||
description: 'Connection string',
|
||||
fields: ['connectionMethod', 'useSharedPooler', 'connectionType'],
|
||||
},
|
||||
{
|
||||
id: 'orm',
|
||||
label: 'ORM',
|
||||
description: 'Third-party library',
|
||||
fields: ['orm'],
|
||||
},
|
||||
{
|
||||
id: 'mcp',
|
||||
label: 'MCP',
|
||||
description: 'Connect your agent',
|
||||
fields: ['mcpClient', 'mcpReadonly', 'mcpFeatures'],
|
||||
},
|
||||
],
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -224,64 +120,6 @@ export const connectSchema: ConnectSchema = {
|
||||
defaultValue: false,
|
||||
dependsOn: { framework: ['nextjs', 'react'] },
|
||||
},
|
||||
|
||||
// Direct connection fields
|
||||
connectionMethod: {
|
||||
id: 'connectionMethod',
|
||||
type: 'radio-list',
|
||||
label: 'Connection Method',
|
||||
options: { source: 'connectionMethods' },
|
||||
defaultValue: 'direct',
|
||||
},
|
||||
useSharedPooler: {
|
||||
id: 'useSharedPooler',
|
||||
type: 'switch',
|
||||
label: 'Use IPv4 connection (Shared Pooler)',
|
||||
description: 'Only recommended when your network does not support IPv6',
|
||||
defaultValue: false,
|
||||
dependsOn: { connectionMethod: ['transaction'] },
|
||||
},
|
||||
connectionType: {
|
||||
id: 'connectionType',
|
||||
type: 'select',
|
||||
label: 'Type',
|
||||
options: { source: 'connectionTypes' },
|
||||
defaultValue: 'uri',
|
||||
},
|
||||
|
||||
// ORM fields
|
||||
orm: {
|
||||
id: 'orm',
|
||||
type: 'radio-list',
|
||||
label: 'ORM',
|
||||
options: { source: 'orms' },
|
||||
defaultValue: 'prisma',
|
||||
},
|
||||
|
||||
// MCP fields
|
||||
mcpClient: {
|
||||
id: 'mcpClient',
|
||||
type: 'select',
|
||||
label: 'Client',
|
||||
description: 'Choose the MCP client you are using.',
|
||||
options: { source: 'mcpClients' },
|
||||
defaultValue: 'cursor',
|
||||
},
|
||||
mcpReadonly: {
|
||||
id: 'mcpReadonly',
|
||||
type: 'switch',
|
||||
label: 'Read-only',
|
||||
description: 'Only allow read operations on your database',
|
||||
defaultValue: false,
|
||||
},
|
||||
mcpFeatures: {
|
||||
id: 'mcpFeatures',
|
||||
type: 'multi-select',
|
||||
label: 'Feature groups',
|
||||
description:
|
||||
'Only enable a subset of features. Helps keep the number of tools within MCP client limits.',
|
||||
options: { source: 'mcpFeatures' },
|
||||
},
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -317,31 +155,6 @@ export const connectSchema: ConnectSchema = {
|
||||
DEFAULT: [frameworkInstallStep, frameworkConfigureStep, skillsInstallStep],
|
||||
},
|
||||
},
|
||||
direct: {
|
||||
connectionType: {
|
||||
nodejs: [directInstallStep, directFilesStep, skillsInstallStep],
|
||||
golang: [directInstallStep, directFilesStep, skillsInstallStep],
|
||||
dotnet: [directInstallStep, directFilesStep, skillsInstallStep],
|
||||
python: [directInstallStep, directFilesStep, skillsInstallStep],
|
||||
sqlalchemy: [directInstallStep, directFilesStep, skillsInstallStep],
|
||||
DEFAULT: [directConnectionStep, skillsInstallStep],
|
||||
},
|
||||
},
|
||||
orm: [ormInstallStep, ormConfigureStep, skillsInstallStep],
|
||||
mcp: {
|
||||
mcpClient: {
|
||||
codex: [
|
||||
codexAddServerStep,
|
||||
codexEnableRemoteStep,
|
||||
codexAuthenticateStep,
|
||||
codexVerifyStep,
|
||||
skillsInstallStep,
|
||||
],
|
||||
'claude-code': [claudeAddServerStep, claudeAuthenticateStep, skillsInstallStep],
|
||||
DEFAULT: [mcpConfigureStep, skillsInstallStep],
|
||||
},
|
||||
},
|
||||
DEFAULT: [skillsInstallStep],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ connectionStringPooler }: ContentFileProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env',
|
||||
language: 'bash',
|
||||
code:
|
||||
connectionStringPooler.ipv4SupportedForDedicatedPooler &&
|
||||
connectionStringPooler.transactionDedicated
|
||||
? `
|
||||
DATABASE_URL="${connectionStringPooler.transactionDedicated}"
|
||||
`
|
||||
: connectionStringPooler.transactionDedicated &&
|
||||
!connectionStringPooler.ipv4SupportedForDedicatedPooler
|
||||
? `
|
||||
# Use Shared connection pooler (supports both IPv4/IPv6)
|
||||
DATABASE_URL="${connectionStringPooler.transactionShared}"
|
||||
|
||||
# If your network supports IPv6 or you purchased IPv4 addon, use dedicated pooler
|
||||
# DATABASE_URL="${connectionStringPooler.transactionDedicated}"
|
||||
`
|
||||
: `
|
||||
DATABASE_URL="${connectionStringPooler.transactionShared}"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'drizzle/schema.tsx',
|
||||
language: 'tsx',
|
||||
code: `
|
||||
import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
fullName: text('full_name'),
|
||||
phone: varchar('phone', { length: 256 }),
|
||||
});
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'index.tsx',
|
||||
language: 'tsx',
|
||||
code: `
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import postgres from 'postgres'
|
||||
import { users } from './schema'
|
||||
|
||||
const connectionString = process.env.DATABASE_URL
|
||||
|
||||
// Disable prefetch as it is not supported for "Transaction" pool mode
|
||||
const client = postgres(connectionString, { prepare: false })
|
||||
const db = drizzle(client);
|
||||
|
||||
const allUsers = await db.select().from(users);
|
||||
`,
|
||||
},
|
||||
]
|
||||
|
||||
return <MultipleCodeBlock files={files} />
|
||||
}
|
||||
|
||||
export default ContentFile
|
||||
@@ -1,62 +0,0 @@
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
|
||||
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
|
||||
|
||||
const ContentFile = ({ connectionStringPooler }: ContentFileProps) => {
|
||||
const files = [
|
||||
{
|
||||
name: '.env.local',
|
||||
language: 'bash',
|
||||
code:
|
||||
connectionStringPooler.ipv4SupportedForDedicatedPooler &&
|
||||
connectionStringPooler.transactionDedicated
|
||||
? `
|
||||
# Connect to Supabase via connection pooling.
|
||||
DATABASE_URL="${connectionStringPooler.transactionDedicated}?pgbouncer=true"
|
||||
|
||||
# Direct connection to the database. Used for migrations.
|
||||
DIRECT_URL="${connectionStringPooler.sessionDedicated}"
|
||||
`
|
||||
: connectionStringPooler.transactionDedicated &&
|
||||
!connectionStringPooler.ipv4SupportedForDedicatedPooler
|
||||
? `
|
||||
# Connect to Supabase via Shared Connection Pooler
|
||||
DATABASE_URL="${connectionStringPooler.transactionShared}?pgbouncer=true"
|
||||
|
||||
# Direct connection to the database through Shared Pooler (supports IPv4/IPv6). Used for migrations.
|
||||
DIRECT_URL="${connectionStringPooler.sessionShared}"
|
||||
|
||||
# If your network supports IPv6 or you purchased IPv4 addon, use dedicated pooler
|
||||
# DATABASE_URL="${connectionStringPooler.transactionDedicated}?pgbouncer=true"
|
||||
# DIRECT_URL="${connectionStringPooler.sessionDedicated}"
|
||||
`
|
||||
: `
|
||||
# Connect to Supabase ${IS_PLATFORM ? 'via connection pooling' : ''}
|
||||
DATABASE_URL="${IS_PLATFORM ? `${connectionStringPooler.transactionShared}?pgbouncer=true` : connectionStringPooler.direct}"
|
||||
|
||||
# Direct connection to the database. Used for migrations
|
||||
DIRECT_URL="${IS_PLATFORM ? connectionStringPooler.sessionShared : connectionStringPooler.direct}"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'prisma/schema.prisma',
|
||||
language: 'bash',
|
||||
code: `
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
`,
|
||||
},
|
||||
]
|
||||
|
||||
return <MultipleCodeBlock files={files} />
|
||||
}
|
||||
|
||||
export default ContentFile
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
import { CodeBlock } from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import {
|
||||
type ConnectionStringMethod,
|
||||
type DatabaseConnectionType,
|
||||
} from '../../../Connect.constants'
|
||||
import type { StepContentProps } from '../../../Connect.types'
|
||||
import { ConnectionParameters } from '../../../ConnectionParameters'
|
||||
import {
|
||||
PASSWORD_PLACEHOLDER,
|
||||
buildConnectionParameters,
|
||||
buildSafeConnectionString,
|
||||
parseConnectionParams,
|
||||
resolveConnectionString,
|
||||
} from '../../../ConnectionString.utils'
|
||||
|
||||
const buildPsqlCommand = (params: { host: string; port: string; database: string; user: string }) =>
|
||||
`psql -h ${params.host} -p ${params.port} -d ${params.database} -U ${params.user}`
|
||||
|
||||
const buildJdbcString = (params: { host: string; port: string; database: string; user: string }) =>
|
||||
`jdbc:postgresql://${params.host}:${params.port}/${params.database}?user=${params.user}&password=${PASSWORD_PLACEHOLDER}`
|
||||
|
||||
/**
|
||||
* Step component for direct database connections.
|
||||
* Uses state to determine which connection string to show.
|
||||
*/
|
||||
function DirectConnectionContent({ state, connectionStringPooler }: StepContentProps) {
|
||||
const connectionType = (state.connectionType as DatabaseConnectionType) ?? 'uri'
|
||||
const connectionMethod = (state.connectionMethod as ConnectionStringMethod) ?? 'direct'
|
||||
const useSharedPooler = Boolean(state.useSharedPooler)
|
||||
|
||||
// Determine which connection string to use
|
||||
const resolvedConnectionString = useMemo(
|
||||
() =>
|
||||
resolveConnectionString({
|
||||
connectionMethod,
|
||||
useSharedPooler,
|
||||
connectionStringPooler,
|
||||
}),
|
||||
[connectionMethod, useSharedPooler, connectionStringPooler]
|
||||
)
|
||||
|
||||
const connectionParams = useMemo(
|
||||
() => parseConnectionParams(resolvedConnectionString),
|
||||
[resolvedConnectionString]
|
||||
)
|
||||
|
||||
const safeConnectionString = useMemo(
|
||||
() => buildSafeConnectionString(resolvedConnectionString, connectionParams),
|
||||
[resolvedConnectionString, connectionParams]
|
||||
)
|
||||
|
||||
const connectionString = useMemo(() => {
|
||||
switch (connectionType) {
|
||||
case 'psql':
|
||||
return buildPsqlCommand(connectionParams)
|
||||
case 'jdbc':
|
||||
return buildJdbcString(connectionParams)
|
||||
case 'php':
|
||||
return `DATABASE_URL=${safeConnectionString}`
|
||||
case 'uri':
|
||||
default:
|
||||
return safeConnectionString
|
||||
}
|
||||
}, [connectionType, connectionParams, safeConnectionString])
|
||||
|
||||
if (!resolvedConnectionString) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<CodeBlock
|
||||
className="[&_code]:text-foreground"
|
||||
wrapperClassName="lg:col-span-2"
|
||||
value={connectionString}
|
||||
hideLineNumbers
|
||||
language="bash"
|
||||
>
|
||||
{connectionString}
|
||||
</CodeBlock>
|
||||
<ConnectionParameters parameters={buildConnectionParameters(connectionParams)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DirectConnectionContent
|
||||
-264
@@ -1,264 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { CodeBlock } from 'ui'
|
||||
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import {
|
||||
type ConnectionStringMethod,
|
||||
type DatabaseConnectionType,
|
||||
} from '../../../Connect.constants'
|
||||
import type { StepContentProps } from '../../../Connect.types'
|
||||
import { ConnectionParameters } from '../../../ConnectionParameters'
|
||||
import {
|
||||
PASSWORD_PLACEHOLDER,
|
||||
buildConnectionParameters,
|
||||
buildSafeConnectionString,
|
||||
parseConnectionParams,
|
||||
resolveConnectionString,
|
||||
} from '../../../ConnectionString.utils'
|
||||
|
||||
const DOTNET_CONFIG_COMMAND =
|
||||
'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION'
|
||||
|
||||
type DirectFilesConfig = {
|
||||
files: {
|
||||
name: string
|
||||
language?: string
|
||||
code: string
|
||||
}[]
|
||||
connectionStringFile?: string
|
||||
postCommands?: { label: string; command: string }[]
|
||||
}
|
||||
|
||||
function DirectFilesContent({ state, connectionStringPooler }: StepContentProps) {
|
||||
const connectionType = (state.connectionType as DatabaseConnectionType) ?? 'uri'
|
||||
const connectionMethod = (state.connectionMethod as ConnectionStringMethod) ?? 'direct'
|
||||
const useSharedPooler = Boolean(state.useSharedPooler)
|
||||
|
||||
const resolvedConnectionString = useMemo(
|
||||
() =>
|
||||
resolveConnectionString({
|
||||
connectionMethod,
|
||||
useSharedPooler,
|
||||
connectionStringPooler,
|
||||
}),
|
||||
[connectionMethod, useSharedPooler, connectionStringPooler]
|
||||
)
|
||||
|
||||
const connectionParams = useMemo(
|
||||
() => parseConnectionParams(resolvedConnectionString),
|
||||
[resolvedConnectionString]
|
||||
)
|
||||
|
||||
const safeConnectionString = useMemo(
|
||||
() => buildSafeConnectionString(resolvedConnectionString, connectionParams),
|
||||
[resolvedConnectionString, connectionParams]
|
||||
)
|
||||
|
||||
const config: DirectFilesConfig | null = useMemo(() => {
|
||||
const envFile = {
|
||||
name: '.env',
|
||||
language: 'bash',
|
||||
code: `DATABASE_URL=${safeConnectionString}`,
|
||||
}
|
||||
|
||||
switch (connectionType) {
|
||||
case 'nodejs':
|
||||
return {
|
||||
files: [
|
||||
{
|
||||
name: 'db.js',
|
||||
language: 'js',
|
||||
code: `import postgres from 'postgres'
|
||||
|
||||
const connectionString = process.env.DATABASE_URL
|
||||
const sql = postgres(connectionString)
|
||||
|
||||
export default sql`,
|
||||
},
|
||||
envFile,
|
||||
],
|
||||
connectionStringFile: envFile.name,
|
||||
}
|
||||
|
||||
case 'golang':
|
||||
return {
|
||||
files: [
|
||||
{
|
||||
name: 'main.go',
|
||||
language: 'go',
|
||||
code: `package main
|
||||
|
||||
import (
|
||||
\t"context"
|
||||
\t"log"
|
||||
\t"os"
|
||||
\t"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func main() {
|
||||
\tconn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
|
||||
\tif err != nil {
|
||||
\t\tlog.Fatalf("Failed to connect to the database: %v", err)
|
||||
\t}
|
||||
\tdefer conn.Close(context.Background())
|
||||
|
||||
\t// Example query to test connection
|
||||
\tvar version string
|
||||
\tif err := conn.QueryRow(context.Background(), "SELECT version()").Scan(&version); err != nil {
|
||||
\t\tlog.Fatalf("Query failed: %v", err)
|
||||
\t}
|
||||
|
||||
\tlog.Println("Connected to:", version)
|
||||
}`,
|
||||
},
|
||||
envFile,
|
||||
],
|
||||
connectionStringFile: envFile.name,
|
||||
}
|
||||
|
||||
case 'dotnet':
|
||||
return {
|
||||
files: [
|
||||
{
|
||||
name: 'appsettings.json',
|
||||
language: 'json',
|
||||
code: `{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=${connectionParams.host};Database=${connectionParams.database};Username=${connectionParams.user};Password=${PASSWORD_PLACEHOLDER};SSL Mode=Require;Trust Server Certificate=true"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
connectionStringFile: 'appsettings.json',
|
||||
postCommands: [
|
||||
{
|
||||
label: 'Add the configuration package to read the settings.',
|
||||
command: DOTNET_CONFIG_COMMAND,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
case 'python':
|
||||
return {
|
||||
files: [
|
||||
{
|
||||
name: 'main.py',
|
||||
language: 'python',
|
||||
code: `import psycopg2
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
# Load environment variables from .env
|
||||
load_dotenv()
|
||||
|
||||
# Fetch variables
|
||||
DATABASE_URL = os.getenv("DATABASE_URL")
|
||||
|
||||
# Connect to the database
|
||||
connection = psycopg2.connect(DATABASE_URL)`,
|
||||
},
|
||||
envFile,
|
||||
],
|
||||
connectionStringFile: envFile.name,
|
||||
}
|
||||
|
||||
case 'sqlalchemy':
|
||||
return {
|
||||
files: [
|
||||
{
|
||||
name: 'main.py',
|
||||
language: 'python',
|
||||
code: `from sqlalchemy import create_engine
|
||||
# from sqlalchemy.pool import NullPool
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
# Load environment variables from .env
|
||||
load_dotenv()
|
||||
|
||||
# Fetch variables
|
||||
USER = os.getenv("user")
|
||||
PASSWORD = os.getenv("password")
|
||||
HOST = os.getenv("host")
|
||||
PORT = os.getenv("port")
|
||||
DBNAME = os.getenv("dbname")
|
||||
|
||||
# Construct the SQLAlchemy connection string
|
||||
DATABASE_URL = f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require"
|
||||
|
||||
# Create the SQLAlchemy engine
|
||||
engine = create_engine(DATABASE_URL)
|
||||
# If using Transaction Pooler or Session Pooler, we want to ensure we disable SQLAlchemy client side pooling -
|
||||
# https://docs.sqlalchemy.org/en/20/core/pooling.html#switching-pool-implementations
|
||||
# engine = create_engine(DATABASE_URL, poolclass=NullPool)
|
||||
|
||||
# Test the connection
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
print("Connection successful!")
|
||||
except Exception as e:
|
||||
print(f"Failed to connect: {e}")`,
|
||||
},
|
||||
{
|
||||
name: '.env',
|
||||
language: 'bash',
|
||||
code: [
|
||||
`user=${connectionParams.user}`,
|
||||
`password=${PASSWORD_PLACEHOLDER}`,
|
||||
`host=${connectionParams.host}`,
|
||||
`port=${connectionParams.port}`,
|
||||
`dbname=${connectionParams.database}`,
|
||||
].join('\n'),
|
||||
},
|
||||
],
|
||||
connectionStringFile: '.env',
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}, [connectionType, safeConnectionString, connectionParams])
|
||||
|
||||
const defaultFile = config?.files[0]?.name ?? ''
|
||||
const [activeFile, setActiveFile] = useState(defaultFile)
|
||||
|
||||
useEffect(() => {
|
||||
setActiveFile(defaultFile)
|
||||
}, [connectionType, defaultFile])
|
||||
|
||||
if (!resolvedConnectionString) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!config?.files.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<MultipleCodeBlock files={config.files} value={activeFile} onValueChange={setActiveFile} />
|
||||
<ConnectionParameters parameters={buildConnectionParameters(connectionParams)} />
|
||||
{(config.postCommands ?? []).map((command) => (
|
||||
<div key={command.command} className="flex flex-col gap-2">
|
||||
<p className="text-sm text-foreground-light">{command.label}</p>
|
||||
<CodeBlock
|
||||
className="[&_code]:text-foreground"
|
||||
wrapperClassName="lg:col-span-2"
|
||||
value={command.command}
|
||||
hideLineNumbers
|
||||
language="bash"
|
||||
>
|
||||
{command.command}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DirectFilesContent
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { CodeBlock } from 'ui'
|
||||
|
||||
import type { StepContentProps } from '../../../Connect.types'
|
||||
import examples from '../../../DirectConnectionExamples'
|
||||
|
||||
/**
|
||||
* Step component for direct connection install commands.
|
||||
*/
|
||||
function DirectInstallContent({ state }: StepContentProps) {
|
||||
const connectionType = (state.connectionType as string) ?? 'uri'
|
||||
const example = examples[connectionType as keyof typeof examples]
|
||||
const exampleInstallCommands = example?.installCommands ?? []
|
||||
|
||||
if (exampleInstallCommands.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{exampleInstallCommands.map((cmd) => (
|
||||
<CodeBlock
|
||||
key={`example-install-command-${cmd}`}
|
||||
className="[&_code]:text-foreground"
|
||||
wrapperClassName="lg:col-span-2"
|
||||
value={cmd}
|
||||
hideLineNumbers
|
||||
language="bash"
|
||||
>
|
||||
{cmd}
|
||||
</CodeBlock>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DirectInstallContent
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
import { CodeBlock } from 'ui'
|
||||
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { FEATURE_GROUPS_NON_PLATFORM, FEATURE_GROUPS_PLATFORM } from 'ui-patterns/McpUrlBuilder'
|
||||
import type { StepContentProps } from '../../../../../Connect.types'
|
||||
|
||||
function useMcpUrl(
|
||||
state: StepContentProps['state'],
|
||||
projectKeys: StepContentProps['projectKeys']
|
||||
): string {
|
||||
const readonly = Boolean(state.mcpReadonly)
|
||||
const selectedFeatures = Array.isArray(state.mcpFeatures) ? state.mcpFeatures : []
|
||||
const baseUrl = IS_PLATFORM ? 'https://mcp.supabase.com' : projectKeys.apiUrl ?? ''
|
||||
|
||||
return useMemo(() => {
|
||||
const params = new URLSearchParams()
|
||||
if (readonly) params.set('readonly', 'true')
|
||||
|
||||
const supportedFeatures = IS_PLATFORM ? FEATURE_GROUPS_PLATFORM : FEATURE_GROUPS_NON_PLATFORM
|
||||
const validFeatures = selectedFeatures.filter((f) =>
|
||||
supportedFeatures.some((group) => group.id === f)
|
||||
)
|
||||
if (validFeatures.length > 0) {
|
||||
params.set('features', validFeatures.join(','))
|
||||
}
|
||||
|
||||
const queryString = params.toString()
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl
|
||||
}, [baseUrl, readonly, selectedFeatures])
|
||||
}
|
||||
|
||||
function ClaudeAddServerContent({ state, projectKeys }: StepContentProps) {
|
||||
const mcpUrl = useMcpUrl(state, projectKeys)
|
||||
const command = `claude mcp add --scope project --transport http supabase "${mcpUrl}"`
|
||||
|
||||
return (
|
||||
<CodeBlock
|
||||
className="[&_code]:text-foreground"
|
||||
value={command}
|
||||
hideLineNumbers
|
||||
language="bash"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ClaudeAddServerContent
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { CodeBlock } from 'ui'
|
||||
|
||||
import type { StepContentProps } from '../../../../../Connect.types'
|
||||
|
||||
function ClaudeAuthenticateContent(_props: StepContentProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<CodeBlock
|
||||
className="[&_code]:text-foreground"
|
||||
value="claude /mcp"
|
||||
hideLineNumbers
|
||||
language="bash"
|
||||
/>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Select the <code className="text-xs bg-surface-300 px-1 py-0.5 rounded">supabase</code>{' '}
|
||||
server, then <span className="font-medium">Authenticate</span> to begin the flow.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ClaudeAuthenticateContent
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
import { CodeBlock } from 'ui'
|
||||
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { FEATURE_GROUPS_NON_PLATFORM, FEATURE_GROUPS_PLATFORM } from 'ui-patterns/McpUrlBuilder'
|
||||
import type { StepContentProps } from '../../../../../Connect.types'
|
||||
|
||||
function useMcpUrl(
|
||||
state: StepContentProps['state'],
|
||||
projectKeys: StepContentProps['projectKeys']
|
||||
): string {
|
||||
const readonly = Boolean(state.mcpReadonly)
|
||||
const selectedFeatures = Array.isArray(state.mcpFeatures) ? state.mcpFeatures : []
|
||||
const baseUrl = IS_PLATFORM ? 'https://mcp.supabase.com' : projectKeys.apiUrl ?? ''
|
||||
|
||||
return useMemo(() => {
|
||||
const params = new URLSearchParams()
|
||||
if (readonly) params.set('readonly', 'true')
|
||||
|
||||
const supportedFeatures = IS_PLATFORM ? FEATURE_GROUPS_PLATFORM : FEATURE_GROUPS_NON_PLATFORM
|
||||
const validFeatures = selectedFeatures.filter((f) =>
|
||||
supportedFeatures.some((group) => group.id === f)
|
||||
)
|
||||
if (validFeatures.length > 0) {
|
||||
params.set('features', validFeatures.join(','))
|
||||
}
|
||||
|
||||
const queryString = params.toString()
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl
|
||||
}, [baseUrl, readonly, selectedFeatures])
|
||||
}
|
||||
|
||||
function CodexAddServerContent({ state, projectKeys }: StepContentProps) {
|
||||
const mcpUrl = useMcpUrl(state, projectKeys)
|
||||
const command = `codex mcp add supabase --url ${mcpUrl}`
|
||||
|
||||
return (
|
||||
<CodeBlock
|
||||
className="[&_code]:text-foreground"
|
||||
value={command}
|
||||
hideLineNumbers
|
||||
language="bash"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodexAddServerContent
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { CodeBlock } from 'ui'
|
||||
|
||||
import type { StepContentProps } from '../../../../../Connect.types'
|
||||
|
||||
function CodexAuthenticateContent(_props: StepContentProps) {
|
||||
const command = 'codex mcp login supabase'
|
||||
|
||||
return (
|
||||
<CodeBlock
|
||||
className="[&_code]:text-foreground"
|
||||
value={command}
|
||||
hideLineNumbers
|
||||
language="bash"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodexAuthenticateContent
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { CodeBlock } from 'ui'
|
||||
|
||||
import type { StepContentProps } from '../../../../../Connect.types'
|
||||
|
||||
function CodexEnableRemoteContent(_props: StepContentProps) {
|
||||
const configContent = `[mcp]
|
||||
remote_mcp_client_enabled = true`
|
||||
|
||||
return (
|
||||
<CodeBlock
|
||||
className="[&_code]:text-foreground"
|
||||
value={configContent}
|
||||
hideLineNumbers
|
||||
language="toml"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodexEnableRemoteContent
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import type { StepContentProps } from '../../../../../Connect.types'
|
||||
|
||||
function CodexVerifyContent(_props: StepContentProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Run <code className="text-xs bg-surface-300 px-1 py-0.5 rounded">/mcp</code> inside Codex to
|
||||
verify authentication.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodexVerifyContent
|
||||
@@ -1,90 +0,0 @@
|
||||
import { useParams } from 'common'
|
||||
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
|
||||
import { useTrack } from 'lib/telemetry/track'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
createMcpCopyHandler,
|
||||
FEATURE_GROUPS_NON_PLATFORM,
|
||||
FEATURE_GROUPS_PLATFORM,
|
||||
getMcpUrl,
|
||||
MCP_CLIENTS,
|
||||
McpConfigurationDisplay,
|
||||
} from 'ui-patterns/McpUrlBuilder'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import type { StepContentProps } from '../../../../Connect.types'
|
||||
|
||||
function McpCursorContent({ state, projectKeys }: StepContentProps) {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
if (!projectRef) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <McpCursorContentInner projectRef={projectRef} projectKeys={projectKeys} state={state} />
|
||||
}
|
||||
|
||||
function McpCursorContentInner({
|
||||
projectRef,
|
||||
projectKeys,
|
||||
state,
|
||||
}: {
|
||||
projectRef: string
|
||||
projectKeys: StepContentProps['projectKeys']
|
||||
state: StepContentProps['state']
|
||||
}) {
|
||||
const track = useTrack()
|
||||
|
||||
const selectedClient = useMemo(() => {
|
||||
const clientKey = String(state.mcpClient ?? '')
|
||||
return MCP_CLIENTS.find((c) => c.key === clientKey) ?? MCP_CLIENTS[0]
|
||||
}, [state.mcpClient])
|
||||
|
||||
const readonly = Boolean(state.mcpReadonly)
|
||||
const selectedFeatures = Array.isArray(state.mcpFeatures) ? state.mcpFeatures : []
|
||||
|
||||
const selectedFeaturesSupported = useMemo(() => {
|
||||
const supportedFeatures = IS_PLATFORM ? FEATURE_GROUPS_PLATFORM : FEATURE_GROUPS_NON_PLATFORM
|
||||
return selectedFeatures.filter((feature) =>
|
||||
supportedFeatures.some((group) => group.id === feature)
|
||||
)
|
||||
}, [selectedFeatures])
|
||||
|
||||
const handleCopy = useMemo(
|
||||
() =>
|
||||
createMcpCopyHandler({
|
||||
selectedClient,
|
||||
source: 'studio',
|
||||
onTrack: (event) => track(event.action, event.properties, event.groups),
|
||||
projectRef,
|
||||
}),
|
||||
[selectedClient, track, projectRef]
|
||||
)
|
||||
|
||||
const { clientConfig } = getMcpUrl({
|
||||
projectRef,
|
||||
isPlatform: IS_PLATFORM,
|
||||
apiUrl: projectKeys.apiUrl ?? undefined,
|
||||
readonly,
|
||||
features: selectedFeaturesSupported,
|
||||
selectedClient,
|
||||
})
|
||||
|
||||
return (
|
||||
<McpConfigurationDisplay
|
||||
className="space-y-4"
|
||||
theme="dark"
|
||||
basePath={BASE_PATH}
|
||||
selectedClient={selectedClient}
|
||||
clientConfig={clientConfig}
|
||||
onCopyCallback={handleCopy}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpCursorContent
|
||||
@@ -1,36 +0,0 @@
|
||||
import { CodeBlock } from 'ui'
|
||||
|
||||
import type { StepContentProps } from '../../../Connect.types'
|
||||
|
||||
const ORM_INSTALL_COMMANDS: Record<string, string[]> = {
|
||||
prisma: ['npm install prisma --save-dev', 'npx prisma init'],
|
||||
drizzle: ['npm install drizzle-orm', 'npm install drizzle-kit --save-dev'],
|
||||
}
|
||||
|
||||
function OrmInstallContent({ state }: StepContentProps) {
|
||||
const ormKey = String(state.orm ?? '')
|
||||
const commands = ORM_INSTALL_COMMANDS[ormKey]
|
||||
|
||||
if (!commands?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{commands.map((cmd, index) => (
|
||||
<CodeBlock
|
||||
key={index}
|
||||
className="[&_code]:text-foreground"
|
||||
wrapperClassName="lg:col-span-2"
|
||||
value={cmd}
|
||||
hideLineNumbers
|
||||
language="bash"
|
||||
>
|
||||
{cmd}
|
||||
</CodeBlock>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrmInstallContent
|
||||
@@ -30,10 +30,11 @@ describe('useConnectState', () => {
|
||||
|
||||
test('should accept initial state override', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
|
||||
useConnectState({ framework: 'react', frameworkVariant: 'vite' })
|
||||
)
|
||||
expect(result.current.state.mode).toBe('direct')
|
||||
expect(result.current.state.connectionMethod).toBe('transaction')
|
||||
expect(result.current.state.mode).toBe('framework')
|
||||
expect(result.current.state.framework).toBe('react')
|
||||
expect(result.current.state.frameworkVariant).toBe('vite')
|
||||
})
|
||||
|
||||
test('should merge initial state with defaults', () => {
|
||||
@@ -48,50 +49,7 @@ describe('useConnectState', () => {
|
||||
// ============================================================================
|
||||
|
||||
describe('setMode', () => {
|
||||
test('should switch to direct mode', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
act(() => {
|
||||
result.current.setMode('direct')
|
||||
})
|
||||
|
||||
expect(result.current.state.mode).toBe('direct')
|
||||
})
|
||||
|
||||
test('should initialize direct mode defaults when switching', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
act(() => {
|
||||
result.current.setMode('direct')
|
||||
})
|
||||
|
||||
expect(result.current.state.connectionMethod).toBeDefined()
|
||||
expect(result.current.state.connectionType).toBeDefined()
|
||||
})
|
||||
|
||||
test('should switch to orm mode and initialize defaults', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
act(() => {
|
||||
result.current.setMode('orm')
|
||||
})
|
||||
|
||||
expect(result.current.state.mode).toBe('orm')
|
||||
expect(result.current.state.orm).toBe('prisma')
|
||||
})
|
||||
|
||||
test('should switch to mcp mode and initialize defaults', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
act(() => {
|
||||
result.current.setMode('mcp')
|
||||
})
|
||||
|
||||
expect(result.current.state.mode).toBe('mcp')
|
||||
expect(result.current.state.mcpClient).toBeDefined()
|
||||
})
|
||||
|
||||
test('should preserve framework state when switching back to framework mode', () => {
|
||||
test('should preserve framework state when setting framework mode', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
// Change framework
|
||||
@@ -99,12 +57,7 @@ describe('useConnectState', () => {
|
||||
result.current.updateField('framework', 'react')
|
||||
})
|
||||
|
||||
// Switch to direct
|
||||
act(() => {
|
||||
result.current.setMode('direct')
|
||||
})
|
||||
|
||||
// Switch back to framework
|
||||
// Set framework mode again
|
||||
act(() => {
|
||||
result.current.setMode('framework')
|
||||
})
|
||||
@@ -167,44 +120,6 @@ describe('useConnectState', () => {
|
||||
expect(result.current.state.library).toBe('supabasejs')
|
||||
})
|
||||
|
||||
test('should update connection method', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
|
||||
|
||||
act(() => {
|
||||
result.current.updateField('connectionMethod', 'transaction')
|
||||
})
|
||||
|
||||
expect(result.current.state.connectionMethod).toBe('transaction')
|
||||
})
|
||||
|
||||
test('should clear useSharedPooler when connectionMethod changes to direct', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useConnectState({
|
||||
mode: 'direct',
|
||||
connectionMethod: 'transaction',
|
||||
useSharedPooler: true,
|
||||
})
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.updateField('connectionMethod', 'direct')
|
||||
})
|
||||
|
||||
// useSharedPooler is cleared because it depends on connectionMethod: ['transaction']
|
||||
// When the dependency is not satisfied, the field is removed from state
|
||||
expect(result.current.state.useSharedPooler).toBeUndefined()
|
||||
})
|
||||
|
||||
test('should update MCP client', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
|
||||
|
||||
act(() => {
|
||||
result.current.updateField('mcpClient', 'codex')
|
||||
})
|
||||
|
||||
expect(result.current.state.mcpClient).toBe('codex')
|
||||
})
|
||||
|
||||
test('should update boolean fields', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
@@ -214,16 +129,6 @@ describe('useConnectState', () => {
|
||||
|
||||
expect(result.current.state.frameworkUi).toBe(true)
|
||||
})
|
||||
|
||||
test('should update ORM selection', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
|
||||
|
||||
act(() => {
|
||||
result.current.updateField('orm', 'drizzle')
|
||||
})
|
||||
|
||||
expect(result.current.state.orm).toBe('drizzle')
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
@@ -258,47 +163,6 @@ describe('useConnectState', () => {
|
||||
const fieldIds = result.current.activeFields.map((f) => f.id)
|
||||
expect(fieldIds).not.toContain('frameworkUi')
|
||||
})
|
||||
|
||||
test('should return direct mode fields', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
|
||||
|
||||
const fieldIds = result.current.activeFields.map((f) => f.id)
|
||||
expect(fieldIds).toContain('connectionMethod')
|
||||
expect(fieldIds).toContain('connectionType')
|
||||
})
|
||||
|
||||
test('should show useSharedPooler only for transaction connection method', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
|
||||
)
|
||||
|
||||
const fieldIds = result.current.activeFields.map((f) => f.id)
|
||||
expect(fieldIds).toContain('useSharedPooler')
|
||||
})
|
||||
|
||||
test('should hide useSharedPooler for direct connection method', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useConnectState({ mode: 'direct', connectionMethod: 'direct' })
|
||||
)
|
||||
|
||||
const fieldIds = result.current.activeFields.map((f) => f.id)
|
||||
expect(fieldIds).not.toContain('useSharedPooler')
|
||||
})
|
||||
|
||||
test('should return orm mode fields', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
|
||||
|
||||
const fieldIds = result.current.activeFields.map((f) => f.id)
|
||||
expect(fieldIds).toContain('orm')
|
||||
})
|
||||
|
||||
test('should return mcp mode fields', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
|
||||
|
||||
const fieldIds = result.current.activeFields.map((f) => f.id)
|
||||
expect(fieldIds).toContain('mcpClient')
|
||||
expect(fieldIds).toContain('mcpReadonly')
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
@@ -319,28 +183,6 @@ describe('useConnectState', () => {
|
||||
expect(stepIds).toContain('install')
|
||||
})
|
||||
|
||||
test('should resolve different steps for mcp mode', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
|
||||
|
||||
const stepIds = result.current.resolvedSteps.map((s) => s.id)
|
||||
// MCP mode should have configure step
|
||||
expect(stepIds.some((id) => id.includes('configure') || id.includes('mcp'))).toBe(true)
|
||||
})
|
||||
|
||||
test('should resolve different steps for different mcp clients', () => {
|
||||
const { result: cursorResult } = renderHook(() =>
|
||||
useConnectState({ mode: 'mcp', mcpClient: 'cursor' })
|
||||
)
|
||||
const { result: codexResult } = renderHook(() =>
|
||||
useConnectState({ mode: 'mcp', mcpClient: 'codex' })
|
||||
)
|
||||
|
||||
// Codex has more steps than cursor
|
||||
expect(codexResult.current.resolvedSteps.length).toBeGreaterThanOrEqual(
|
||||
cursorResult.current.resolvedSteps.length
|
||||
)
|
||||
})
|
||||
|
||||
test('should include skills install step', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
@@ -348,21 +190,6 @@ describe('useConnectState', () => {
|
||||
expect(stepIds).toContain('install-skills')
|
||||
})
|
||||
|
||||
test('should resolve steps for direct mode', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
|
||||
|
||||
expect(result.current.resolvedSteps.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('should resolve steps for orm mode', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
|
||||
|
||||
expect(result.current.resolvedSteps.length).toBeGreaterThan(0)
|
||||
const stepIds = result.current.resolvedSteps.map((s) => s.id)
|
||||
expect(stepIds).toContain('install')
|
||||
expect(stepIds).toContain('configure')
|
||||
})
|
||||
|
||||
test('should resolve shadcn steps when frameworkUi is true', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useConnectState({ framework: 'nextjs', frameworkUi: true })
|
||||
@@ -403,41 +230,6 @@ describe('useConnectState', () => {
|
||||
expect(options).toEqual([])
|
||||
})
|
||||
|
||||
test('should return connection method options', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
|
||||
|
||||
const options = result.current.getFieldOptions('connectionMethod')
|
||||
expect(options.length).toBeGreaterThan(0)
|
||||
expect(options.some((o) => o.value === 'direct')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'transaction')).toBe(true)
|
||||
})
|
||||
|
||||
test('should return connection type options', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
|
||||
|
||||
const options = result.current.getFieldOptions('connectionType')
|
||||
expect(options.length).toBeGreaterThan(0)
|
||||
expect(options.some((o) => o.value === 'uri')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'psql')).toBe(true)
|
||||
})
|
||||
|
||||
test('should return ORM options', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
|
||||
|
||||
const options = result.current.getFieldOptions('orm')
|
||||
expect(options.length).toBeGreaterThan(0)
|
||||
expect(options.some((o) => o.value === 'prisma')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'drizzle')).toBe(true)
|
||||
})
|
||||
|
||||
test('should return MCP client options', () => {
|
||||
const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
|
||||
|
||||
const options = result.current.getFieldOptions('mcpClient')
|
||||
expect(options.length).toBeGreaterThan(0)
|
||||
expect(options.some((o) => o.value === 'cursor')).toBe(true)
|
||||
})
|
||||
|
||||
test('should return empty array for unknown field', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
@@ -473,10 +265,7 @@ describe('useConnectState', () => {
|
||||
const { result } = renderHook(() => useConnectState())
|
||||
|
||||
const modeIds = result.current.schema.modes.map((m) => m.id)
|
||||
expect(modeIds).toContain('framework')
|
||||
expect(modeIds).toContain('direct')
|
||||
expect(modeIds).toContain('orm')
|
||||
expect(modeIds).toContain('mcp')
|
||||
expect(modeIds).toEqual(['framework'])
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user