just the button

This commit is contained in:
Saxon Fletcher committed 2026-02-02 10:30:44 +10:00
1 parent 0a7ae0d1d8
commit dbbbfed3f8
58 files changed
+8 -7326

No files matched your search

@@ -1,412 +0,0 @@
import { DOCS_URL } from 'lib/constants'
import { CodeBlockLang } from 'ui'
export type DatabaseConnectionType =
| 'uri'
| 'psql'
| 'golang'
| 'jdbc'
| 'dotnet'
| 'nodejs'
| 'php'
| 'python'
| 'sqlalchemy'
export const DATABASE_CONNECTION_TYPES: {
id: DatabaseConnectionType
label: string
contentType: 'input' | 'code'
lang: CodeBlockLang
fileTitle: string | undefined
}[] = [
{ id: 'uri', label: 'URI', contentType: 'input', lang: 'bash', fileTitle: undefined },
{ id: 'psql', label: 'PSQL', contentType: 'code', lang: 'bash', fileTitle: undefined },
{ id: 'golang', label: 'Golang', contentType: 'code', lang: 'go', fileTitle: '.env' },
{ id: 'jdbc', label: 'JDBC', contentType: 'input', lang: 'bash', fileTitle: undefined },
{
id: 'dotnet',
label: '.NET',
contentType: 'code',
lang: 'csharp',
fileTitle: 'appsettings.json',
},
{ id: 'nodejs', label: 'Node.js', contentType: 'code', lang: 'js', fileTitle: '.env' },
{ id: 'php', label: 'PHP', contentType: 'code', lang: 'php', fileTitle: '.env' },
{ id: 'python', label: 'Python', contentType: 'code', lang: 'python', fileTitle: '.env' },
{ id: 'sqlalchemy', label: 'SQLAlchemy', contentType: 'code', lang: 'python', fileTitle: '.env' },
]
export const CONNECTION_PARAMETERS = {
host: {
key: 'host',
description: 'The hostname of your database',
},
port: {
key: 'port',
description: 'Port number for the connection',
},
database: {
key: 'database',
description: 'Default database name',
},
user: {
key: 'user',
description: 'Database user',
},
pool_mode: {
key: 'pool_mode',
description: 'Connection pooling behavior',
},
} as const
export type ConnectionType = {
key: string
icon: string
label: string
guideLink?: string
children: ConnectionType[]
files?: {
name: string
content: string
}[]
}
export const FRAMEWORKS: ConnectionType[] = [
{
key: 'nextjs',
label: 'Next.js',
icon: 'nextjs',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/nextjs`,
children: [
{
key: 'app',
label: 'App Router',
icon: '',
children: [
{
key: 'supabasejs',
label: 'supabase-js',
icon: 'supabase',
children: [],
},
],
},
{
key: 'pages',
label: 'Pages Router',
icon: '',
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
],
},
{
key: 'remix',
label: 'Remix',
icon: 'remix',
guideLink: `${DOCS_URL}/guides/auth/server-side/creating-a-client?framework=remix&environment=remix-loader`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'react',
label: 'React',
icon: 'react',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/reactjs`,
children: [
{
key: 'create-react-app',
label: 'Create React App',
icon: 'react',
children: [
{
key: 'supabasejs',
label: 'supabase-js',
icon: 'supabase',
children: [],
},
],
},
{
key: 'vite',
label: 'Vite',
icon: 'vite',
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
],
},
{
key: 'nuxt',
label: 'Nuxt',
icon: 'nuxt',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/nuxtjs`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'vuejs',
label: 'Vue.JS',
icon: 'vuejs',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/vue`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'sveltekit',
label: 'SvelteKit',
icon: 'sveltekit',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/sveltekit`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'solidjs',
label: 'Solid.js',
icon: 'solidjs',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/solidjs`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'astro',
label: 'Astro',
icon: 'astro',
guideLink: 'https://docs.astro.build/en/guides/backend/supabase/',
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'refine',
label: 'Refine',
icon: 'refine',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/refine`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'tanstack',
label: 'TanStack Start',
icon: 'tanstack',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/tanstack`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'flask',
label: 'Flask (Python)',
icon: 'python',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/flask`,
children: [
{
key: 'supabasepy',
label: 'supabase-py',
children: [],
icon: 'supabase',
},
],
},
]
export const MOBILES: ConnectionType[] = [
{
key: 'exporeactnative',
label: 'Expo React Native',
icon: 'expo',
guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/expo-react-native`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'flutter',
label: 'Flutter',
icon: 'flutter',
guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-flutter`,
children: [
{
key: 'supabaseflutter',
label: 'supabase-flutter',
children: [],
icon: 'supabase',
},
],
},
{
key: 'ionicreact',
label: 'Ionic React',
icon: 'react',
guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-ionic-react`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'swift',
label: 'Swift',
icon: 'swift',
guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-swift`,
children: [
{
key: 'supabaseswift',
label: 'supabase-swift',
children: [],
icon: 'supabase',
},
],
},
{
key: 'androidkotlin',
label: 'Android Kotlin',
icon: 'kotlin',
guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-kotlin`,
children: [
{
key: 'supabasekt',
label: 'supabase-kt',
children: [],
icon: 'supabase',
},
],
},
{
key: 'ionicangular',
label: 'Ionic Angular',
icon: 'ionic-angular',
guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-ionic-angular`,
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
]
export const ORMS: ConnectionType[] = [
{
key: 'prisma',
label: 'Prisma',
icon: 'prisma',
guideLink: 'https://supabase.com/partners/integrations/prisma',
children: [],
},
{
key: 'drizzle',
label: 'Drizzle',
icon: 'drizzle',
guideLink: `${DOCS_URL}/guides/database/connecting-to-postgres#connecting-with-drizzle`,
children: [],
},
]
export const CONNECTION_TYPES = [
{ key: 'direct', label: 'Connection String', obj: [] },
{ key: 'frameworks', label: 'App Frameworks', obj: FRAMEWORKS },
{ key: 'mobiles', label: 'Mobile Frameworks', obj: MOBILES },
{ key: 'orms', label: 'ORMs', obj: ORMS },
{ key: 'mcp', label: 'MCP', obj: [] },
]
export const PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT =
'Purchase IPv4 add-on or use Shared Pooler if on a IPv4 network'
export const IPV4_ADDON_TEXT = 'Connections are IPv4 proxied with IPv4 add-on'
export type ConnectionStringMethod = 'direct' | 'transaction' | 'session'
export const connectionStringMethodOptions: Record<
ConnectionStringMethod,
{ value: string; label: string; description: string }
> = {
direct: {
value: 'direct',
label: 'Direct connection',
description:
'Ideal for applications with persistent and long-lived connections, such as those running on virtual machines or long-standing containers.',
},
transaction: {
value: 'transaction',
label: 'Transaction pooler',
description:
'Ideal for stateless applications like serverless functions where each interaction with Postgres is brief and isolated.',
},
session: {
value: 'session',
label: 'Session pooler',
description:
'Only recommended as an alternative to Direct Connection, when connecting via an IPv4 network.',
},
}
@@ -1,187 +0,0 @@
// ============================================================================
// Project Keys (existing)
// ============================================================================
export type ProjectKeys = {
apiUrl: string | null
anonKey: string | null
publishableKey: string | null
}
/** @deprecated Use ProjectKeys instead */
export type projectKeys = ProjectKeys
// ============================================================================
// Connection Strings
// ============================================================================
export interface ConnectionStringPooler {
transactionShared: string
sessionShared: string
transactionDedicated?: string
sessionDedicated?: string
ipv4SupportedForDedicatedPooler: boolean
direct?: string
}
/** @deprecated Use ConnectionStringPooler instead */
export type ConnectionStrings = ConnectionStringPooler
/** @deprecated Use StepContentProps instead */
export interface ContentFileProps {
projectKeys: {
apiUrl: string
anonKey?: string
publishableKey?: string
}
connectionStringPooler: {
transactionShared: string
sessionShared: string
transactionDedicated?: string
sessionDedicated?: string
ipv4SupportedForDedicatedPooler: boolean
direct?: string
}
connectionTab: 'App Frameworks' | 'Mobile Frameworks' | 'ORMs'
onCopy?: () => void
}
// ============================================================================
// Schema Types - Conditional Resolution
// ============================================================================
/**
* A value that can be resolved conditionally based on state.
* Resolution walks the tree using state values, falling back to DEFAULT.
*
* Example:
* {
* framework: {
* nextjs: 'NextJsContent',
* react: 'ReactContent',
* DEFAULT: 'GenericFrameworkContent'
* },
* direct: 'DirectContent',
* DEFAULT: null
* }
*/
export type ConditionalValue<T> =
| T
| {
[stateValue: string]: ConditionalValue<T> | undefined
DEFAULT?: T
}
// ============================================================================
// Schema Types - Modes
// ============================================================================
export type ConnectMode = 'framework' | 'direct' | 'orm' | 'mcp'
export interface ModeDefinition {
id: ConnectMode
label: string
description: string
icon?: string
fields: string[] // References to field IDs
}
// ============================================================================
// Schema Types - Fields
// ============================================================================
export type FieldType = 'select' | 'radio-grid' | 'radio-list' | 'switch' | 'multi-select'
export interface FieldOption {
value: string
label: string
icon?: string
description?: string
}
export interface FieldDefinition {
id: string
type: FieldType
label: string
description?: string
// Options can be static, or reference a data source, or be conditional
options?: FieldOption[] | { source: string } | ConditionalValue<FieldOption[]>
// Only show this field when these state conditions are met
dependsOn?: Record<string, string[]>
// Default value for this field
defaultValue?: string | boolean | string[]
}
// ============================================================================
// Schema Types - Steps
// ============================================================================
export interface StepDefinition {
id: string
title: string
description: string
// Component identifier or content file path, can be conditional
content: ConditionalValue<string | null>
}
export type StepTree =
| StepDefinition[]
| {
[fieldId: string]: StepFieldValueMap
}
export type StepFieldValueMap = {
[fieldValue: string]: StepTree | undefined
DEFAULT?: StepTree
}
// ============================================================================
// Schema Types - Main Schema
// ============================================================================
export interface ConnectSchema {
modes: ModeDefinition[]
fields: Record<string, FieldDefinition>
// Steps are fully conditional based on state
steps: StepTree
}
// ============================================================================
// State Types
// ============================================================================
export interface ConnectState {
mode: ConnectMode
[fieldId: string]: string | boolean | string[]
}
export interface ResolvedStep {
id: string
title: string
description: string
content: string // Resolved component identifier
}
export interface ResolvedField extends FieldDefinition {
resolvedOptions: FieldOption[]
}
// ============================================================================
// Step Content Props - Unified props for all step content components
// ============================================================================
/**
* Props passed to all step content components.
* Components receive full state and can conditionally render whatever they need.
*/
export interface StepContentProps {
state: ConnectState
projectKeys: ProjectKeys
connectionStringPooler: ConnectionStringPooler
}
/** @deprecated Use StepContentProps instead */
export interface StepComponentProps {
state: ConnectState
projectKeys: ProjectKeys
}
@@ -1,223 +0,0 @@
import { describe, test, expect } from 'vitest'
import { resolveFrameworkLibraryKey } from './Connect.utils'
describe('Connect.utils:resolveFrameworkLibraryKey', () => {
test('should return null if no framework provided', () => {
const result = resolveFrameworkLibraryKey({
framework: undefined,
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBeNull()
})
test('should return null for empty framework string', () => {
const result = resolveFrameworkLibraryKey({
framework: '',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBeNull()
})
test('should return explicit library if provided', () => {
const result = resolveFrameworkLibraryKey({
framework: 'nextjs',
frameworkVariant: 'app',
library: 'custom-library',
})
expect(result).toBe('custom-library')
})
test('should resolve library for Next.js App Router', () => {
const result = resolveFrameworkLibraryKey({
framework: 'nextjs',
frameworkVariant: 'app',
library: undefined,
})
// Next.js App Router has supabasejs as its library
expect(result).toBe('supabasejs')
})
test('should resolve library for Next.js Pages Router', () => {
const result = resolveFrameworkLibraryKey({
framework: 'nextjs',
frameworkVariant: 'pages',
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should resolve library for React with Vite variant', () => {
const result = resolveFrameworkLibraryKey({
framework: 'react',
frameworkVariant: 'vite',
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should resolve library for React with Create React App variant', () => {
const result = resolveFrameworkLibraryKey({
framework: 'react',
frameworkVariant: 'create-react-app',
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should resolve library for framework without variants (Remix)', () => {
const result = resolveFrameworkLibraryKey({
framework: 'remix',
frameworkVariant: undefined,
library: undefined,
})
// Remix has single child which is supabasejs
expect(result).toBe('supabasejs')
})
test('should resolve library for Flutter', () => {
const result = resolveFrameworkLibraryKey({
framework: 'flutter',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabaseflutter')
})
test('should resolve library for Swift', () => {
const result = resolveFrameworkLibraryKey({
framework: 'swift',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabaseswift')
})
test('should resolve library for Android Kotlin', () => {
const result = resolveFrameworkLibraryKey({
framework: 'androidkotlin',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasekt')
})
test('should resolve library for Flask (Python)', () => {
const result = resolveFrameworkLibraryKey({
framework: 'flask',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasepy')
})
test('should fallback to first variant library when variant not specified for multi-variant framework', () => {
const result = resolveFrameworkLibraryKey({
framework: 'nextjs',
frameworkVariant: undefined,
library: undefined,
})
// Should get library from first variant (app router)
expect(result).toBe('supabasejs')
})
test('should return null for unknown framework', () => {
const result = resolveFrameworkLibraryKey({
framework: 'unknown-framework',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBeNull()
})
test('should handle SvelteKit framework', () => {
const result = resolveFrameworkLibraryKey({
framework: 'sveltekit',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should handle Nuxt framework', () => {
const result = resolveFrameworkLibraryKey({
framework: 'nuxt',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should handle Vue.js framework', () => {
const result = resolveFrameworkLibraryKey({
framework: 'vuejs',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should handle Solid.js framework', () => {
const result = resolveFrameworkLibraryKey({
framework: 'solidjs',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should handle Astro framework', () => {
const result = resolveFrameworkLibraryKey({
framework: 'astro',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should handle Expo React Native', () => {
const result = resolveFrameworkLibraryKey({
framework: 'exporeactnative',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should handle Ionic React', () => {
const result = resolveFrameworkLibraryKey({
framework: 'ionicreact',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should handle Ionic Angular', () => {
const result = resolveFrameworkLibraryKey({
framework: 'ionicangular',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should handle Refine framework', () => {
const result = resolveFrameworkLibraryKey({
framework: 'refine',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
test('should handle TanStack Start framework', () => {
const result = resolveFrameworkLibraryKey({
framework: 'tanstack',
frameworkVariant: undefined,
library: undefined,
})
expect(result).toBe('supabasejs')
})
})
@@ -1,37 +0,0 @@
import { FRAMEWORKS, MOBILES } from './Connect.constants'
type FieldValue = string | boolean | string[] | undefined
interface FrameworkLibraryInput {
framework?: FieldValue
frameworkVariant?: FieldValue
library?: FieldValue
[key: string]: FieldValue
}
export function resolveFrameworkLibraryKey(state: FrameworkLibraryInput): string | null {
const { framework, frameworkVariant, library } = state
if (!framework) return null
if (library) return String(library)
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
const selectedFramework = allFrameworks.find((f) => f.key === framework)
if (!selectedFramework?.children?.length) return null
if (frameworkVariant) {
const variant = selectedFramework.children.find((c) => c.key === frameworkVariant)
if (variant?.children?.length) {
return variant.children[0].key
}
}
const firstChild = selectedFramework.children[0]
if (firstChild?.children?.length) {
return firstChild.children[0].key
}
return firstChild?.key ?? null
}
@@ -1,313 +0,0 @@
import { BASE_PATH } from 'lib/constants'
import { Box, Cable, Database, Sparkles } from 'lucide-react'
import { useTheme } from 'next-themes'
import {
cn,
RadioGroupStacked,
RadioGroupStackedItem,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Switch,
} from 'ui'
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
import { ClientSelectDropdown, MCP_CLIENTS } from 'ui-patterns/McpUrlBuilder'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
import { FrameworkSelector } from '../HomeNew/GettingStarted/FrameworkSelector'
import { FRAMEWORKS, MOBILES } from './Connect.constants'
import type { ConnectMode, FieldOption, ResolvedField } from './Connect.types'
import { ConnectionIcon } from './ConnectionIcon'
// Icon mapping for modes
const MODE_ICONS: Record<string, React.ReactNode> = {
framework: <Box size={16} strokeWidth={1.5} />,
direct: <Database size={16} strokeWidth={1.5} />,
orm: <Cable size={16} strokeWidth={1.5} />,
mcp: <Sparkles size={16} strokeWidth={1.5} />,
}
interface ConnectConfigSectionProps {
activeFields: ResolvedField[]
state: Record<string, string | boolean | string[]>
onFieldChange: (fieldId: string, value: string | boolean | string[]) => void
getFieldOptions: (fieldId: string) => FieldOption[]
}
export function ConnectConfigSection({
activeFields,
state,
onFieldChange,
getFieldOptions,
}: ConnectConfigSectionProps) {
const { resolvedTheme } = useTheme()
const formLayoutClassName =
'md:gap-8 md:[&>div:first-child]:!w-[calc(50%-16px)] md:[&_[data-formlayout-id=labelContainer]]:!w-[calc(50%-16px)]'
if (activeFields.length === 0) return null
// Get all frameworks for the combobox
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
return (
<div className="space-y-6">
{activeFields.map((field) => {
const options = getFieldOptions(field.id)
const value = state[field.id]
// Skip fields with no options (or single option that's auto-selected)
// Exception: switch and multi-select fields don't require options
if (field.type !== 'switch' && field.type !== 'multi-select') {
if (options.length === 0) return null
if (options.length === 1) return null
}
switch (field.type) {
case 'radio-grid':
// For framework field, use the combobox selector
if (field.id === 'framework') {
return (
<FormLayout
key={field.id}
layout="flex-row-reverse"
label={field.label}
description="What is your preferred coding agent"
className={formLayoutClassName}
>
<FrameworkSelector
value={String(value ?? '')}
onChange={(v) => onFieldChange(field.id, v)}
items={allFrameworks}
className="w-full"
size="small"
/>
</FormLayout>
)
}
// Default radio-grid behavior for other fields
return (
<FormLayout
key={field.id}
layout="flex-row-reverse"
label={field.label}
className={formLayoutClassName}
>
<RadioGroupStacked
value={String(value ?? '')}
onValueChange={(v) => onFieldChange(field.id, v)}
className="flex-row gap-3 space-y-0"
>
{options.map((option) => (
<RadioGroupStackedItem
key={option.value}
id={`connect-${field.id}-${option.value}`}
value={option.value}
label=""
className="flex-1 rounded-lg text-left"
>
<div className="flex items-center gap-2">
{option.icon && <ConnectionIcon icon={option.icon} />}
<span className="text-sm">{option.label}</span>
</div>
</RadioGroupStackedItem>
))}
</RadioGroupStacked>
</FormLayout>
)
case 'radio-list':
return (
<FormLayout
key={field.id}
layout="flex-row-reverse"
label={field.label}
className={formLayoutClassName}
>
<RadioGroupStacked
value={String(value ?? '')}
onValueChange={(v) => onFieldChange(field.id, v)}
>
{options.map((option) => (
<RadioGroupStackedItem
key={option.value}
id={`connect-${field.id}-${option.value}`}
value={option.value}
label=""
className="w-full text-left"
>
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-2">
{option.icon && <ConnectionIcon icon={option.icon} />}
<span className="text-sm">{option.label}</span>
</div>
{option.description && (
<span className="text-sm text-foreground-lighter">
{option.description}
</span>
)}
</div>
</RadioGroupStackedItem>
))}
</RadioGroupStacked>
</FormLayout>
)
case 'select':
// Special case for MCP client - use the custom dropdown
if (field.id === 'mcpClient') {
const selectedClient = MCP_CLIENTS.find((c) => c.key === value) ?? MCP_CLIENTS[0]
return (
<FormLayout
key={field.id}
layout="flex-row-reverse"
label={field.label}
description={field.description}
className={formLayoutClassName}
>
<ClientSelectDropdown
basePath={BASE_PATH}
theme={(resolvedTheme ?? 'dark') as 'light' | 'dark'}
label=""
className="w-full"
clients={MCP_CLIENTS}
selectedClient={selectedClient}
onClientChange={(v) => onFieldChange(field.id, v)}
/>
</FormLayout>
)
}
return (
<FormLayout
key={field.id}
layout="flex-row-reverse"
label={field.label}
description={field.description}
className={formLayoutClassName}
>
<Select_Shadcn_
value={String(value ?? '')}
onValueChange={(v) => onFieldChange(field.id, v)}
>
<SelectTrigger_Shadcn_ size="small" className="w-full">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{options.map((option) => (
<SelectItem_Shadcn_ key={option.value} value={option.value}>
{option.label}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormLayout>
)
case 'switch':
return (
<FormLayout
key={field.id}
layout="flex-row-reverse"
label={field.label}
description={field.description}
className={formLayoutClassName}
>
<Switch
id={field.id}
checked={Boolean(value)}
onCheckedChange={(v) => onFieldChange(field.id, v)}
/>
</FormLayout>
)
case 'multi-select':
return (
<FormLayout
key={field.id}
layout="flex-row-reverse"
label={field.label}
description={field.description}
className={formLayoutClassName}
>
<MultiSelector
values={Array.isArray(value) ? value : []}
onValuesChange={(v) => onFieldChange(field.id, v)}
>
<MultiSelectorTrigger
className="w-full"
label="All features except Storage enabled by default"
badgeLimit="wrap"
showIcon={true}
/>
<MultiSelectorContent>
<MultiSelectorList>
{options.map((option) => (
<MultiSelectorItem key={option.value} value={option.value}>
<div className="flex flex-col">
<span className="font-medium">{option.label}</span>
{option.description && (
<span className="text-xs text-foreground-light">
{option.description}
</span>
)}
</div>
</MultiSelectorItem>
))}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
</FormLayout>
)
default:
return null
}
})}
</div>
)
}
// ============================================================================
// Mode Selector
// ============================================================================
interface ModeSelectorProps {
modes: Array<{ id: ConnectMode; label: string; description: string }>
selected: ConnectMode
onChange: (mode: ConnectMode) => void
}
export function ModeSelector({ modes, selected, onChange }: ModeSelectorProps) {
return (
<div className="grid grid-cols-4 rounded-lg border">
{modes.map((mode) => (
<button
key={mode.id}
type="button"
onClick={() => onChange(mode.id)}
className={cn(
'flex flex-col items-center gap-2 p-4 transition-colors border-r last:border-r-0',
selected === mode.id
? 'bg-surface-200'
: 'border-default hover:border-strong hover:bg-surface-100 '
)}
>
<span className="text-foreground-light">{MODE_ICONS[mode.id]}</span>
<div>
<p className="heading-default text-center">{mode.label}</p>
<p className="text-sm text-foreground-lighter text-center">{mode.description}</p>
</div>
</button>
))}
</div>
)
}
@@ -1,18 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { DatabaseSelector } from 'components/ui/DatabaseSelector'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useEffect, useMemo } from 'react'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, cn } from 'ui'
import type { ProjectKeys } from './Connect.types'
import { ConnectConfigSection, ModeSelector } from './ConnectConfigSection'
import { ConnectStepsSection } from './ConnectStepsSection'
import { useConnectState } from './useConnectState'
interface ConnectSheetProps {
open: boolean
onOpenChange: (open: boolean) => void
@@ -20,83 +7,6 @@ interface ConnectSheetProps {
}
export const ConnectSheet = ({ open, onOpenChange, connectTab }: ConnectSheetProps) => {
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])
const { state, updateField, setMode, activeFields, resolvedSteps, getFieldOptions, schema } =
useConnectState()
// Filter modes based on feature flags
const availableModes = useMemo(
() => schema.modes.filter((m) => availableModeIds.includes(m.id)),
[schema.modes, availableModeIds]
)
// Project keys for step components
const { ref: projectRef } = useParams()
const { data: settings } = useProjectSettingsV2Query({ projectRef }, { enabled: open })
const { can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
)
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const { anonKey, publishableKey } = canReadAPIKeys
? getKeys(apiKeys)
: { anonKey: null, publishableKey: null }
const projectKeys: ProjectKeys = useMemo(() => {
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint ?? ''
const apiHost = canReadAPIKeys ? `${protocol}://${endpoint ?? '-'}` : ''
return {
apiUrl: apiHost ?? null,
anonKey: anonKey?.api_key ?? null,
publishableKey: publishableKey?.api_key ?? null,
}
}, [
settings?.app_config?.protocol,
settings?.app_config?.endpoint,
canReadAPIKeys,
anonKey?.api_key,
publishableKey?.api_key,
])
const handleSourceChange = (databaseId: string) => {
// Database selection is handled by the DatabaseSelector's internal state
// We just need to trigger a re-render of connection strings
}
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')
}
}, [connectTab, open, setMode])
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent size="lg" className="flex flex-col gap-0 p-0 space-y-0" tabIndex={undefined}>
@@ -106,36 +16,12 @@ export const ConnectSheet = ({ open, onOpenChange, connectTab }: ConnectSheetPro
<SheetTitle>Connect to your project</SheetTitle>
<SheetDescription>Choose how you want to use Supabase</SheetDescription>
</div>
<div className="w-full lg:w-auto">
<DatabaseSelector
align="end"
buttonProps={{
size: 'small',
className: 'w-full lg:w-auto justify-between pr-2.5 [&_svg]:h-4',
}}
className="w-full [&>span]:w-1/2 [&>span]:md:w-auto"
onSelectId={handleSourceChange}
/>
</div>
</div>
</SheetHeader>
<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} />
<div className="border-t pt-8">
<ConnectConfigSection
activeFields={activeFields}
state={state}
onFieldChange={updateField}
getFieldOptions={getFieldOptions}
/>
</div>
</div>
{/* Steps Section */}
<ConnectStepsSection steps={resolvedSteps} state={state} projectKeys={projectKeys} />
<div className="space-y-6 border-b p-8 shrink-0"></div>
</div>
</SheetContent>
</Sheet>
@@ -1,64 +0,0 @@
import { type ReactNode } from 'react'
import { cn } from 'ui'
interface ConnectSheetStepProps {
number: number
title: string
description: string
children: ReactNode
className?: string
isFirst?: boolean
isLast?: boolean
}
export const ConnectSheetStep = ({
number,
title,
description,
children,
className,
isFirst = false,
isLast = false,
}: ConnectSheetStepProps) => {
const lineTopClass = isFirst ? 'top-1/2' : '-top-6'
const lineBottomClass = isLast ? 'bottom-1/2' : '-bottom-6'
return (
<div
className={cn('py-6', className)}
data-connect-step
data-step-title={title}
data-step-description={description}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-8 items-start">
{/* Left column: number, title, description */}
<div className="flex items-start gap-6 self-stretch">
<div className="relative self-stretch shrink-0 w-6">
<div className="absolute inset-0 flex items-start justify-center">
<div
aria-hidden="true"
className={cn(
'absolute left-[calc(50%-1px)] w-px bg-border opacity-60',
lineTopClass,
lineBottomClass
)}
/>
<div className="relative z-10 flex font-mono text-xs items-center justify-center min-w-6 w-6 h-6 border border-default rounded-md bg-surface-100 text-foreground-light">
{number}
</div>
</div>
</div>
<div className="flex flex-col">
<p className="text-sm font-medium text-foreground">{title}</p>
<p className="text-sm text-foreground-light">{description}</p>
</div>
</div>
{/* Right column: content */}
<div className="lg:pl-0 [&_pre.code-block]:!bg-surface-75" data-step-content>
{children}
</div>
</div>
</div>
)
}
@@ -1,184 +0,0 @@
import dynamic from 'next/dynamic'
import { useMemo, useRef } from 'react'
import { useParams } from 'common'
import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query'
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { pluckObjectFields } from 'lib/helpers'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import type {
ConnectionStringPooler,
ConnectState,
ProjectKeys,
ResolvedStep,
StepContentProps,
} from './Connect.types'
import { ConnectSheetStep } from './ConnectSheetStep'
import { CopyPromptAdmonition } from './CopyPromptAdmonition'
import { getConnectionStrings } from './DatabaseSettings.utils'
interface ConnectStepsSectionProps {
steps: ResolvedStep[]
state: ConnectState
projectKeys: ProjectKeys
}
/**
* Resolves a content path template by replacing {{key}} placeholders with state values.
* Empty segments are filtered out to handle optional state values like frameworkVariant.
*
* Examples:
* - '{{framework}}/{{frameworkVariant}}/{{library}}' with state {framework: 'nextjs', frameworkVariant: 'app', library: 'supabasejs'}
* → 'nextjs/app/supabasejs'
* - '{{orm}}' with state {orm: 'prisma'}
* → 'prisma'
* - 'steps/install' (no templates)
* → 'steps/install'
*/
function resolveContentPath(template: string, state: ConnectState): string {
return template
.replace(/\{\{(\w+)\}\}/g, (_, key) => String(state[key] ?? ''))
.split('/')
.filter(Boolean)
.join('/')
}
/**
* Hook to fetch and prepare connection strings for step content.
*/
function useConnectionStringPooler(): ConnectionStringPooler {
const { ref: projectRef } = useParams()
const { data: selectedOrg } = useSelectedOrganizationQuery()
const allowPgBouncerSelection = useMemo(() => selectedOrg?.plan.id !== 'free', [selectedOrg])
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
const { data: addons } = useProjectAddonsQuery({ projectRef })
const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
const connectionInfo = pluckObjectFields(settings || emptyState, DB_FIELDS)
const poolingConfigurationShared = supavisorConfig?.find((x) => x.database_type === 'PRIMARY')
const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
const connectionStringsShared = getConnectionStrings({
connectionInfo,
poolingInfo: {
connectionString: poolingConfigurationShared?.connection_string ?? '',
db_host: poolingConfigurationShared?.db_host ?? '',
db_name: poolingConfigurationShared?.db_name ?? '',
db_port: poolingConfigurationShared?.db_port ?? 0,
db_user: poolingConfigurationShared?.db_user ?? '',
},
metadata: { projectRef },
})
const connectionStringsDedicated =
poolingConfigurationDedicated !== undefined
? getConnectionStrings({
connectionInfo,
poolingInfo: {
connectionString: poolingConfigurationDedicated.connection_string,
db_host: poolingConfigurationDedicated.db_host,
db_name: poolingConfigurationDedicated.db_name,
db_port: poolingConfigurationDedicated.db_port,
db_user: poolingConfigurationDedicated.db_user,
},
metadata: { projectRef },
})
: undefined
return useMemo(
() => ({
transactionShared: connectionStringsShared.pooler.uri,
sessionShared: connectionStringsShared.pooler.uri.replace('6543', '5432'),
transactionDedicated: connectionStringsDedicated?.pooler.uri,
sessionDedicated: connectionStringsDedicated?.pooler.uri.replace('6543', '5432'),
ipv4SupportedForDedicatedPooler: !!ipv4Addon,
direct: connectionStringsShared.direct.uri,
}),
[connectionStringsShared, connectionStringsDedicated, ipv4Addon]
)
}
/**
* Dynamically loads and renders a content component from the content directory.
* All step content uses this unified loader - no built-in component registry needed.
*/
function StepContent({
contentId,
state,
projectKeys,
connectionStringPooler,
}: {
contentId: string
state: ConnectState
projectKeys: ProjectKeys
connectionStringPooler: ConnectionStringPooler
}) {
// Resolve any template placeholders in the content path
const filePath = useMemo(() => resolveContentPath(contentId, state), [contentId, state])
// Dynamically import the content component
const ContentComponent = useMemo(() => {
return dynamic<StepContentProps>(() => import(`./content/${filePath}/content`), {
loading: () => (
<div className="p-4 min-h-[200px]">
<GenericSkeletonLoader />
</div>
),
})
}, [filePath])
return (
<ContentComponent
state={state}
projectKeys={projectKeys}
connectionStringPooler={connectionStringPooler}
/>
)
}
export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsSectionProps) {
const stepsContainerRef = useRef<HTMLDivElement | null>(null)
const connectionStringPooler = useConnectionStringPooler()
if (steps.length === 0) return null
return (
<div className="bg-muted/50 flex-1">
<div className="p-8">
<h3 className="heading-subTitle mb-6">Connect your app</h3>
<CopyPromptAdmonition stepsContainerRef={stepsContainerRef} />
<div className="space-y-0" ref={stepsContainerRef}>
{steps.map((step, index) => (
<ConnectSheetStep
key={step.id}
number={index + 1}
title={step.title}
description={step.description}
isFirst={index === 0}
isLast={index === steps.length - 1}
>
<StepContent
contentId={step.content}
state={state}
projectKeys={projectKeys}
connectionStringPooler={connectionStringPooler}
/>
</ConnectSheetStep>
))}
</div>
</div>
</div>
)
}
@@ -1,48 +0,0 @@
import { useTheme } from 'next-themes'
import Image from 'next/image'
import { BASE_PATH } from 'lib/constants'
import { cn } from 'ui'
interface ConnectionIconProps {
icon: string
iconFolder?: string
supportsDarkMode?: boolean
size?: number
className?: string
}
export const ConnectionIcon = ({
icon,
iconFolder,
supportsDarkMode,
size = 14,
className,
}: ConnectionIconProps) => {
const { resolvedTheme } = useTheme()
const imageFolder =
iconFolder || (['ionic-angular'].includes(icon) ? 'icons/frameworks' : 'libraries')
const imageExtension = imageFolder === 'icons/frameworks' ? '' : '-icon'
const shouldUseDarkMode =
supportsDarkMode ||
['expo', 'nextjs', 'prisma', 'drizzle', 'astro', 'remix', 'refine'].includes(icon.toLowerCase())
const iconImgSrc = icon.startsWith('http')
? icon
: `${BASE_PATH}/img/${imageFolder}/${icon.toLowerCase()}${
shouldUseDarkMode && resolvedTheme?.includes('dark') ? '-dark' : ''
}${imageExtension}.svg`
return (
<Image
className={cn('transition-all group-hover:scale-110', className)}
src={iconImgSrc}
alt={`${icon} logo`}
width={size}
height={size}
/>
)
}
@@ -1,52 +0,0 @@
import { Check, Copy } from 'lucide-react'
import { useState } from 'react'
import { cn, copyToClipboard } from 'ui'
interface Parameter {
key: string
value: string
}
interface ConnectionParametersProps {
parameters: Parameter[]
}
export const ConnectionParameters = ({ parameters }: ConnectionParametersProps) => {
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
return (
<div className="bg-surface-75 rounded-lg border font-mono text-sm p-4">
{parameters.map((param) => (
<div key={param.key} className="py-0.5 group/param">
<div className="text-xs flex items-center">
<span className="text-foreground-lighter">{param.key}: </span>
<span className="ml-1 text-foreground">{param.value}</span>
<button
onClick={() => {
copyToClipboard(param.value, () => {
setCopiedMap((prev) => ({ ...prev, [param.key]: true }))
setTimeout(() => {
setCopiedMap((prev) => ({ ...prev, [param.key]: false }))
}, 1000)
})
}}
className={cn(
'text-foreground-lighter',
'ml-2 opacity-0 group-hover/param:opacity-100',
'hover:text-foreground rounded-sm p-1',
copiedMap[param.key] && 'opacity-100',
'transition-all'
)}
>
{copiedMap[param.key] ? (
<Check size={12} strokeWidth={1.5} />
) : (
<Copy size={12} strokeWidth={1.5} />
)}
</button>
</div>
</div>
))}
</div>
)
}
@@ -1,88 +0,0 @@
import type { ConnectionStringPooler } from './Connect.types'
import type { ConnectionStringMethod } from './Connect.constants'
export const DEFAULT_PORT = '5432'
export const PASSWORD_PLACEHOLDER = '[YOUR-PASSWORD]'
export type ConnectionParams = {
host: string
port: string
user: string
database: string
}
export const resolveConnectionString = ({
connectionMethod,
useSharedPooler,
connectionStringPooler,
}: {
connectionMethod: ConnectionStringMethod
useSharedPooler: boolean
connectionStringPooler: ConnectionStringPooler
}) => {
if (connectionMethod === 'direct') {
return connectionStringPooler.direct ?? ''
}
if (connectionMethod === 'session') {
return connectionStringPooler.sessionShared ?? ''
}
if (useSharedPooler || !connectionStringPooler.transactionDedicated) {
return connectionStringPooler.transactionShared ?? ''
}
return connectionStringPooler.transactionDedicated ?? ''
}
export const parseConnectionParams = (connectionString: string): ConnectionParams => {
if (!connectionString) {
return {
host: 'hidden',
port: DEFAULT_PORT,
user: 'hidden',
database: 'hidden',
}
}
try {
const parsed = new URL(connectionString)
return {
host: parsed.hostname || 'hidden',
port: parsed.port || DEFAULT_PORT,
user: parsed.username || 'hidden',
database: parsed.pathname?.replace(/^\//, '') || 'hidden',
}
} catch (error) {
return {
host: 'hidden',
port: DEFAULT_PORT,
user: 'hidden',
database: 'hidden',
}
}
}
export const buildSafeConnectionString = (
connectionString: string,
params: ConnectionParams
): string => {
if (!connectionString) return ''
const search = (() => {
try {
return new URL(connectionString).search
} catch (error) {
return ''
}
})()
return `postgresql://${params.user}:${PASSWORD_PLACEHOLDER}@${params.host}:${params.port}/${params.database}${search}`
}
export const buildConnectionParameters = (params: ConnectionParams) => [
{ key: 'host', value: params.host },
{ key: 'port', value: params.port },
{ key: 'database', value: params.database },
{ key: 'user', value: params.user },
]
@@ -1,180 +0,0 @@
import { Copy } from 'lucide-react'
import { useEffect, useRef, useState, type RefObject } from 'react'
import { BASE_PATH } from 'lib/constants'
import { Badge, Button, copyToClipboard } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
interface CopyPromptAdmonitionProps {
stepsContainerRef: RefObject<HTMLDivElement | null>
}
export function CopyPromptAdmonition({ stepsContainerRef }: CopyPromptAdmonitionProps) {
const copyResetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [hasCopiedPrompt, setHasCopiedPrompt] = useState(false)
useEffect(() => {
return () => {
if (copyResetTimerRef.current) {
clearTimeout(copyResetTimerRef.current)
}
}
}, [])
const normalizeTextLines = (value: string) => {
return value
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.join('\n')
}
const getStepTextContent = (contentElement: HTMLElement) => {
const clone = contentElement.cloneNode(true) as HTMLElement
clone
.querySelectorAll('pre, button, svg, input, textarea, select, [aria-hidden="true"]')
.forEach((element) => element.remove())
const text = clone.textContent ?? ''
return normalizeTextLines(text)
}
const getStepCodeSnippets = (contentElement: HTMLElement) => {
const snippets: Array<{ label: string; snippet: string }> = []
const seen = new Set<string>()
const addSnippet = (label: string, snippet: string) => {
if (!snippet || seen.has(snippet)) return
seen.add(snippet)
snippets.push({ label, snippet })
}
const tabContents = Array.from(
contentElement.querySelectorAll('[data-connect-tab-content]')
) as HTMLElement[]
tabContents.forEach((tabContent) => {
const label = tabContent.getAttribute('data-tab-label') || 'Code'
const tabSnippets = Array.from(tabContent.querySelectorAll('pre'))
.map((pre) => pre.textContent?.trim())
.filter((snippet): snippet is string => Boolean(snippet))
if (tabSnippets.length === 0) {
const inlineSnippets = Array.from(tabContent.querySelectorAll('code'))
.filter((code) => !code.closest('pre') && code.closest('.font-mono'))
.map((code) => code.textContent?.trim())
.filter((snippet): snippet is string => Boolean(snippet))
inlineSnippets.forEach((snippet, index) => {
const inlineLabel = inlineSnippets.length > 1 ? `${label} (part ${index + 1})` : label
addSnippet(inlineLabel, snippet)
})
return
}
tabSnippets.forEach((snippet, index) => {
const tabLabel = tabSnippets.length > 1 ? `${label} (part ${index + 1})` : label
addSnippet(tabLabel, snippet)
})
})
contentElement.querySelectorAll('pre').forEach((pre) => {
if (pre.closest('[data-connect-tab-content]')) return
const snippet = pre.textContent?.trim()
if (snippet) addSnippet('Code', snippet)
})
contentElement.querySelectorAll('code').forEach((code) => {
if (code.closest('pre')) return
if (code.closest('[data-connect-tab-content]')) return
if (!code.closest('.font-mono')) return
const snippet = code.textContent?.trim()
if (snippet) addSnippet('Code', snippet)
})
return snippets
}
const handleCopyPrompt = () => {
const stepElements = stepsContainerRef.current?.querySelectorAll('[data-connect-step]')
if (!stepElements?.length) return
const promptContent = Array.from(stepElements)
.map((stepElement, index) => {
const title = stepElement.getAttribute('data-step-title') ?? `Step ${index + 1}`
const description = stepElement.getAttribute('data-step-description') ?? ''
const contentElement = stepElement.querySelector(
'[data-step-content]'
) as HTMLElement | null
const details = contentElement ? getStepTextContent(contentElement) : ''
const codeSnippets = contentElement ? getStepCodeSnippets(contentElement) : []
const sections = [
`${index + 1}. ${title}`,
description,
details ? `Details:\n${details}` : null,
codeSnippets.length
? `Code:\n${codeSnippets
.map(({ label, snippet }) => `File: ${label}\n\`\`\`\n${snippet}\n\`\`\``)
.join('\n\n')}`
: null,
].filter(Boolean)
return sections.join('\n')
})
.join('\n\n')
copyToClipboard(promptContent)
setHasCopiedPrompt(true)
if (copyResetTimerRef.current) {
clearTimeout(copyResetTimerRef.current)
}
copyResetTimerRef.current = setTimeout(() => {
setHasCopiedPrompt(false)
}, 2000)
}
return (
<Admonition
type="tip"
showIcon={false}
layout="horizontal"
className="mb-6"
actions={
<Button type="default" size="small" icon={<Copy size={14} />} onClick={handleCopyPrompt}>
{hasCopiedPrompt ? 'Copied' : 'Copy prompt'}
</Button>
}
>
{/* Background image */}
<div className="absolute -inset-16 z-0 opacity-50">
<img
src={`${BASE_PATH}/img/reports/bg-grafana-dark.svg`}
alt="Supabase Grafana"
className="w-full h-full object-cover object-right hidden dark:block"
/>
<img
src={`${BASE_PATH}/img/reports/bg-grafana-light.svg`}
alt="Supabase Grafana"
className="w-full h-full object-cover object-right dark:hidden"
/>
<div className="absolute inset-0 bg-gradient-to-r from-background-alternative to-transparent" />
</div>
{/* Content */}
<div className="relative z-10 flex flex-col md:flex-row md:items-center gap-y-2 md:gap-x-8 justify-between px-2">
<div className="flex flex-col gap-y-0.5">
<div className="flex flex-col gap-y-2 items-start">
<Badge variant="success" className="-ml-0.5">
Skip the steps
</Badge>
<p className="heading-default">Prompt your agent</p>
</div>
<p className="text-sm text-foreground-lighter text-balance">
Copy a prompt with everything your agent needs to connect your app for you.
</p>
</div>
</div>
</Admonition>
)
}
@@ -1,388 +0,0 @@
type ConnectionStrings = {
psql: string
uri: string
golang: string
jdbc: string
dotnet: string
nodejs: string
php: string
python: string
sqlalchemy: string
}
export const getConnectionStrings = ({
connectionInfo,
poolingInfo,
metadata,
}: {
connectionInfo: {
db_user: string
db_port: number
db_host: string
db_name: string
}
poolingInfo?: {
connectionString: string
db_user: string
db_port: number
db_host: string
db_name: string
}
metadata: {
projectRef?: string
pgVersion?: string
}
}): {
direct: ConnectionStrings
pooler: ConnectionStrings
} => {
const isMd5 = poolingInfo?.connectionString.includes('options=reference')
const { projectRef } = metadata
const password = '[YOUR-PASSWORD]'
// Direct connection variables
const directUser = connectionInfo.db_user
const directPort = connectionInfo.db_port
const directHost = connectionInfo.db_host
const directName = connectionInfo.db_name
// Pooler connection variables
const poolerUser = poolingInfo?.db_user
const poolerPort = poolingInfo?.db_port
const poolerHost = poolingInfo?.db_host
const poolerName = poolingInfo?.db_name
// Direct connection strings
const directPsqlString = isMd5
? `psql "postgresql://${directUser}:${password}@${directHost}:${directPort}/${directName}"`
: `psql -h ${directHost} -p ${directPort} -d ${directName} -U ${directUser}`
const directUriString = `postgresql://${directUser}:${password}@${directHost}:${directPort}/${directName}`
const directGolangString = `DATABASE_URL=${directUriString}`
const directJdbcString = `jdbc:postgresql://${directHost}:${directPort}/${directName}?user=${directUser}&password=${password}`
// User Id=${directUser};Password=${password};Server=${directHost};Port=${directPort};Database=${directName}`
const directDotNetString = `{
"ConnectionStrings": {
"DefaultConnection": "Host=${directHost};Database=${directName};Username=${directUser};Password=${password};SSL Mode=Require;Trust Server Certificate=true"
}
}`
// `User Id=${poolerUser};Password=${password};Server=${poolerHost};Port=${poolerPort};Database=${poolerName}${isMd5 ? `;Options='reference=${projectRef}'` : ''}`
const poolerDotNetString = `{
"ConnectionStrings": {
"DefaultConnection": "User Id=${poolerUser};Password=${password};Server=${poolerHost};Port=${poolerPort};Database=${poolerName}${isMd5 ? `;Options='reference=${projectRef}'` : ''}"
}
}`
const directNodejsString = `DATABASE_URL=${directUriString}`
// Pooler connection strings
const poolerPsqlString = isMd5
? `psql "postgresql://${poolerUser}:${password}@${poolerHost}:${poolerPort}/${poolerName}?options=reference%3D${projectRef}"`
: `psql -h ${poolerHost} -p ${poolerPort} -d ${poolerName} -U ${poolerUser}`
const poolerUriString = poolingInfo?.connectionString ?? ''
const nodejsPoolerUriString = `DATABASE_URL=${poolingInfo?.connectionString}`
const poolerGolangString = `user=${poolerUser}
password=${password}
host=${poolerHost}
port=${poolerPort}
dbname=${poolerName}${isMd5 ? `options=reference=${projectRef}` : ''}`
const poolerJdbcString = `jdbc:postgresql://${poolerHost}:${poolerPort}/${poolerName}?user=${poolerUser}${isMd5 ? `&options=reference%3D${projectRef}` : ''}&password=${password}`
const sqlalchemyString = `user=${directUser}
password=${password}
host=${directHost}
port=${directPort}
dbname=${directName}`
const poolerSqlalchemyString = `user=${poolerUser}
password=${password}
host=${poolerHost}
port=${poolerPort}
dbname=${poolerName}`
return {
direct: {
psql: directPsqlString,
uri: directUriString,
golang: directGolangString,
jdbc: directJdbcString,
dotnet: directDotNetString,
nodejs: directNodejsString,
php: directGolangString,
python: directGolangString,
sqlalchemy: sqlalchemyString,
},
pooler: {
psql: poolerPsqlString,
uri: poolerUriString,
golang: poolerGolangString,
jdbc: poolerJdbcString,
dotnet: poolerDotNetString,
nodejs: nodejsPoolerUriString,
php: poolerGolangString,
python: poolerGolangString,
sqlalchemy: poolerSqlalchemyString,
},
}
}
const DB_USER_DESC = 'Database user (e.g postgres)'
const DB_PASS_DESC = 'Database password'
const DB_NAME_DESC = 'Database name (e.g postgres)'
const PROJECT_REF_DESC = "Project's reference ID"
const PORT_NUMBER_DESC = 'Port number (Use 5432 if using prepared statements)'
// [Joshen] This is to the best of interpreting the syntax from the API response
// // There's different format for PG13 (depending on authentication method being md5) and PG14
export const constructConnStringSyntax = (
connString: string,
{
selectedTab,
usePoolerConnection,
ref,
cloudProvider,
region,
tld,
portNumber,
}: {
selectedTab: 'uri' | 'psql' | 'golang' | 'jdbc' | 'dotnet' | 'nodejs' | 'php' | 'python'
usePoolerConnection: boolean
ref: string
cloudProvider: string
region: string
tld: string
portNumber: string
}
) => {
const isMd5 = connString.includes('options=reference')
const poolerHostDetails = [
{ value: cloudProvider.toLocaleLowerCase(), tooltip: 'Cloud provider' },
{ value: '-0-', tooltip: undefined },
{ value: region, tooltip: "Project's region" },
{ value: `.pooler.supabase.${tld}`, tooltip: undefined },
]
const dbHostDetails = [
{ value: 'db.', tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
{ value: `.supabase.${tld}`, tooltip: undefined },
]
if (selectedTab === 'uri' || selectedTab === 'nodejs') {
if (isMd5) {
return [
{ value: 'postgresql://', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
{ value: ':', tooltip: undefined },
{ value: '[password]', tooltip: DB_PASS_DESC },
{ value: '@', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: ':', tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: '/', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
...(usePoolerConnection
? [
{ value: `?options=reference%3D`, tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
]
: []),
]
} else {
return [
{ value: 'postgresql://', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
...(usePoolerConnection
? [
{ value: '.', tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
]
: []),
{ value: ':', tooltip: undefined },
{ value: '[password]', tooltip: DB_PASS_DESC },
{ value: '@', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: ':', tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: '/', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
]
}
}
if (selectedTab === 'psql') {
if (isMd5) {
return [
{ value: 'psql "postgresql://', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
{ value: ':', tooltip: undefined },
{ value: '[password]', tooltip: DB_PASS_DESC },
{ value: '@', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: ':', tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: '/', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
...(usePoolerConnection
? [
{ value: '?options=reference%3D', tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
]
: []),
]
} else {
return [
{ value: 'psql -h ', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: ' -p ', tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: ' -d ', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
{ value: ' -U ', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
...(usePoolerConnection
? [
{ value: '.', tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
]
: []),
]
}
}
if (selectedTab === 'golang' || selectedTab === 'php' || selectedTab === 'python') {
if (isMd5) {
return [
{ value: 'user=', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
{ value: ' password=', tooltip: undefined },
{ value: '[password]', tooltip: DB_PASS_DESC },
{ value: ' host=', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: ' port=', tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: ' dbname=', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
...(usePoolerConnection
? [
{ value: ' options=reference=', tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
]
: []),
]
} else {
return [
{ value: 'user=', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
...(usePoolerConnection
? [
{ value: '.', tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
]
: []),
{ value: ' password=', tooltip: undefined },
{ value: '[password]', tooltip: DB_PASS_DESC },
{ value: ' host=', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: ' port=', tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: ' dbname=', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
]
}
}
if (selectedTab === 'jdbc') {
if (isMd5) {
return [
{ value: 'jdbc:postgresql://', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: ':', tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: '/', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
{ value: '?user=', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
{ value: '&password=', tooltip: undefined },
{ value: '[password]', tooltip: DB_PASS_DESC },
...(usePoolerConnection
? [
{ value: '&options=reference%3D', tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
]
: []),
]
} else {
return [
{ value: 'jdbc:postgresql://', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: `:`, tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: '/', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
{ value: '?user=', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
...(usePoolerConnection
? [
{ value: '.', tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
]
: []),
{ value: '&password=', tooltip: undefined },
{ value: '[password]', tooltip: DB_PASS_DESC },
]
}
}
if (selectedTab === 'dotnet') {
if (isMd5) {
return [
{ value: 'User Id=', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
{ value: ';Password=', tooltip: undefined },
{ value: '[password]', tooltip: DB_PASS_DESC },
{ value: ';Server=', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: ';Port=', tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: ';Database=', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
...(usePoolerConnection
? [
{ value: ";Options='reference=", tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
{ value: "'", tooltip: undefined },
]
: []),
]
} else {
return [
{ value: 'User Id=', tooltip: undefined },
{ value: '[user]', tooltip: DB_USER_DESC },
...(usePoolerConnection
? [
{ value: '.', tooltip: undefined },
{ value: ref, tooltip: PROJECT_REF_DESC },
]
: []),
{ value: ';Password=', tooltip: undefined },
{ value: '[password]', tooltip: DB_PASS_DESC },
{ value: ';Server=', tooltip: undefined },
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
{ value: ';Port=', tooltip: undefined },
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
{ value: ';Database=', tooltip: undefined },
{ value: '[db-name]', tooltip: DB_NAME_DESC },
]
}
}
return []
}
@@ -1,153 +0,0 @@
export type Example = {
installCommands?: string[]
postInstallCommands?: string[]
files?: {
name: string
content: string
}[]
}
const examples = {
nodejs: {
installCommands: ['npm install postgres'],
files: [
{
name: 'db.js',
content: `import postgres from 'postgres'
const connectionString = process.env.DATABASE_URL
const sql = postgres(connectionString)
export default sql`,
},
],
},
golang: {
installCommands: ['go get github.com/jackc/pgx/v5'],
files: [
{
name: 'main.go',
content: `package main
import (
"context"
"log"
"os"
"github.com/jackc/pgx/v5"
)
func main() {
conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("Failed to connect to the database: %v", err)
}
defer conn.Close(context.Background())
// Example query to test connection
var version string
if err := conn.QueryRow(context.Background(), "SELECT version()").Scan(&version); err != nil {
log.Fatalf("Query failed: %v", err)
}
log.Println("Connected to:", version)
}`,
},
],
},
dotnet: {
installCommands: [
'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION',
],
postInstallCommands: [
'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION',
],
},
python: {
installCommands: ['pip install python-dotenv psycopg2'],
files: [
{
name: 'main.py',
content: `import psycopg2
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")
# Connect to the database
try:
connection = psycopg2.connect(
user=USER,
password=PASSWORD,
host=HOST,
port=PORT,
dbname=DBNAME
)
print("Connection successful!")
# Create a cursor to execute SQL queries
cursor = connection.cursor()
# Example query
cursor.execute("SELECT NOW();")
result = cursor.fetchone()
print("Current Time:", result)
# Close the cursor and connection
cursor.close()
connection.close()
print("Connection closed.")
except Exception as e:
print(f"Failed to connect: {e}")`,
},
],
},
sqlalchemy: {
installCommands: ['pip install python-dotenv sqlalchemy psycopg2'],
files: [
{
name: 'main.py',
content: `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}")`,
},
],
},
}
export default examples
@@ -1,624 +0,0 @@
import { describe, expect, test } from 'vitest'
import type { ConditionalValue, ConnectSchema, ConnectState, StepTree } from './Connect.types'
import {
getActiveFields,
getDefaultState,
resetDependentFields,
resolveConditional,
resolveSteps,
} from './connect.resolver'
// ============================================================================
// resolveConditional Tests
// ============================================================================
describe('connect.resolver:resolveConditional', () => {
test('should return primitive values directly', () => {
expect(resolveConditional('hello', { mode: 'framework' })).toBe('hello')
expect(resolveConditional(42, { mode: 'framework' })).toBe(42)
expect(resolveConditional(null, { mode: 'framework' })).toBe(null)
expect(resolveConditional(true, { mode: 'framework' })).toBe(true)
})
test('should return arrays directly', () => {
const arr = ['a', 'b', 'c']
expect(resolveConditional(arr, { mode: 'framework' })).toBe(arr)
})
test('should resolve single-level conditional based on mode', () => {
const conditional: ConditionalValue<string> = {
framework: 'Framework Content',
direct: 'Direct Content',
DEFAULT: 'Default Content',
}
expect(resolveConditional(conditional, { mode: 'framework' })).toBe('Framework Content')
expect(resolveConditional(conditional, { mode: 'direct' })).toBe('Direct Content')
expect(resolveConditional(conditional, { mode: 'orm' })).toBe('Default Content')
})
test('should resolve nested conditionals (mode -> framework)', () => {
const conditional: ConditionalValue<string> = {
framework: {
nextjs: 'Next.js Content',
react: 'React Content',
DEFAULT: 'Generic Framework',
},
direct: 'Direct Content',
}
expect(resolveConditional(conditional, { mode: 'framework', framework: 'nextjs' })).toBe(
'Next.js Content'
)
expect(resolveConditional(conditional, { mode: 'framework', framework: 'react' })).toBe(
'React Content'
)
expect(resolveConditional(conditional, { mode: 'framework', framework: 'vue' })).toBe(
'Generic Framework'
)
expect(resolveConditional(conditional, { mode: 'direct' })).toBe('Direct Content')
})
test('should resolve deeply nested conditionals (mode -> framework -> variant)', () => {
const conditional: ConditionalValue<string> = {
framework: {
nextjs: {
app: 'App Router Content',
pages: 'Pages Router Content',
DEFAULT: 'Generic Next.js',
},
DEFAULT: 'Generic Framework',
},
}
expect(
resolveConditional(conditional, {
mode: 'framework',
framework: 'nextjs',
frameworkVariant: 'app',
})
).toBe('App Router Content')
expect(
resolveConditional(conditional, {
mode: 'framework',
framework: 'nextjs',
frameworkVariant: 'pages',
})
).toBe('Pages Router Content')
expect(
resolveConditional(conditional, {
mode: 'framework',
framework: 'nextjs',
frameworkVariant: 'unknown',
})
).toBe('Generic Next.js')
})
test('should resolve MCP client-specific content', () => {
const conditional: ConditionalValue<string> = {
mcp: {
cursor: 'Cursor Config',
codex: 'Codex Config',
'claude-code': 'Claude Code Config',
DEFAULT: 'Generic MCP',
},
}
expect(resolveConditional(conditional, { mode: 'mcp', mcpClient: 'cursor' })).toBe(
'Cursor Config'
)
expect(resolveConditional(conditional, { mode: 'mcp', mcpClient: 'codex' })).toBe(
'Codex Config'
)
expect(resolveConditional(conditional, { mode: 'mcp', mcpClient: 'claude-code' })).toBe(
'Claude Code Config'
)
})
test('should return undefined when no match and no DEFAULT', () => {
const conditional: ConditionalValue<string> = {
framework: 'Framework',
direct: 'Direct',
}
expect(resolveConditional(conditional, { mode: 'orm' })).toBe(undefined)
})
test('should handle frameworkUi boolean state (as string "true"/"false")', () => {
const conditional: ConditionalValue<string> = {
framework: {
nextjs: {
true: 'Shadcn Steps',
DEFAULT: 'Regular Steps',
},
},
}
// When frameworkUi is true (boolean), it gets converted to string "true"
expect(
resolveConditional(conditional, { mode: 'framework', framework: 'nextjs', frameworkUi: true })
).toBe('Shadcn Steps')
expect(
resolveConditional(conditional, {
mode: 'framework',
framework: 'nextjs',
frameworkUi: false,
})
).toBe('Regular Steps')
})
})
// ============================================================================
// resolveSteps Tests
// ============================================================================
describe('connect.resolver:resolveSteps', () => {
const createMockSchema = (steps: StepTree): ConnectSchema => ({
modes: [
{ id: 'framework', label: 'Framework', description: '', fields: [] },
{ id: 'direct', label: 'Direct', description: '', fields: [] },
],
fields: {},
steps,
})
test('should resolve steps array for framework mode', () => {
const schema = createMockSchema({
mode: {
framework: [
{ id: 'step1', title: 'Install', description: 'Install pkg', content: 'install-content' },
{ id: 'step2', title: 'Configure', description: 'Configure', content: 'config-content' },
],
direct: [
{
id: 'connection',
title: 'Connection',
description: 'Connect',
content: 'direct-content',
},
],
},
})
const steps = resolveSteps(schema, { mode: 'framework' })
expect(steps).toHaveLength(2)
expect(steps[0].id).toBe('step1')
expect(steps[0].title).toBe('Install')
expect(steps[1].id).toBe('step2')
})
test('should resolve steps for direct mode', () => {
const schema = createMockSchema({
mode: {
framework: [{ id: 'step1', title: 'Install', description: 'Install', content: 'install' }],
direct: [
{ id: 'connection', title: 'Connection', description: 'Connect', content: 'direct' },
],
},
})
const steps = resolveSteps(schema, { mode: 'direct' })
expect(steps).toHaveLength(1)
expect(steps[0].id).toBe('connection')
})
test('should filter out steps with empty content', () => {
const schema = createMockSchema({
mode: {
framework: [
{ id: 'step1', title: 'Valid', description: 'Valid step', content: 'valid-content' },
{ id: 'step2', title: 'Empty', description: 'Empty step', content: '' },
{ id: 'step3', title: 'Null', description: 'Null step', content: null },
],
},
})
const steps = resolveSteps(schema, { mode: 'framework' })
expect(steps).toHaveLength(1)
expect(steps[0].id).toBe('step1')
})
test('should resolve conditional step content based on state', () => {
const schema = createMockSchema({
mode: {
framework: [
{
id: 'configure',
title: 'Configure',
description: 'Configure',
content: {
nextjs: 'nextjs-content',
react: 'react-content',
DEFAULT: 'generic-content',
},
},
],
},
})
const nextjsSteps = resolveSteps(schema, { mode: 'framework', framework: 'nextjs' })
expect(nextjsSteps[0].content).toBe('nextjs-content')
const reactSteps = resolveSteps(schema, { mode: 'framework', framework: 'react' })
expect(reactSteps[0].content).toBe('react-content')
const vueSteps = resolveSteps(schema, { mode: 'framework', framework: 'vue' })
expect(vueSteps[0].content).toBe('generic-content')
})
test('should return empty array when no steps resolve', () => {
const schema = createMockSchema({
mode: {
framework: [{ id: 'step1', title: 'Step', description: 'Desc', content: '' }],
},
})
const steps = resolveSteps(schema, { mode: 'framework' })
expect(steps).toEqual([])
})
test('should return empty array when steps is not an array', () => {
const schema = createMockSchema({
mode: {
framework: {
framework: {},
},
},
} as any)
const steps = resolveSteps(schema, { mode: 'framework' })
expect(steps).toEqual([])
})
test('should append steps from multiple field conditions in order', () => {
const schema = createMockSchema({
mode: {
framework: {
framework: {
nextjs: [{ id: 'base', title: 'Base', description: 'Base step', content: 'base' }],
},
frameworkUi: {
true: [{ id: 'ui', title: 'UI', description: 'UI step', content: 'ui' }],
},
},
},
})
const steps = resolveSteps(schema, {
mode: 'framework',
framework: 'nextjs',
frameworkUi: true,
})
expect(steps.map((step) => step.id)).toEqual(['base', 'ui'])
})
})
// ============================================================================
// getActiveFields Tests
// ============================================================================
describe('connect.resolver:getActiveFields', () => {
const createSchemaWithFields = (
modes: ConnectSchema['modes'],
fields: ConnectSchema['fields']
): ConnectSchema => ({
modes,
fields,
steps: [],
})
test('should return fields for the current mode', () => {
const schema = createSchemaWithFields(
[
{ id: 'framework', label: 'Framework', description: '', fields: ['framework', 'library'] },
{ id: 'direct', label: 'Direct', description: '', fields: ['connectionType'] },
],
{
framework: {
id: 'framework',
type: 'radio-grid',
label: 'Framework',
defaultValue: 'nextjs',
},
library: {
id: 'library',
type: 'select',
label: 'Library',
defaultValue: 'supabasejs',
},
connectionType: {
id: 'connectionType',
type: 'select',
label: 'Type',
defaultValue: 'uri',
},
}
)
const frameworkFields = getActiveFields(schema, { mode: 'framework' })
expect(frameworkFields).toHaveLength(2)
expect(frameworkFields.map((f) => f.id)).toEqual(['framework', 'library'])
const directFields = getActiveFields(schema, { mode: 'direct' })
expect(directFields).toHaveLength(1)
expect(directFields[0].id).toBe('connectionType')
})
test('should filter fields by dependsOn conditions', () => {
const schema = createSchemaWithFields(
[
{
id: 'framework',
label: 'Framework',
description: '',
fields: ['framework', 'frameworkVariant', 'frameworkUi'],
},
],
{
framework: {
id: 'framework',
type: 'radio-grid',
label: 'Framework',
defaultValue: 'nextjs',
},
frameworkVariant: {
id: 'frameworkVariant',
type: 'select',
label: 'Variant',
dependsOn: { framework: ['nextjs', 'react'] },
},
frameworkUi: {
id: 'frameworkUi',
type: 'switch',
label: 'Shadcn',
dependsOn: { framework: ['nextjs', 'react'] },
},
}
)
// With nextjs - should show all fields
const nextjsFields = getActiveFields(schema, { mode: 'framework', framework: 'nextjs' })
expect(nextjsFields).toHaveLength(3)
// With vue - should hide frameworkVariant and frameworkUi
const vueFields = getActiveFields(schema, { mode: 'framework', framework: 'vue' })
expect(vueFields).toHaveLength(1)
expect(vueFields[0].id).toBe('framework')
})
test('should handle multiple dependsOn conditions', () => {
const schema = createSchemaWithFields(
[
{
id: 'direct',
label: 'Direct',
description: '',
fields: ['connectionMethod', 'useSharedPooler'],
},
],
{
connectionMethod: {
id: 'connectionMethod',
type: 'radio-list',
label: 'Method',
defaultValue: 'direct',
},
useSharedPooler: {
id: 'useSharedPooler',
type: 'switch',
label: 'Use Shared Pooler',
dependsOn: { connectionMethod: ['transaction'] },
},
}
)
// Transaction mode - show shared pooler option
const transactionFields = getActiveFields(schema, {
mode: 'direct',
connectionMethod: 'transaction',
})
expect(transactionFields).toHaveLength(2)
// Direct mode - hide shared pooler option
const directFields = getActiveFields(schema, { mode: 'direct', connectionMethod: 'direct' })
expect(directFields).toHaveLength(1)
expect(directFields[0].id).toBe('connectionMethod')
})
test('should return empty array for invalid mode', () => {
const schema = createSchemaWithFields(
[{ id: 'framework', label: 'Framework', description: '', fields: ['framework'] }],
{ framework: { id: 'framework', type: 'radio-grid', label: 'Framework' } }
)
const fields = getActiveFields(schema, { mode: 'invalid' as any })
expect(fields).toEqual([])
})
test('should include resolvedOptions for each field', () => {
const schema = createSchemaWithFields(
[{ id: 'framework', label: 'Framework', description: '', fields: ['framework'] }],
{
framework: {
id: 'framework',
type: 'radio-grid',
label: 'Framework',
options: { source: 'frameworks' }, // Source reference - resolved elsewhere
},
}
)
const fields = getActiveFields(schema, { mode: 'framework' })
expect(fields[0]).toHaveProperty('resolvedOptions')
// Source options are resolved by the hook, not the resolver
expect(fields[0].resolvedOptions).toEqual([])
})
})
// ============================================================================
// getDefaultState Tests
// ============================================================================
describe('connect.resolver:getDefaultState', () => {
test('should return default state with first mode', () => {
const schema: ConnectSchema = {
modes: [
{ id: 'framework', label: 'Framework', description: '', fields: [] },
{ id: 'direct', label: 'Direct', description: '', fields: [] },
],
fields: {},
steps: [],
}
const state = getDefaultState(schema)
expect(state.mode).toBe('framework')
})
test('should include default values from fields', () => {
const schema: ConnectSchema = {
modes: [{ id: 'framework', label: 'Framework', description: '', fields: ['framework'] }],
fields: {
framework: {
id: 'framework',
type: 'radio-grid',
label: 'Framework',
defaultValue: 'nextjs',
},
library: {
id: 'library',
type: 'select',
label: 'Library',
defaultValue: 'supabasejs',
},
mcpReadonly: {
id: 'mcpReadonly',
type: 'switch',
label: 'Readonly',
defaultValue: false,
},
},
steps: [],
}
const state = getDefaultState(schema)
expect(state.framework).toBe('nextjs')
expect(state.library).toBe('supabasejs')
expect(state.mcpReadonly).toBe(false)
})
test('should fallback to "direct" if no modes defined', () => {
const schema: ConnectSchema = {
modes: [],
fields: {},
steps: [],
}
const state = getDefaultState(schema)
expect(state.mode).toBe('direct')
})
})
// ============================================================================
// resetDependentFields Tests
// ============================================================================
describe('connect.resolver:resetDependentFields', () => {
const createSchemaForReset = (): ConnectSchema => ({
modes: [
{
id: 'framework',
label: 'Framework',
description: '',
fields: ['framework', 'frameworkVariant', 'frameworkUi'],
},
{ id: 'direct', label: 'Direct', description: '', fields: ['connectionMethod'] },
],
fields: {
framework: {
id: 'framework',
type: 'radio-grid',
label: 'Framework',
defaultValue: 'nextjs',
},
frameworkVariant: {
id: 'frameworkVariant',
type: 'select',
label: 'Variant',
dependsOn: { framework: ['nextjs', 'react'] },
},
frameworkUi: {
id: 'frameworkUi',
type: 'switch',
label: 'Shadcn',
dependsOn: { framework: ['nextjs', 'react'] },
},
connectionMethod: {
id: 'connectionMethod',
type: 'radio-list',
label: 'Method',
defaultValue: 'direct',
},
},
steps: [],
})
test('should reset dependent fields when dependency no longer satisfied', () => {
const schema = createSchemaForReset()
const state: ConnectState = {
mode: 'framework',
framework: 'vue', // Changed from nextjs to vue
frameworkVariant: 'app', // This should be reset
frameworkUi: true, // This should be reset
}
const newState = resetDependentFields(state, 'framework', schema)
expect(newState.frameworkVariant).toBeUndefined()
expect(newState.frameworkUi).toBeUndefined()
})
test('should keep dependent fields when dependency still satisfied', () => {
const schema = createSchemaForReset()
const state: ConnectState = {
mode: 'framework',
framework: 'react', // Still in the allowed list
frameworkVariant: 'vite',
frameworkUi: true,
}
const newState = resetDependentFields(state, 'framework', schema)
expect(newState.frameworkVariant).toBe('vite')
expect(newState.frameworkUi).toBe(true)
})
test('should handle mode changes', () => {
const schema = createSchemaForReset()
const state: ConnectState = {
mode: 'direct', // Changed mode
framework: 'nextjs',
frameworkVariant: 'app',
}
// Note: The current implementation of resetDependentFields for mode changes
// looks for fields not in the current mode, but the logic compares against previous mode
const newState = resetDependentFields(state, 'mode', schema)
// Mode-specific field reset logic is handled
expect(newState.mode).toBe('direct')
})
test('should not modify state for fields without dependencies', () => {
const schema = createSchemaForReset()
const state: ConnectState = {
mode: 'framework',
framework: 'nextjs',
}
const newState = resetDependentFields(state, 'framework', schema)
expect(newState.mode).toBe('framework')
expect(newState.framework).toBe('nextjs')
})
})
@@ -1,254 +0,0 @@
import type {
ConditionalValue,
ConnectSchema,
ConnectState,
FieldOption,
ResolvedField,
ResolvedStep,
StepDefinition,
StepFieldValueMap,
StepTree,
} from './Connect.types'
/**
* The order in which state keys are checked during conditional value resolution.
* Used for ConditionalValue (value-keyed) resolution, not for step trees.
*/
const STATE_KEY_ORDER = [
'mode',
'framework',
'frameworkVariant',
'library',
'frameworkUi',
'orm',
'connectionMethod',
'connectionType',
'mcpClient',
] as const
/**
* Check if a value is a conditional object (has nested state keys or DEFAULT)
*/
function isConditionalObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/**
* Resolves a conditional value based on current state.
* Walks the tree using stateKeys in order, falling back to DEFAULT at each level.
*
* Example: Given state { mode: 'mcp', mcpClient: 'codex' }
* and stateKeys ['mode', 'framework', ..., 'mcpClient']
*
* 1. Look up 'mcp' (state.mode value) in tree -> found, continue
* 2. At mcp subtree { codex: [...], DEFAULT: [...] }, skip irrelevant keys
* until we find a key whose state value matches an entry in the object
* 3. Look up 'codex' (state.mcpClient value) in that subtree -> found, return value
* 4. If no state key matches, try DEFAULT at that level
*/
export function resolveConditional<T>(
value: ConditionalValue<T>,
state: ConnectState,
stateKeys: readonly string[] = STATE_KEY_ORDER
): T | undefined {
// Base case: we've reached a leaf value (string, array, null, boolean, etc.)
if (!isConditionalObject(value)) {
return value as T
}
const conditionalObj = value as Record<string, ConditionalValue<T>>
const objectKeys = Object.keys(conditionalObj).filter((k) => k !== 'DEFAULT')
// Try each state key in order to find one that matches an entry in the object
for (let i = 0; i < stateKeys.length; i++) {
const currentKey = stateKeys[i]
const stateValue = String(state[currentKey] ?? '')
// If this state value matches a key in the conditional object, use it
if (stateValue && objectKeys.includes(stateValue)) {
const nextValue = conditionalObj[stateValue]
// Continue resolving with remaining keys (after this one)
return resolveConditional(nextValue, state, stateKeys.slice(i + 1))
}
}
// No state key matched - use DEFAULT if available
if (conditionalObj.DEFAULT !== undefined) {
return resolveConditional(conditionalObj.DEFAULT, state, stateKeys)
}
return undefined
}
/**
* Resolves the steps array based on current state.
* Returns only steps that have non-null content.
*/
export function resolveSteps(schema: ConnectSchema, state: ConnectState): ResolvedStep[] {
const steps = resolveStepTree(schema.steps, state)
if (steps.length === 0) return []
return steps
.map((step) => {
const content = resolveConditional<string | null>(step.content, state)
return {
id: step.id,
title: step.title,
description: step.description,
content: content ?? '',
}
})
.filter((step) => step.content !== '' && step.content !== null)
}
/**
* Resolves a step tree by evaluating field-specific branches in insertion order.
* Each matching branch appends its steps to the final list.
*/
function resolveStepTree(tree: StepTree, state: ConnectState): StepDefinition[] {
if (Array.isArray(tree)) return tree
if (!isConditionalObject(tree)) return []
const resolved: StepDefinition[] = []
for (const [fieldId, valueMap] of Object.entries(tree)) {
if (fieldId === 'DEFAULT') continue
if (!isConditionalObject(valueMap)) continue
const branch = resolveStepBranch(valueMap as StepFieldValueMap, state[fieldId])
if (!branch) continue
resolved.push(...resolveStepTree(branch, state))
}
return resolved
}
function resolveStepBranch(
valueMap: StepFieldValueMap,
stateValue: ConnectState[keyof ConnectState] | undefined
): StepTree | undefined {
const key = String(stateValue ?? '')
if (key && Object.prototype.hasOwnProperty.call(valueMap, key)) {
return valueMap[key]
}
if (valueMap.DEFAULT !== undefined) {
return valueMap.DEFAULT
}
return undefined
}
/**
* Gets the active fields for the current mode, filtering by dependsOn conditions.
*/
export function getActiveFields(schema: ConnectSchema, state: ConnectState): ResolvedField[] {
const currentMode = schema.modes.find((m) => m.id === state.mode)
if (!currentMode) return []
return currentMode.fields
.map((fieldId) => schema.fields[fieldId])
.filter((field): field is NonNullable<typeof field> => !!field)
.filter((field) => {
// Check dependsOn conditions
if (!field.dependsOn) return true
return Object.entries(field.dependsOn).every(([key, values]) => {
const stateValue = String(state[key] ?? '')
return values.includes(stateValue)
})
})
.map((field) => ({
...field,
resolvedOptions: resolveFieldOptions(field, state),
}))
}
/**
* Resolves field options based on current state.
*/
function resolveFieldOptions(field: { options?: unknown }, state: ConnectState): FieldOption[] {
if (!field.options) return []
// Static options array
if (Array.isArray(field.options)) {
return field.options
}
// Reference to data source (handled elsewhere)
if (
typeof field.options === 'object' &&
'source' in field.options &&
typeof field.options.source === 'string'
) {
// This will be resolved by the component using getFieldOptionsFromSource
return []
}
// Conditional options
const resolved = resolveConditional<FieldOption[]>(
field.options as ConditionalValue<FieldOption[]>,
state
)
return resolved ?? []
}
/**
* Gets default state for the schema, using first mode and default field values.
*/
export function getDefaultState(schema: ConnectSchema): ConnectState {
const defaultMode = schema.modes[0]?.id ?? 'direct'
const state: ConnectState = { mode: defaultMode }
// Set default values for all fields
Object.values(schema.fields).forEach((field) => {
if (field.defaultValue !== undefined) {
state[field.id] = field.defaultValue
}
})
return state
}
/**
* Resets dependent fields when a parent field changes.
* For example, changing framework should reset frameworkVariant.
*/
export function resetDependentFields(
state: ConnectState,
changedFieldId: string,
schema: ConnectSchema
): ConnectState {
const newState = { ...state }
// Find fields that depend on the changed field
Object.values(schema.fields).forEach((field) => {
if (field.dependsOn && changedFieldId in field.dependsOn) {
// Only reset if dependency conditions are no longer satisfied
const dependencySatisfied = Object.entries(field.dependsOn).every(([key, values]) => {
const stateValue = String(newState[key] ?? '')
return values.includes(stateValue)
})
if (!dependencySatisfied) {
delete newState[field.id]
}
}
})
// Special case: changing mode resets all mode-specific fields
if (changedFieldId === 'mode') {
const previousMode = schema.modes.find((m) => m.id !== state.mode)
const currentMode = schema.modes.find((m) => m.id === state.mode)
// Reset fields from previous mode that aren't in current mode
previousMode?.fields.forEach((fieldId) => {
if (!currentMode?.fields.includes(fieldId)) {
delete newState[fieldId]
}
})
}
return newState
}
@@ -1,401 +0,0 @@
import { describe, test, expect } from 'vitest'
import { connectSchema, INSTALL_COMMANDS } from './connect.schema'
import { resolveSteps } from './connect.resolver'
import type { ConnectState } from './Connect.types'
// ============================================================================
// Schema Structure Tests
// ============================================================================
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')
})
test('each mode should have required properties', () => {
connectSchema.modes.forEach((mode) => {
expect(mode.id).toBeDefined()
expect(mode.label).toBeDefined()
expect(mode.description).toBeDefined()
expect(mode.fields).toBeDefined()
expect(Array.isArray(mode.fields)).toBe(true)
})
})
test('framework mode should have correct fields', () => {
const frameworkMode = connectSchema.modes.find((m) => m.id === 'framework')
expect(frameworkMode?.fields).toContain('framework')
expect(frameworkMode?.fields).toContain('frameworkVariant')
expect(frameworkMode?.fields).toContain('library')
expect(frameworkMode?.fields).toContain('frameworkUi')
})
test('direct mode should have correct fields', () => {
const directMode = connectSchema.modes.find((m) => m.id === 'direct')
expect(directMode?.fields).toContain('connectionMethod')
expect(directMode?.fields).toContain('useSharedPooler')
expect(directMode?.fields).toContain('connectionType')
})
test('orm mode should have correct fields', () => {
const ormMode = connectSchema.modes.find((m) => m.id === 'orm')
expect(ormMode?.fields).toContain('orm')
})
test('mcp mode should have correct fields', () => {
const mcpMode = connectSchema.modes.find((m) => m.id === 'mcp')
expect(mcpMode?.fields).toContain('mcpClient')
expect(mcpMode?.fields).toContain('mcpReadonly')
expect(mcpMode?.fields).toContain('mcpFeatures')
})
test('all mode fields should exist in fields definition', () => {
connectSchema.modes.forEach((mode) => {
mode.fields.forEach((fieldId) => {
expect(
connectSchema.fields[fieldId],
`Field "${fieldId}" in mode "${mode.id}" should exist in fields definition`
).toBeDefined()
})
})
})
})
// ============================================================================
// Field Definition Tests
// ============================================================================
describe('connect.schema:fields', () => {
test('framework field should have correct type', () => {
const field = connectSchema.fields.framework
expect(field.type).toBe('radio-grid')
expect(field.options).toEqual({ source: 'frameworks' })
expect(field.defaultValue).toBe('nextjs')
})
test('frameworkVariant field should depend on framework', () => {
const field = connectSchema.fields.frameworkVariant
expect(field.dependsOn).toEqual({ framework: ['nextjs', 'react'] })
})
test('frameworkUi field should be a switch type', () => {
const field = connectSchema.fields.frameworkUi
expect(field.type).toBe('switch')
expect(field.defaultValue).toBe(false)
expect(field.dependsOn).toEqual({ framework: ['nextjs', 'react'] })
})
test('connectionMethod field should have radio-list type', () => {
const field = connectSchema.fields.connectionMethod
expect(field.type).toBe('radio-list')
expect(field.options).toEqual({ source: 'connectionMethods' })
expect(field.defaultValue).toBe('direct')
})
test('useSharedPooler field should depend on transaction connection method', () => {
const field = connectSchema.fields.useSharedPooler
expect(field.type).toBe('switch')
expect(field.dependsOn).toEqual({ connectionMethod: ['transaction'] })
})
test('orm field should have radio-list type', () => {
const field = connectSchema.fields.orm
expect(field.type).toBe('radio-list')
expect(field.options).toEqual({ source: 'orms' })
expect(field.defaultValue).toBe('prisma')
})
test('mcpClient field should have select type', () => {
const field = connectSchema.fields.mcpClient
expect(field.type).toBe('select')
expect(field.options).toEqual({ source: 'mcpClients' })
expect(field.defaultValue).toBe('cursor')
})
test('mcpFeatures field should have multi-select type', () => {
const field = connectSchema.fields.mcpFeatures
expect(field.type).toBe('multi-select')
expect(field.options).toEqual({ source: 'mcpFeatures' })
})
})
// ============================================================================
// Install Commands Tests
// ============================================================================
describe('connect.schema:INSTALL_COMMANDS', () => {
test('should have install command for supabase-js', () => {
expect(INSTALL_COMMANDS.supabasejs).toBe('npm install @supabase/supabase-js')
})
test('should have install command for supabase-py', () => {
expect(INSTALL_COMMANDS.supabasepy).toBe('pip install supabase')
})
test('should have install command for supabase-flutter', () => {
expect(INSTALL_COMMANDS.supabaseflutter).toBe('flutter pub add supabase_flutter')
})
test('should have install command for supabase-swift', () => {
expect(INSTALL_COMMANDS.supabaseswift).toContain('swift package add-dependency')
})
test('should have install command for supabase-kt', () => {
expect(INSTALL_COMMANDS.supabasekt).toContain('io.github.jan-tennert.supabase')
})
})
// ============================================================================
// Steps Resolution Integration Tests
// ============================================================================
describe('connect.schema:steps resolution', () => {
describe('framework mode steps', () => {
test('should resolve steps for nextjs without shadcn', () => {
const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: false }
const steps = resolveSteps(connectSchema, state)
expect(steps.length).toBeGreaterThan(0)
expect(steps.find((s) => s.id === 'install')).toBeDefined()
expect(steps.find((s) => s.id === 'install-skills')).toBeDefined()
})
test('should resolve shadcn steps for nextjs with frameworkUi true', () => {
const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined()
expect(steps.find((s) => s.id === 'shadcn-explore')).toBeDefined()
})
test('should resolve steps for react without shadcn', () => {
const state: ConnectState = { mode: 'framework', framework: 'react', frameworkUi: false }
const steps = resolveSteps(connectSchema, state)
expect(steps.length).toBeGreaterThan(0)
expect(steps.find((s) => s.id === 'install')).toBeDefined()
})
test('should resolve shadcn steps for react with frameworkUi true', () => {
const state: ConnectState = { mode: 'framework', framework: 'react', frameworkUi: true }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined()
})
test('should resolve default steps for other frameworks', () => {
const state: ConnectState = { mode: 'framework', framework: 'remix' }
const steps = resolveSteps(connectSchema, state)
expect(steps.length).toBeGreaterThan(0)
expect(steps.find((s) => s.id === 'install')).toBeDefined()
expect(steps.find((s) => s.id === 'configure')).toBeDefined()
})
})
describe('direct mode steps', () => {
test('should resolve connection step for default 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()
})
})
describe('orm mode steps', () => {
test('should resolve install and configure steps for prisma', () => {
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()
})
})
describe('mcp mode steps', () => {
test('should resolve configure step for cursor client', () => {
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()
})
})
describe('skills install step', () => {
test('should include skills install step in all modes', () => {
const modes: ConnectState['mode'][] = ['framework', 'direct', 'orm', 'mcp']
modes.forEach((mode) => {
const state: ConnectState = { mode }
const steps = resolveSteps(connectSchema, state)
expect(
steps.find((s) => s.id === 'install-skills'),
`Mode "${mode}" should have skills install step`
).toBeDefined()
})
})
})
})
// ============================================================================
// Step Content Path Tests
// ============================================================================
describe('connect.schema:step content paths', () => {
test('install step should have valid content path', () => {
const state: ConnectState = { mode: 'framework', framework: 'nextjs' }
const steps = resolveSteps(connectSchema, state)
const installStep = steps.find((s) => s.id === 'install')
expect(installStep?.content).toBe('steps/install')
})
test('shadcn command step should have valid content path', () => {
const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true }
const steps = resolveSteps(connectSchema, state)
const shadcnStep = steps.find((s) => s.id === 'shadcn-add')
expect(shadcnStep?.content).toBe('steps/shadcn/command')
})
test('shadcn explore step should have valid content path', () => {
const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true }
const steps = resolveSteps(connectSchema, state)
const exploreStep = steps.find((s) => s.id === 'shadcn-explore')
expect(exploreStep?.content).toBe('steps/shadcn/explore')
})
test('direct connection step should have valid content path', () => {
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')
})
test('skills install step should have valid content path', () => {
const state: ConnectState = { mode: 'framework' }
const steps = resolveSteps(connectSchema, state)
const skillsStep = steps.find((s) => s.id === 'install-skills')
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
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}}')
})
test('mcp cursor configure step should have valid content path', () => {
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')
})
test('codex steps should have valid content paths', () => {
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')
})
test('claude-code steps should have valid content paths', () => {
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'
)
})
})
@@ -1,347 +0,0 @@
import type { ConnectSchema, StepDefinition } from './Connect.types'
/**
* Install commands for different packages
*/
export const INSTALL_COMMANDS: Record<string, string> = {
supabasejs: 'npm install @supabase/supabase-js',
supabasepy: 'pip install supabase',
supabaseflutter: 'flutter pub add supabase_flutter',
supabaseswift:
'swift package add-dependency https://github.com/supabase-community/supabase-swift',
supabasekt: 'implementation("io.github.jan-tennert.supabase:supabase-kt:VERSION")',
}
// ============================================================================
// Step Definitions (reusable)
// All content paths use template syntax: {{stateKey}} is replaced with state values
// ============================================================================
const frameworkInstallStep: StepDefinition = {
id: 'install',
title: 'Install package',
description: 'Run this command to install the required dependencies.',
content: 'steps/install',
}
const frameworkConfigureStep: StepDefinition = {
id: 'configure',
title: 'Add files',
description: 'Copy the following code into your project.',
content: '{{framework}}/{{frameworkVariant}}/{{library}}',
}
const frameworkNextJsFilesStep: StepDefinition = {
id: 'configure-nextjs',
title: 'Add files',
description:
'Add env variables, create Supabase client helpers, and set up middleware to keep sessions refreshed.',
content: '{{framework}}/{{frameworkVariant}}/{{library}}',
}
const frameworkReactFilesStep: StepDefinition = {
id: 'configure-react',
title: 'Add files',
description: 'Add env variables, create a Supabase client, and use it in your app to query data.',
content: '{{framework}}/{{frameworkVariant}}/{{library}}',
}
const frameworkShadcnStep: StepDefinition = {
id: 'shadcn-add',
title: 'Add Supabase UI components',
description: 'Run this command to install the Supabase shadcn components.',
content: 'steps/shadcn/command',
}
const frameworkShadcnExploreStep: StepDefinition = {
id: 'shadcn-explore',
title: 'Check out more UI components',
description: 'Add auth, realtime and storage functionality to your project',
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)',
description:
'Agent Skills give AI coding tools ready-made instructions, scripts, and resources for working with Supabase more accurately and efficiently.',
content: 'steps/skills-install',
}
// ============================================================================
// Main Schema
// ============================================================================
export const connectSchema: ConnectSchema = {
// -------------------------------------------------------------------------
// Mode Definitions
// -------------------------------------------------------------------------
modes: [
{
id: 'framework',
label: 'Framework',
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'],
},
],
// -------------------------------------------------------------------------
// Field Definitions
// -------------------------------------------------------------------------
fields: {
// Framework fields
framework: {
id: 'framework',
type: 'radio-grid',
label: 'Framework',
options: { source: 'frameworks' },
defaultValue: 'nextjs',
},
frameworkVariant: {
id: 'frameworkVariant',
type: 'select',
label: 'Variant',
options: { source: 'frameworkVariants' },
defaultValue: 'app',
dependsOn: { framework: ['nextjs', 'react'] }, // Only show for frameworks with multiple variants
},
library: {
id: 'library',
type: 'select',
label: 'Library',
options: { source: 'libraries' },
defaultValue: 'supabasejs',
},
frameworkUi: {
id: 'frameworkUi',
type: 'switch',
label: 'Shadcn',
description: 'Install components via the Supabase shadcn registry.',
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' },
},
},
// -------------------------------------------------------------------------
// Steps - Conditional based on mode and nested selections
// -------------------------------------------------------------------------
steps: {
// Keys are field IDs; each field maps state values to step trees.
mode: {
framework: {
framework: {
nextjs: {
frameworkUi: {
true: [
frameworkInstallStep,
frameworkShadcnStep,
frameworkShadcnExploreStep,
skillsInstallStep,
],
DEFAULT: [frameworkInstallStep, frameworkNextJsFilesStep, skillsInstallStep],
},
},
react: {
frameworkUi: {
true: [
frameworkInstallStep,
frameworkShadcnStep,
frameworkShadcnExploreStep,
skillsInstallStep,
],
DEFAULT: [frameworkInstallStep, frameworkReactFilesStep, skillsInstallStep],
},
},
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,71 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: 'MainActivity.kt',
language: 'kotlin',
code: `
val supabase = createSupabaseClient(
supabaseUrl = "${projectKeys.apiUrl ?? 'your-project-url'}",
supabaseKey = "${projectKeys.publishableKey ?? '<prefer publishable key instead of anon key for mobile apps>'}"
) {
install(Postgrest)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
TodoList()
}
}
}
}
}
@Composable
fun TodoList() {
var items by remember { mutableStateOf<List<TodoItem>>(listOf()) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
items = supabase.from("todos")
.select().decodeList<TodoItem>()
}
}
LazyColumn {
items(
items,
key = { item -> item.id },
) { item ->
Text(
item.name,
modifier = Modifier.padding(8.dp),
)
}
}
}
`,
},
{
name: 'TodoItem.kt',
language: 'kotlin',
code: `
@Serializable
data class TodoItem(val id: Int, val name: String)
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,53 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: `
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'}
`,
},
{
name: 'src/db/supabase.js',
language: 'js',
code: `
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = import.meta.env.SUPABASE_URL;
const supabaseKey = import.meta.env.SUPABASE_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
`,
},
{
name: 'src/pages/index.astro',
language: 'html',
code: `
---
import { supabase } from '../db/supabase';
const { data, error } = await supabase.from("todos").select('*');
---
{
(
<ul>
{data.map((entry) => (
<li>{entry.name}</li>
))}
</ul>
)
}
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,64 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
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,88 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: `
EXPO_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
EXPO_PUBLIC_SUPABASE_KEY=${projectKeys.publishableKey ?? '<prefer publishable key instead of anon key for mobile and desktop apps>'}
`,
},
{
name: 'utils/supabase.ts',
language: 'ts',
code: `
import 'react-native-url-polyfill/auto'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient, processLock } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_KEY!,
{
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
lock: processLock,
},
})
`,
},
{
name: 'App.tsx',
language: 'tsx',
code: `
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList } from 'react-native';
import { supabase } from '../utils/supabase';
export default function App() {
const [todos, setTodos] = useState([]);
useEffect(() => {
const getTodos = async () => {
try {
const { data: todos, error } = await supabase.from('todos').select();
if (error) {
console.error('Error fetching todos:', error.message);
return;
}
if (todos && todos.length > 0) {
setTodos(todos);
}
} catch (error) {
console.error('Error fetching todos:', error.message);
}
};
getTodos();
}, []);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Todo List</Text>
<FlatList
data={todos}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <Text key={item.id}>{item.title}</Text>}
/>
</View>
);
};
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,54 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env',
language: 'bash',
code: `
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'}
`,
},
{
name: 'app.py',
language: 'python',
code: `
import os
from flask import Flask
from supabase import create_client, Client
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
supabase: Client = create_client(
os.environ.get("SUPABASE_URL"),
os.environ.get("SUPABASE_KEY")
)
@app.route('/')
def index():
response = supabase.table('todos').select("*").execute()
todos = response.data
html = '<h1>Todos</h1><ul>'
for todo in todos:
html += f'<li>{todo["name"]}</li>'
html += '</ul>'
return html
if __name__ == '__main__':
app.run(debug=True)
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,81 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: 'lib/main.dart',
language: 'dart',
code: `
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
await Supabase.initialize(
url: '${projectKeys.apiUrl ?? 'your-project-url'}',
anonKey: '${projectKeys.publishableKey ?? '<prefer publishable key instead of anon key for mobile and desktop apps>'}',
);
runApp(MyApp());
}
`,
},
{
name: 'lib/main.dart (app)',
language: 'dart',
code: `
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Todos',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _future = Supabase.instance.client
.from('todos')
.select();
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _future,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final todos = snapshot.data!;
return ListView.builder(
itemCount: todos.length,
itemBuilder: ((context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo['name']),
);
}),
);
},
),
);
}
}
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,127 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: 'environments/environment.ts',
language: 'ts',
code: `
export const environment = {
supabaseUrl: '${projectKeys.apiUrl ?? 'your-project-url'}',
supabaseKey: '${projectKeys.publishableKey ?? '<prefer publishable key instead of anon key for mobile apps>'}',
};
`,
},
{
name: 'src/app/supabase.service.ts',
language: 'ts',
code: `
import { Injectable } from '@angular/core';
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { environment } from '../environments/environment';
@Injectable({
providedIn: 'root',
})
export class SupabaseService {
private supabase: SupabaseClient;
constructor() {
this.supabase = createClient(
environment.supabaseUrl,
environment.supabaseKey
);
}
getTodos() {
return this.supabase.from('todos').select('*');
}
}
`,
},
{
name: 'src/app/app.component.ts',
language: 'ts',
code: `
import { Component, OnInit } from '@angular/core';
import { SupabaseService } from './supabase.service';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent implements OnInit {
todos: any[] = [];
constructor(private supabaseService: SupabaseService) {}
async ngOnInit() {
await this.loadTodos();
}
async loadTodos() {
const { data, error } = await this.supabaseService.getTodos();
if (error) {
console.error('Error fetching todos:', error);
} else {
this.todos = data;
}
}
}
`,
},
{
name: 'src/app/app.component.html',
language: 'html',
code: `
<ion-header>
<ion-toolbar>
<ion-title>Todo List</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngFor="let todo of todos">
<ion-label>{{ todo.title }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
`,
},
{
name: 'src/app/app.module.ts',
language: 'ts',
code: `
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { AppComponent } from './app.component';
import { SupabaseService } from './supabase.service';
@NgModule({
imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot([]),
IonicModule.forRoot({ mode: 'ios' }),
],
declarations: [AppComponent],
providers: [SupabaseService],
bootstrap: [AppComponent],
})
export class AppModule {}
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,101 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env',
language: 'bash',
code: `
REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
REACT_APP_SUPABASE_KEY=${projectKeys.publishableKey ?? '<prefer publishable key instead of anon key for mobile or desktop apps>'}
`,
},
{
name: 'src/supabaseClient.tsx',
language: 'ts',
code: `
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
const supabaseKey = process.env.REACT_APP_SUPABASE_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
`,
},
{
name: 'src/App.tsx',
language: 'ts',
code: `
import React, { useEffect, useState } from 'react';
import { setupIonicReact, IonApp } from '@ionic/react';
import {
IonContent,
IonHeader,
IonTitle,
IonToolbar,
IonList,
IonItem,
} from '@ionic/react';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Theme variables */
import './theme/variables.css';
import { supabase } from './supabaseClient';
setupIonicReact();
export default function App() {
const [todos, setTodos] = useState([]);
useEffect(() => {
getTodos();
}, []);
const getTodos = async () => {
try {
const { data, error } = await supabase.from('todos').select();
if (error) {
console.error('Error fetching todos:', error.message);
return;
}
if (data) {
setTodos(data);
}
} catch (error) {
console.error('Error fetching todos:', error.message);
}
};
return (
<IonApp>
<>
<IonHeader>
<IonToolbar>
<IonTitle>Todos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
{todos.map((todo) => (
<IonItem key={todo.id}>{todo.title}</IonItem>
))}
</IonList>
</IonContent>
</>
</IonApp>
);
}
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,141 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: [
`NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
},
{
name: 'page.tsx',
language: 'tsx',
code: `
import { createClient } from '@/utils/supabase/server'
import { cookies } from 'next/headers'
export default async function Page() {
const cookieStore = await cookies()
const supabase = createClient(cookieStore)
const { data: todos } = await supabase.from('todos').select()
return (
<ul>
{todos?.map((todo) => (
<li>{todo}</li>
))}
</ul>
)
}
`,
},
{
name: 'utils/supabase/server.ts',
language: 'ts',
code: `
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'};
export const createClient = (cookieStore: ReturnType<typeof cookies>) => {
return createServerClient(
supabaseUrl!,
supabaseKey!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
} catch {
// The \`setAll\` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
},
);
};
`,
},
{
name: 'utils/supabase/client.ts',
language: 'ts',
code: `
import { createBrowserClient } from "@supabase/ssr";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'};
export const createClient = () =>
createBrowserClient(
supabaseUrl!,
supabaseKey!,
);
`,
},
{
name: 'utils/supabase/middleware.ts',
language: 'ts',
code: `
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'};
export const createClient = (request: NextRequest) => {
// Create an unmodified response
let supabaseResponse = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
supabaseUrl!,
supabaseKey!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
},
);
return supabaseResponse
};
`,
},
]
return <MultipleCodeBlock files={files} />
}
// [Joshen] Used as a dynamic import
// eslint-disable-next-line no-restricted-exports
export default ContentFile
@@ -1,69 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: [
`NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
},
{
name: 'utils/supabase.ts',
language: 'ts',
code: `
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'};
export const supabase = createClient(supabaseUrl, supabaseKey);
`,
},
{
name: '_app.tsx',
language: 'tsx',
code: `
import { useState, useEffect } from 'react'
import { supabase } from '../utils/supabase'
function Page() {
const [todos, setTodos] = useState([])
useEffect(() => {
function getTodos() {
const { data: todos } = await supabase.from('todos').select()
if (todos.length > 1) {
setTodos(todos)
}
}
getTodos()
}, [])
return (
<div>
{todos.map((todo) => (
<li key={todo}>{todo}</li>
))}
</div>
)
}
export default Page
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,58 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: `
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'}
`,
},
{
name: 'utils/supabase.ts',
language: 'ts',
code: `
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
`,
},
{
name: 'src/App.vue',
language: 'jsx',
code: `
<script setup>
import { supabase } from '../utils/supabase'
const todos = ref([])
async function getTodos() {
const { data } = await supabase.from('todos').select()
todos.value = data
}
onMounted(() => {
getTodos()
})
</script>
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">{{ todo.name }}</li>
</ul>
</template>
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,62 +0,0 @@
import { IS_PLATFORM } from 'lib/constants'
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
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
@@ -1,68 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: [
`REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `REACT_APP_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `REACT_APP_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
},
{
name: 'utils/supabase.ts',
language: 'ts',
code: `
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL;
const supabaseKey = process.env.${projectKeys.publishableKey ? 'REACT_APP_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'REACT_APP_SUPABASE_ANON_KEY'};
export const supabase = createClient(supabaseUrl, supabaseKey);
`,
},
{
name: 'App.tsx',
language: 'tsx',
code: `
import { useState, useEffect } from 'react'
import { supabase } from '../utils/supabase'
function Page() {
const [todos, setTodos] = useState([])
useEffect(() => {
function getTodos() {
const { data: todos } = await supabase.from('todos').select()
if (todos.length > 1) {
setTodos(todos)
}
}
getTodos()
}, [])
return (
<div>
{todos.map((todo) => (
<li key={todo}>{todo}</li>
))}
</div>
)
}
export default Page
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,70 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env',
language: 'bash',
code: [
`VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
},
{
name: 'utils/supabase.ts',
language: 'ts',
code: `
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.${projectKeys.publishableKey ? 'VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'VITE_SUPABASE_ANON_KEY'};
const supabase = createClient(supabaseUrl, supabaseKey);
export default supabase
`,
},
{
name: 'App.tsx',
language: 'tsx',
code: `
import { useState, useEffect } from 'react'
import { supabase } from '../utils/supabase'
function Page() {
const [todos, setTodos] = useState([])
useEffect(() => {
function getTodos() {
const { data: todos } = await supabase.from('todos').select()
if (todos.length > 1) {
setTodos(todos)
}
}
getTodos()
}, [])
return (
<div>
{todos.map((todo) => (
<li key={todo}>{todo}</li>
))}
</div>
)
}
export default Page
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,102 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: [
`SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
`SUPABASE_KEY=${projectKeys?.publishableKey ?? projectKeys?.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
},
{
name: 'src/utility/supabaseClient.ts',
language: 'ts',
code: `
import { createClient } from "@refinedev/supabase";
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_KEY = process.env.SUPABASE_KEY;
export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, {
db: {
schema: "public",
},
auth: {
persistSession: true,
},
});
`,
},
{
name: 'src/App.tsx',
language: 'tsx',
code: `
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import routerProvider, {
DocumentTitleHandler,
NavigateToResource,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import { dataProvider, liveProvider } from "@refinedev/supabase";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import authProvider from "./authProvider";
import { supabaseClient } from "./utility";
import { CountriesCreate, CountriesEdit, CountriesList, CountriesShow } from "./pages/countries";
function App() {
return (
<BrowserRouter>
<RefineKbarProvider>
<Refine
dataProvider={dataProvider(supabaseClient)}
liveProvider={liveProvider(supabaseClient)}
authProvider={authProvider}
routerProvider={routerProvider}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
}}
resources={[{
name: "countries",
list: "/countries",
create: "/countries/create",
edit: "/countries/edit/:id",
show: "/countries/show/:id"
}]}>
<Routes>
<Route index
element={<NavigateToResource resource="countries" />}
/>
<Route path="/countries">
<Route index element={<CountriesList />} />
<Route path="create" element={<CountriesCreate />} />
<Route path="edit/:id" element={<CountriesEdit />} />
<Route path="show/:id" element={<CountriesShow />} />
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</RefineKbarProvider>
</BrowserRouter>
);
}
export default App;
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,91 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env',
language: 'bash',
code: [
`VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
},
{
name: 'app/utils/supabase.server.ts',
language: 'ts',
code: `
import {
createServerClient,
parseCookieHeader,
serializeCookieHeader,
} from "@supabase/ssr";
export function createClient(request: Request) {
const headers = new Headers();
const supabase = createServerClient(
process.env.VITE_SUPABASE_URL!,
process.env.VITE_${projectKeys.publishableKey ? 'SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'SUPABASE_ANON_KEY'};,
{
cookies: {
getAll() {
return parseCookieHeader(request.headers.get("Cookie") ?? "") as {
name: string;
value: string;
}[];
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
headers.append(
"Set-Cookie",
serializeCookieHeader(name, value, options)
)
);
},
},
}
);
return { supabase, headers };
}
`,
},
{
name: 'app/routes/_index.tsx',
language: 'tsx',
code: `
import type { Route } from "./+types/home";
import { createClient } from "~/utils/supabase.server";
export async function loader({ request }: Route.LoaderArgs) {
const { supabase } = createClient(request);
const { data: todos } = await supabase.from("todos").select();
return { todos };
}
export default function Home({ loaderData }: Route.ComponentProps) {
return (
<>
<ul>
{loaderData.todos?.map((todo) => (
<li key={todo.id}>{todo.name}</li>
))}
</ul>
</>
);
}
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,60 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: [
`SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
},
{
name: 'utils/supabase.ts',
language: 'ts',
code: `
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.${projectKeys.publishableKey ? 'SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'SUPABASE_ANON_KEY'};
export const supabase = createClient(supabaseUrl!, supabaseKey!);
`,
},
{
name: 'src/App.jsx',
language: 'jsx',
code: `
import { supabase } from '../utils/supabase'
import { createResource, For } from "solid-js";
async function getTodos() {
const { data: todos } = await supabase.from("todos").select();
return data;
}
function App() {
const [todos] = createResource(getTodos);
return (
<ul>
<For each={todos()}>{(country) => <li>{todo.name}</li>}</For>
</ul>
);
}
export default App;
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -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
@@ -1,264 +0,0 @@
import { useEffect, useMemo, useState } from 'react'
import { CodeBlock } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
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
@@ -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
@@ -1,55 +0,0 @@
import { Copy } from 'lucide-react'
import { useMemo, useState } from 'react'
import { Button, copyToClipboard } from 'ui'
import type { StepContentProps } from '../../../Connect.types'
import { resolveFrameworkLibraryKey } from '../../../Connect.utils'
import { INSTALL_COMMANDS } from '../../../connect.schema'
/**
* Gets the install command for the current framework selection.
*/
function getInstallCommand(state: StepContentProps['state']): string | null {
const libraryKey = resolveFrameworkLibraryKey(state)
if (libraryKey && INSTALL_COMMANDS[libraryKey]) return INSTALL_COMMANDS[libraryKey]
return null
}
function InstallContent({ state }: StepContentProps) {
const installCommand = useMemo(() => getInstallCommand(state), [state])
const [copyLabel, setCopyLabel] = useState('Copy')
if (!installCommand) {
return null
}
const handleCopy = () => {
copyToClipboard(installCommand, () => {
setCopyLabel('Copied')
setTimeout(() => setCopyLabel('Copy'), 2000)
})
}
return (
<div className="relative group">
<div className="bg-surface-75 border rounded-lg p-3 pr-20 font-mono text-sm text-foreground-light overflow-x-auto">
<code>{installCommand}</code>
</div>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<Button
type="default"
size="tiny"
icon={<Copy size={14} />}
onClick={handleCopy}
className="opacity-0 group-hover:opacity-100 transition-opacity"
>
{copyLabel}
</Button>
</div>
</div>
)
}
export default InstallContent
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,52 +0,0 @@
import { Copy } from 'lucide-react'
import { useMemo, useState } from 'react'
import { Button, copyToClipboard } from 'ui'
import type { StepContentProps } from '../../../../Connect.types'
function getShadcnCommand(state: StepContentProps['state']): string | null {
if (state.framework === 'nextjs') {
return 'npx shadcn@latest add @supabase/supabase-client-nextjs'
}
if (state.framework === 'react') {
return 'npx shadcn@latest add @supabase/supabase-client-react-router'
}
return null
}
function ShadcnCommandContent({ state }: StepContentProps) {
const command = useMemo(() => getShadcnCommand(state), [state])
const [copyLabel, setCopyLabel] = useState('Copy')
if (!command) return null
const handleCopy = () => {
copyToClipboard(command, () => {
setCopyLabel('Copied')
setTimeout(() => setCopyLabel('Copy'), 2000)
})
}
return (
<div className="relative group">
<div className="bg-surface-75 border rounded-lg p-3 pr-20 font-mono text-sm text-foreground-light overflow-x-auto">
<code>{command}</code>
</div>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<Button
type="default"
size="tiny"
icon={<Copy size={14} />}
onClick={handleCopy}
className="opacity-0 group-hover:opacity-100 transition-opacity"
>
{copyLabel}
</Button>
</div>
</div>
)
}
export default ShadcnCommandContent
@@ -1,16 +0,0 @@
import { ExternalLink } from 'lucide-react'
import { Button } from 'ui'
import type { StepContentProps } from '../../../../Connect.types'
function ShadcnExploreContent(_props: StepContentProps) {
return (
<Button asChild type="default" icon={<ExternalLink size={14} />}>
<a href="https://supabase.com/ui" target="_blank" rel="noreferrer">
Explore supabase.com/ui
</a>
</Button>
)
}
export default ShadcnExploreContent
@@ -1,39 +0,0 @@
import { Copy } from 'lucide-react'
import { useState } from 'react'
import { Button, copyToClipboard } from 'ui'
import type { StepContentProps } from '../../../Connect.types'
const SKILLS_COMMAND = 'npx skills add supabase/agent-skills'
function SkillsInstallContent(_props: StepContentProps) {
const [copyLabel, setCopyLabel] = useState('Copy')
const handleCopy = () => {
copyToClipboard(SKILLS_COMMAND, () => {
setCopyLabel('Copied')
setTimeout(() => setCopyLabel('Copy'), 2000)
})
}
return (
<div className="relative group">
<div className="bg-surface-75 border rounded-lg p-3 pr-20 font-mono text-sm text-foreground-light overflow-x-auto">
<code>{SKILLS_COMMAND}</code>
</div>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<Button
type="default"
size="tiny"
icon={<Copy size={14} />}
onClick={handleCopy}
className="opacity-0 group-hover:opacity-100 transition-opacity"
>
{copyLabel}
</Button>
</div>
</div>
)
}
export default SkillsInstallContent
@@ -1,65 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: [
`PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
},
{
name: 'src/lib/supabaseClient.js',
language: 'js',
code: `
import { createClient } from "@supabase/supabase-js";
import { PUBLIC_SUPABASE_URL, ${projectKeys.publishableKey ? 'PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'PUBLIC_SUPABASE_ANON_KEY'} } from "$env/static/public"
const supabaseUrl = PUBLIC_SUPABASE_URL;
const supabaseKey = ${projectKeys.publishableKey ? 'PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'PUBLIC_SUPABASE_ANON_KEY'};
export const supabase = createClient(supabaseUrl, supabaseKey);
`,
},
{
name: 'src/routes/+page.server.js',
language: 'js',
code: `
import { supabase } from "$lib/supabaseClient";
export async function load() {
const { data } = await supabase.from("countries").select();
return {
countries: data ?? [],
};
}
`,
},
{
name: 'src/routes/+page.svelte',
language: 'html',
code: `
<script>
export let data;
</script>
<ul>
{#each data.countries as country}
<li>{country.name}</li>
{/each}
</ul>
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,70 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: 'Supabase.swift',
language: 'swift',
code: `
import Foundation
import Supabase
let supabase = SupabaseClient(
supabaseURL: URL(string: "${projectKeys.apiUrl ?? 'your-project-url'}")!,
supabaseKey: "${projectKeys.publishableKey ?? '<prefer publishable key for native apps instead of anon key>'}"
)
`,
},
{
name: 'Todo.swift',
language: 'swift',
code: `
import Foundation
struct Todo: Identifiable, Decodable {
var id: Int
var title: String
}
`,
},
{
name: 'ContentView.swift',
language: 'swift',
code: `
import Supabase
import SwiftUI
struct ContentView: View {
@State var todos: [Todo] = []
var body: some View {
NavigationStack {
List(todos) { todo in
Text(todo.title)
}
.navigationTitle("Todos")
.task {
do {
todos = try await supabase.from("todos").select().execute().value
} catch {
debugPrint(error)
}
}
}
}
}
#Preview {
ContentView()
}
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,60 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env',
language: 'bash',
code: `
VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
VITE_SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'}
`,
},
{
name: 'src/utils/supabase.ts',
language: 'ts',
code: `
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_KEY
);
`,
},
{
name: 'src/routes/index.tsx',
language: 'tsx',
code: `
import { createFileRoute } from '@tanstack/react-router'
import { supabase } from '../utils/supabase'
export const Route = createFileRoute('/')({
loader: async () => {
const { data: todos } = await supabase.from('todos').select()
return { todos }
},
component: Home,
})
function Home() {
const { todos } = Route.useLoaderData()
return (
<ul>
{todos?.map((todo) => (
<li key={todo.id}>{todo.name}</li>
))}
</ul>
)
}
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,58 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/multiple-code-block'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code: `
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'}
`,
},
{
name: 'utils/supabase.ts',
language: 'ts',
code: `
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
`,
},
{
name: 'App.vue',
language: 'jsx',
code: `
<script setup>
import { supabase } from '../utils/supabase'
const todos = ref([])
async function getTodos() {
const { data } = await supabase.from('todos').select()
todos.value = data
}
onMounted(() => {
getTodos()
})
</script>
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">{{ todo.name }}</li>
</ul>
</template>
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,482 +0,0 @@
import { describe, test, expect } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useConnectState } from './useConnectState'
describe('useConnectState', () => {
// ============================================================================
// Initial State Tests
// ============================================================================
describe('initial state', () => {
test('should initialize with framework mode by default', () => {
const { result } = renderHook(() => useConnectState())
expect(result.current.state.mode).toBe('framework')
})
test('should initialize with nextjs as default framework', () => {
const { result } = renderHook(() => useConnectState())
expect(result.current.state.framework).toBe('nextjs')
})
test('should initialize with app variant for nextjs', () => {
const { result } = renderHook(() => useConnectState())
expect(result.current.state.frameworkVariant).toBe('app')
})
test('should initialize with supabasejs library', () => {
const { result } = renderHook(() => useConnectState())
expect(result.current.state.library).toBe('supabasejs')
})
test('should accept initial state override', () => {
const { result } = renderHook(() =>
useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
)
expect(result.current.state.mode).toBe('direct')
expect(result.current.state.connectionMethod).toBe('transaction')
})
test('should merge initial state with defaults', () => {
const { result } = renderHook(() => useConnectState({ framework: 'react' }))
expect(result.current.state.mode).toBe('framework')
expect(result.current.state.framework).toBe('react')
})
})
// ============================================================================
// Mode Switching Tests
// ============================================================================
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', () => {
const { result } = renderHook(() => useConnectState())
// Change framework
act(() => {
result.current.updateField('framework', 'react')
})
// Switch to direct
act(() => {
result.current.setMode('direct')
})
// Switch back to framework
act(() => {
result.current.setMode('framework')
})
expect(result.current.state.framework).toBe('react')
})
})
// ============================================================================
// Field Update Tests
// ============================================================================
describe('updateField', () => {
test('should update framework selection', () => {
const { result } = renderHook(() => useConnectState())
act(() => {
result.current.updateField('framework', 'react')
})
expect(result.current.state.framework).toBe('react')
})
test('should cascade variant reset when changing framework', () => {
const { result } = renderHook(() => useConnectState())
// Start with nextjs which has variants
expect(result.current.state.frameworkVariant).toBe('app')
// Switch to a framework with multiple variants
act(() => {
result.current.updateField('framework', 'react')
})
// Should have the first variant of react
expect(result.current.state.frameworkVariant).toBeDefined()
})
test('should remove variant when switching to framework without variants', () => {
const { result } = renderHook(() => useConnectState())
// Start with nextjs which has variants
expect(result.current.state.frameworkVariant).toBe('app')
// Switch to remix which has no variants
act(() => {
result.current.updateField('framework', 'remix')
})
expect(result.current.state.frameworkVariant).toBeUndefined()
})
test('should update library when variant changes', () => {
const { result } = renderHook(() => useConnectState())
act(() => {
result.current.updateField('frameworkVariant', 'pages')
})
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())
act(() => {
result.current.updateField('frameworkUi', true)
})
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')
})
})
// ============================================================================
// Active Fields Tests
// ============================================================================
describe('activeFields', () => {
test('should return framework mode fields', () => {
const { result } = renderHook(() => useConnectState())
const fieldIds = result.current.activeFields.map((f) => f.id)
expect(fieldIds).toContain('framework')
})
test('should include variant field for nextjs', () => {
const { result } = renderHook(() => useConnectState({ framework: 'nextjs' }))
const fieldIds = result.current.activeFields.map((f) => f.id)
expect(fieldIds).toContain('frameworkVariant')
})
test('should include frameworkUi field for nextjs', () => {
const { result } = renderHook(() => useConnectState({ framework: 'nextjs' }))
const fieldIds = result.current.activeFields.map((f) => f.id)
expect(fieldIds).toContain('frameworkUi')
})
test('should not include frameworkUi for non-nextjs/react frameworks', () => {
const { result } = renderHook(() => useConnectState({ framework: 'remix' }))
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')
})
})
// ============================================================================
// Resolved Steps Tests
// ============================================================================
describe('resolvedSteps', () => {
test('should resolve steps for framework mode', () => {
const { result } = renderHook(() => useConnectState())
expect(result.current.resolvedSteps.length).toBeGreaterThan(0)
})
test('should have install step for framework mode', () => {
const { result } = renderHook(() => useConnectState())
const stepIds = result.current.resolvedSteps.map((s) => s.id)
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())
const stepIds = result.current.resolvedSteps.map((s) => s.id)
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 })
)
const stepIds = result.current.resolvedSteps.map((s) => s.id)
expect(stepIds).toContain('shadcn-add')
})
})
// ============================================================================
// Field Options Tests
// ============================================================================
describe('getFieldOptions', () => {
test('should return framework options', () => {
const { result } = renderHook(() => useConnectState())
const options = result.current.getFieldOptions('framework')
expect(options.length).toBeGreaterThan(0)
expect(options.some((o) => o.value === 'nextjs')).toBe(true)
expect(options.some((o) => o.value === 'react')).toBe(true)
})
test('should return variant options for nextjs', () => {
const { result } = renderHook(() => useConnectState({ framework: 'nextjs' }))
const options = result.current.getFieldOptions('frameworkVariant')
expect(options.length).toBeGreaterThan(0)
expect(options.some((o) => o.value === 'app')).toBe(true)
expect(options.some((o) => o.value === 'pages')).toBe(true)
})
test('should return empty variant options for frameworks without variants', () => {
const { result } = renderHook(() => useConnectState({ framework: 'remix' }))
const options = result.current.getFieldOptions('frameworkVariant')
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())
const options = result.current.getFieldOptions('unknownField')
expect(options).toEqual([])
})
test('should return library options for selected framework', () => {
const { result } = renderHook(() =>
useConnectState({ framework: 'nextjs', frameworkVariant: 'app' })
)
const options = result.current.getFieldOptions('library')
expect(options.length).toBeGreaterThan(0)
})
})
// ============================================================================
// Schema Access Tests
// ============================================================================
describe('schema', () => {
test('should expose the connect schema', () => {
const { result } = renderHook(() => useConnectState())
expect(result.current.schema).toBeDefined()
expect(result.current.schema.modes).toBeDefined()
expect(result.current.schema.fields).toBeDefined()
expect(result.current.schema.steps).toBeDefined()
})
test('should have all expected modes in schema', () => {
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')
})
})
})
@@ -1,307 +0,0 @@
import { useCallback, useMemo, useState } from 'react'
import { FEATURE_GROUPS_PLATFORM, MCP_CLIENTS } from 'ui-patterns/McpUrlBuilder'
import {
connectionStringMethodOptions,
DATABASE_CONNECTION_TYPES,
FRAMEWORKS,
MOBILES,
ORMS,
} from './Connect.constants'
import type {
ConnectMode,
ConnectSchema,
ConnectState,
FieldOption,
ResolvedField,
ResolvedStep,
} from './Connect.types'
import { resolveFrameworkLibraryKey } from './Connect.utils'
import {
getActiveFields,
getDefaultState,
resetDependentFields,
resolveSteps,
} from './connect.resolver'
import { connectSchema } from './connect.schema'
// ============================================================================
// Data Source Helpers
// ============================================================================
/**
* Get field options from a data source reference.
* This maps source names to actual data.
*/
function getFieldOptionsFromSource(source: string, state: ConnectState): FieldOption[] {
switch (source) {
case 'frameworks':
return [...FRAMEWORKS, ...MOBILES].map((f) => ({
value: f.key,
label: f.label,
icon: f.icon,
}))
case 'frameworkVariants': {
// Get variants for the selected framework
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
const selected = allFrameworks.find((f) => f.key === state.framework)
if (!selected?.children?.length) return []
// Only return if there are multiple children (variants)
if (selected.children.length <= 1) return []
return selected.children.map((c) => ({
value: c.key,
label: c.label,
icon: c.icon,
}))
}
case 'libraries': {
// Get libraries for the selected framework and variant
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
const selectedFramework = allFrameworks.find((f) => f.key === state.framework)
if (!selectedFramework) return []
// If framework has variants, look in the variant
if (selectedFramework.children?.length > 1 && state.frameworkVariant) {
const variant = selectedFramework.children.find((c) => c.key === state.frameworkVariant)
if (variant?.children?.length) {
return variant.children.map((c) => ({
value: c.key,
label: c.label,
icon: c.icon,
}))
}
}
// Otherwise look directly in framework children
if (selectedFramework.children?.length === 1) {
const child = selectedFramework.children[0]
if (child.children?.length) {
return child.children.map((c) => ({
value: c.key,
label: c.label,
icon: c.icon,
}))
}
// The child itself is the library
return [{ value: child.key, label: child.label, icon: child.icon }]
}
return []
}
case 'connectionMethods':
return Object.values(connectionStringMethodOptions).map((m) => ({
value: m.value,
label: m.label,
description: m.description,
}))
case 'connectionTypes':
return DATABASE_CONNECTION_TYPES.map((t) => ({
value: t.id,
label: t.label,
}))
case 'orms':
return ORMS.map((o) => ({
value: o.key,
label: o.label,
icon: o.icon,
}))
case 'mcpClients':
return MCP_CLIENTS.map((c) => ({
value: c.key,
label: c.label,
icon: c.icon,
}))
case 'mcpFeatures':
return FEATURE_GROUPS_PLATFORM.map((f) => ({
value: f.id,
label: f.name,
description: f.description,
}))
default:
return []
}
}
/**
* Resolve field options, handling both static options and data source references.
*/
function resolveFieldOptionsWithSource(field: ResolvedField, state: ConnectState): FieldOption[] {
// If already resolved (from conditional resolution)
if (field.resolvedOptions.length > 0) {
return field.resolvedOptions
}
// Check if it's a source reference
const options = connectSchema.fields[field.id]?.options
if (options && typeof options === 'object' && 'source' in options) {
return getFieldOptionsFromSource(options.source as string, state)
}
return []
}
// ============================================================================
// Hook
// ============================================================================
export interface UseConnectStateReturn {
state: ConnectState
updateField: (fieldId: string, value: string | boolean | string[]) => void
setMode: (mode: ConnectMode) => void
activeFields: ResolvedField[]
resolvedSteps: ResolvedStep[]
getFieldOptions: (fieldId: string) => FieldOption[]
schema: ConnectSchema
}
export function useConnectState(initialState?: Partial<ConnectState>): UseConnectStateReturn {
const [state, setState] = useState<ConnectState>(() => {
const defaults = getDefaultState(connectSchema)
// Set initial framework if mode is framework
if (defaults.mode === 'framework' && !defaults.framework) {
const firstFramework = FRAMEWORKS[0]
defaults.framework = firstFramework?.key ?? ''
// Set initial variant if framework has variants
if (firstFramework?.children?.length > 1) {
defaults.frameworkVariant = firstFramework.children[0]?.key ?? ''
}
// Set initial library
const libraryKey = resolveFrameworkLibraryKey({
framework: defaults.framework,
frameworkVariant: defaults.frameworkVariant,
library: defaults.library,
})
if (libraryKey) defaults.library = libraryKey
}
// Set initial ORM if mode is orm
if (defaults.mode === 'orm' && !defaults.orm) {
defaults.orm = ORMS[0]?.key ?? ''
}
// Set initial MCP client if mode is mcp
if (defaults.mode === 'mcp' && !defaults.mcpClient) {
defaults.mcpClient = MCP_CLIENTS[0]?.key ?? ''
}
return { ...defaults, ...initialState } as ConnectState
})
const updateField = useCallback((fieldId: string, value: string | boolean | string[]) => {
setState((prev) => {
const next = { ...prev, [fieldId]: value }
// Handle cascading updates for framework selection
if (fieldId === 'framework') {
const allFrameworks = [...FRAMEWORKS, ...MOBILES]
const selected = allFrameworks.find((f) => f.key === value)
// Reset variant if framework changed
if (selected?.children && selected.children.length > 1) {
next.frameworkVariant = selected.children[0]?.key ?? ''
} else {
delete next.frameworkVariant
}
// Reset library
const libraryKey = resolveFrameworkLibraryKey({
framework: next.framework,
frameworkVariant: next.frameworkVariant,
library: next.library,
})
if (libraryKey) {
next.library = libraryKey
} else {
delete next.library
}
}
// Handle cascading updates for variant selection
if (fieldId === 'frameworkVariant') {
const libraryKey = resolveFrameworkLibraryKey({
framework: prev.framework,
frameworkVariant: String(value),
library: next.library,
})
if (libraryKey) next.library = libraryKey
}
// Reset useSharedPooler when connectionMethod changes to 'direct'
if (fieldId === 'connectionMethod' && value === 'direct') {
next.useSharedPooler = false
}
return resetDependentFields(next, fieldId, connectSchema)
})
}, [])
const setMode = useCallback((mode: ConnectMode) => {
setState((prev) => {
const next: ConnectState = { ...prev, mode }
// Initialize mode-specific defaults
if (mode === 'framework' && !next.framework) {
const firstFramework = FRAMEWORKS[0]
next.framework = firstFramework?.key ?? ''
if (firstFramework?.children?.length > 1) {
next.frameworkVariant = firstFramework.children[0]?.key ?? ''
}
const libraryKey = resolveFrameworkLibraryKey({
framework: next.framework,
frameworkVariant: next.frameworkVariant,
library: next.library,
})
if (libraryKey) next.library = libraryKey
}
if (mode === 'direct') {
next.connectionMethod = next.connectionMethod ?? 'direct'
next.connectionType = next.connectionType ?? 'uri'
}
if (mode === 'orm' && !next.orm) {
next.orm = ORMS[0]?.key ?? ''
}
if (mode === 'mcp' && !next.mcpClient) {
next.mcpClient = MCP_CLIENTS[0]?.key ?? ''
}
return next
})
}, [])
const activeFields = useMemo(() => getActiveFields(connectSchema, state), [state])
const resolvedSteps = useMemo(() => resolveSteps(connectSchema, state), [state])
const getFieldOptions = useCallback(
(fieldId: string): FieldOption[] => {
const field = activeFields.find((f) => f.id === fieldId)
if (!field) return []
return resolveFieldOptionsWithSource(field, state)
},
[activeFields, state]
)
return {
state,
updateField,
setMode,
activeFields,
resolvedSteps,
getFieldOptions,
schema: connectSchema,
}
}
@@ -1,9 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { Loader } from 'lucide-react'
import { ConnectButton } from '@/components/interfaces/ConnectButton/ConnectButton'
import { ConnectionIcon } from '@/components/interfaces/ConnectSheet/ConnectionIcon'
import { useParams } from 'common'
import { AlertError } from 'components/ui/AlertError'
import { InlineLink } from 'components/ui/InlineLink'
@@ -12,11 +8,15 @@ import { useLegacyAPIKeysStatusQuery } from 'data/api-keys/legacy-api-keys-statu
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Loader } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'ui'
import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { ConnectionIcon } from '@/components/interfaces/Connect/ConnectionIcon'
import { ConnectButton } from '@/components/interfaces/ConnectButton/ConnectButton'
export const APIKeys = () => {
const { ref: projectRef } = useParams()
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
@@ -1,5 +1,3 @@
import { ConnectionType } from '@/components/interfaces/ConnectSheet/Connect.constants'
import { ConnectionIcon } from '@/components/interfaces/ConnectSheet/ConnectionIcon'
import { Box, Check, ChevronDown } from 'lucide-react'
import { useState } from 'react'
import {
@@ -16,6 +14,9 @@ import {
cn,
} from 'ui'
import { ConnectionType } from '@/components/interfaces/Connect/Connect.constants'
import { ConnectionIcon } from '@/components/interfaces/Connect/ConnectionIcon'
interface FrameworkSelectorProps {
value: string
onChange: (value: string) => void