diff --git a/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts b/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts deleted file mode 100644 index 51ec0adf146..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts +++ /dev/null @@ -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.', - }, -} diff --git a/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts b/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts deleted file mode 100644 index 32d27e6a231..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts +++ /dev/null @@ -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 - | { - [stateValue: string]: ConditionalValue | 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 - // Only show this field when these state conditions are met - dependsOn?: Record - // 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 -} - -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 - // 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 -} diff --git a/apps/studio/components/interfaces/ConnectSheet/Connect.utils.test.ts b/apps/studio/components/interfaces/ConnectSheet/Connect.utils.test.ts deleted file mode 100644 index 62d04774378..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/Connect.utils.test.ts +++ /dev/null @@ -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') - }) -}) diff --git a/apps/studio/components/interfaces/ConnectSheet/Connect.utils.ts b/apps/studio/components/interfaces/ConnectSheet/Connect.utils.ts deleted file mode 100644 index ca4f3a69d55..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/Connect.utils.ts +++ /dev/null @@ -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 -} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx deleted file mode 100644 index 7b888aff1f2..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx +++ /dev/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 = { - framework: , - direct: , - orm: , - mcp: , -} - -interface ConnectConfigSectionProps { - activeFields: ResolvedField[] - state: Record - 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 ( -
- {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 ( - - onFieldChange(field.id, v)} - items={allFrameworks} - className="w-full" - size="small" - /> - - ) - } - - // Default radio-grid behavior for other fields - return ( - - onFieldChange(field.id, v)} - className="flex-row gap-3 space-y-0" - > - {options.map((option) => ( - -
- {option.icon && } - {option.label} -
-
- ))} -
-
- ) - - case 'radio-list': - return ( - - onFieldChange(field.id, v)} - > - {options.map((option) => ( - -
-
- {option.icon && } - {option.label} -
- {option.description && ( - - {option.description} - - )} -
-
- ))} -
-
- ) - - 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 ( - - onFieldChange(field.id, v)} - /> - - ) - } - - return ( - - onFieldChange(field.id, v)} - > - - - - - {options.map((option) => ( - - {option.label} - - ))} - - - - ) - - case 'switch': - return ( - - onFieldChange(field.id, v)} - /> - - ) - - case 'multi-select': - return ( - - onFieldChange(field.id, v)} - > - - - - {options.map((option) => ( - -
- {option.label} - {option.description && ( - - {option.description} - - )} -
-
- ))} -
-
-
-
- ) - - default: - return null - } - })} -
- ) -} - -// ============================================================================ -// 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 ( -
- {modes.map((mode) => ( - - ))} -
- ) -} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx index 94e88691cbf..e2064944460 100644 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx @@ -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 ( @@ -106,36 +16,12 @@ export const ConnectSheet = ({ open, onOpenChange, connectTab }: ConnectSheetPro Connect to your project Choose how you want to use Supabase -
- -
{/* Configuration Section */} -
- -
- -
-
- - {/* Steps Section */} - +
diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectSheetStep.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectSheetStep.tsx deleted file mode 100644 index 374499af592..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectSheetStep.tsx +++ /dev/null @@ -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 ( -
-
- {/* Left column: number, title, description */} -
-
-
- -
-
-

{title}

-

{description}

-
-
- - {/* Right column: content */} -
- {children} -
-
-
- ) -} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx deleted file mode 100644 index f927d7c0e7d..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx +++ /dev/null @@ -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(() => import(`./content/${filePath}/content`), { - loading: () => ( -
- -
- ), - }) - }, [filePath]) - - return ( - - ) -} - -export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsSectionProps) { - const stepsContainerRef = useRef(null) - const connectionStringPooler = useConnectionStringPooler() - - if (steps.length === 0) return null - - return ( -
-
-

Connect your app

- - - -
- {steps.map((step, index) => ( - - - - ))} -
-
-
- ) -} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectionIcon.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectionIcon.tsx deleted file mode 100644 index b2de0e20795..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectionIcon.tsx +++ /dev/null @@ -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 ( - {`${icon} - ) -} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectionParameters.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectionParameters.tsx deleted file mode 100644 index db729effa07..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectionParameters.tsx +++ /dev/null @@ -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>({}) - - return ( -
- {parameters.map((param) => ( -
-
- {param.key}: - {param.value} - -
-
- ))} -
- ) -} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectionString.utils.ts b/apps/studio/components/interfaces/ConnectSheet/ConnectionString.utils.ts deleted file mode 100644 index a681ca002c4..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectionString.utils.ts +++ /dev/null @@ -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 }, -] diff --git a/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx b/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx deleted file mode 100644 index de8d545544f..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx +++ /dev/null @@ -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 -} - -export function CopyPromptAdmonition({ stepsContainerRef }: CopyPromptAdmonitionProps) { - const copyResetTimerRef = useRef | 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() - - 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 ( - } onClick={handleCopyPrompt}> - {hasCopiedPrompt ? 'Copied' : 'Copy prompt'} - - } - > - {/* Background image */} -
- Supabase Grafana - Supabase Grafana -
-
- - {/* Content */} -
-
-
- - Skip the steps - -

Prompt your agent

-
-

- Copy a prompt with everything your agent needs to connect your app for you. -

-
-
- - ) -} diff --git a/apps/studio/components/interfaces/ConnectSheet/DatabaseSettings.utils.ts b/apps/studio/components/interfaces/ConnectSheet/DatabaseSettings.utils.ts deleted file mode 100644 index 0f331df2b8b..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/DatabaseSettings.utils.ts +++ /dev/null @@ -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 [] -} diff --git a/apps/studio/components/interfaces/ConnectSheet/DirectConnectionExamples.tsx b/apps/studio/components/interfaces/ConnectSheet/DirectConnectionExamples.tsx deleted file mode 100644 index 80084d1337d..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/DirectConnectionExamples.tsx +++ /dev/null @@ -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 diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts deleted file mode 100644 index 0b196767a12..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts +++ /dev/null @@ -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 = { - 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 = { - 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 = { - 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 = { - 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 = { - framework: 'Framework', - direct: 'Direct', - } - - expect(resolveConditional(conditional, { mode: 'orm' })).toBe(undefined) - }) - - test('should handle frameworkUi boolean state (as string "true"/"false")', () => { - const conditional: ConditionalValue = { - 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') - }) -}) diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts deleted file mode 100644 index 2baeb7940c1..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts +++ /dev/null @@ -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 { - 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( - value: ConditionalValue, - 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> - 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(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 => !!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( - field.options as ConditionalValue, - 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 -} diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts deleted file mode 100644 index c574b40871f..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts +++ /dev/null @@ -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' - ) - }) -}) diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts deleted file mode 100644 index f4171d1877b..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts +++ /dev/null @@ -1,347 +0,0 @@ -import type { ConnectSchema, StepDefinition } from './Connect.types' - -/** - * Install commands for different packages - */ -export const INSTALL_COMMANDS: Record = { - 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], - }, - }, -} diff --git a/apps/studio/components/interfaces/ConnectSheet/content/androidkotlin/supabasekt/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/androidkotlin/supabasekt/content.tsx deleted file mode 100644 index c6b7e2b9c7f..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/androidkotlin/supabasekt/content.tsx +++ /dev/null @@ -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 ?? ''}" - ) { - 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>(listOf()) } - LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { - items = supabase.from("todos") - .select().decodeList() - } - } - 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 -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/astro/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/astro/supabasejs/content.tsx deleted file mode 100644 index 68dfffcfedd..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/astro/supabasejs/content.tsx +++ /dev/null @@ -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('*'); ---- - -{ - ( -
    - {data.map((entry) => ( -
  • {entry.name}
  • - ))} -
- ) -} -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/drizzle/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/drizzle/content.tsx deleted file mode 100644 index dd7321e25c0..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/drizzle/content.tsx +++ /dev/null @@ -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 -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/exporeactnative/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/exporeactnative/supabasejs/content.tsx deleted file mode 100644 index 5fba8bfd52d..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/exporeactnative/supabasejs/content.tsx +++ /dev/null @@ -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 ?? ''} - `, - }, - { - 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 ( - - Todo List - item.id.toString()} - renderItem={({ item }) => {item.title}} - /> - - ); -}; - -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/flask/supabasepy/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/flask/supabasepy/content.tsx deleted file mode 100644 index 111537c3d4c..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/flask/supabasepy/content.tsx +++ /dev/null @@ -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 = '

Todos

    ' - for todo in todos: - html += f'
  • {todo["name"]}
  • ' - html += '
' - - return html - -if __name__ == '__main__': - app.run(debug=True) -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/flutter/supabaseflutter/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/flutter/supabaseflutter/content.tsx deleted file mode 100644 index bcca04e6700..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/flutter/supabaseflutter/content.tsx +++ /dev/null @@ -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 main() async { - await Supabase.initialize( - url: '${projectKeys.apiUrl ?? 'your-project-url'}', - anonKey: '${projectKeys.publishableKey ?? ''}', - ); - 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 createState() => _HomePageState(); -} - -class _HomePageState extends State { - 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 -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/ionicangular/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/ionicangular/supabasejs/content.tsx deleted file mode 100644 index f5ace62ad61..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/ionicangular/supabasejs/content.tsx +++ /dev/null @@ -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 ?? ''}', -}; -`, - }, - { - 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: ` - - - Todo List - - - - - - - {{ todo.title }} - - - -`, - }, - { - 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 -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/ionicreact/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/ionicreact/supabasejs/content.tsx deleted file mode 100644 index 38a4ea91b93..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/ionicreact/supabasejs/content.tsx +++ /dev/null @@ -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 ?? ''} - `, - }, - { - 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 ( - - <> - - - Todos - - - - - {todos.map((todo) => ( - {todo.title} - ))} - - - - - ); -} -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/nextjs/app/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/nextjs/app/supabasejs/content.tsx deleted file mode 100644 index d92080af898..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/nextjs/app/supabasejs/content.tsx +++ /dev/null @@ -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 ( -
    - {todos?.map((todo) => ( -
  • {todo}
  • - ))} -
- ) -} -`, - }, - { - 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) => { - 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 -} - -// [Joshen] Used as a dynamic import -// eslint-disable-next-line no-restricted-exports -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/nextjs/pages/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/nextjs/pages/supabasejs/content.tsx deleted file mode 100644 index cd081ed13a4..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/nextjs/pages/supabasejs/content.tsx +++ /dev/null @@ -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 ( -
- {todos.map((todo) => ( -
  • {todo}
  • - ))} -
    - ) -} -export default Page - -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/nuxt/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/nuxt/supabasejs/content.tsx deleted file mode 100644 index d5a7d97bb69..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/nuxt/supabasejs/content.tsx +++ /dev/null @@ -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: ` - - - -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/prisma/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/prisma/content.tsx deleted file mode 100644 index 1a4dcfd8244..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/prisma/content.tsx +++ /dev/null @@ -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 -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/react/create-react-app/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/react/create-react-app/supabasejs/content.tsx deleted file mode 100644 index 987f67af318..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/react/create-react-app/supabasejs/content.tsx +++ /dev/null @@ -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 ( -
    - {todos.map((todo) => ( -
  • {todo}
  • - ))} -
    - ) -} -export default Page -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/react/vite/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/react/vite/supabasejs/content.tsx deleted file mode 100644 index dbaf786b5d2..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/react/vite/supabasejs/content.tsx +++ /dev/null @@ -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 ( -
    - {todos.map((todo) => ( -
  • {todo}
  • - ))} -
    - ) -} -export default Page -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/refine/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/refine/supabasejs/content.tsx deleted file mode 100644 index 8e0932c6fd9..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/refine/supabasejs/content.tsx +++ /dev/null @@ -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 ( - - - - - } - /> - - } /> - } /> - } /> - } /> - - - - - - - - - ); -} - -export default App; -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/remix/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/remix/supabasejs/content.tsx deleted file mode 100644 index b0eae7a6810..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/remix/supabasejs/content.tsx +++ /dev/null @@ -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 ( - <> -
      - {loaderData.todos?.map((todo) => ( -
    • {todo.name}
    • - ))} -
    - - ); -} - -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/solidjs/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/solidjs/supabasejs/content.tsx deleted file mode 100644 index 301aeba29db..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/solidjs/supabasejs/content.tsx +++ /dev/null @@ -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 ( -
      - {(country) =>
    • {todo.name}
    • }
      -
    - ); -} - -export default App; -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx deleted file mode 100644 index ec404cfc90d..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx +++ /dev/null @@ -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 ( -
    - -
    - ) - } - - return ( -
    - - {connectionString} - - -
    - ) -} - -export default DirectConnectionContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-files/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-files/content.tsx deleted file mode 100644 index e9fd54851e7..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-files/content.tsx +++ /dev/null @@ -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 ( -
    - -
    - ) - } - - if (!config?.files.length) { - return null - } - - return ( -
    - - - {(config.postCommands ?? []).map((command) => ( -
    -

    {command.label}

    - - {command.command} - -
    - ))} -
    - ) -} - -export default DirectFilesContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-install/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-install/content.tsx deleted file mode 100644 index e66e2a7359e..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-install/content.tsx +++ /dev/null @@ -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 ( -
    - {exampleInstallCommands.map((cmd) => ( - - {cmd} - - ))} -
    - ) -} - -export default DirectInstallContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/install/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/install/content.tsx deleted file mode 100644 index 76c94087ff6..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/install/content.tsx +++ /dev/null @@ -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 ( -
    -
    - {installCommand} -
    -
    - -
    -
    - ) -} - -export default InstallContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/claude-code/add-server/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/claude-code/add-server/content.tsx deleted file mode 100644 index 00d1d68013e..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/claude-code/add-server/content.tsx +++ /dev/null @@ -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 ( - - ) -} - -export default ClaudeAddServerContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/claude-code/authenticate/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/claude-code/authenticate/content.tsx deleted file mode 100644 index 298140ef247..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/claude-code/authenticate/content.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { CodeBlock } from 'ui' - -import type { StepContentProps } from '../../../../../Connect.types' - -function ClaudeAuthenticateContent(_props: StepContentProps) { - return ( -
    - -

    - Select the supabase{' '} - server, then Authenticate to begin the flow. -

    -
    - ) -} - -export default ClaudeAuthenticateContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/add-server/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/add-server/content.tsx deleted file mode 100644 index 4a6ade248eb..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/add-server/content.tsx +++ /dev/null @@ -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 ( - - ) -} - -export default CodexAddServerContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/authenticate/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/authenticate/content.tsx deleted file mode 100644 index 979eb58049f..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/authenticate/content.tsx +++ /dev/null @@ -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 ( - - ) -} - -export default CodexAuthenticateContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/enable-remote/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/enable-remote/content.tsx deleted file mode 100644 index ee6e8bd59ef..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/enable-remote/content.tsx +++ /dev/null @@ -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 ( - - ) -} - -export default CodexEnableRemoteContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/verify/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/verify/content.tsx deleted file mode 100644 index 2f2d55e6ab7..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/verify/content.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { StepContentProps } from '../../../../../Connect.types' - -function CodexVerifyContent(_props: StepContentProps) { - return ( -
    -

    - Run /mcp inside Codex to - verify authentication. -

    -
    - ) -} - -export default CodexVerifyContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/cursor/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/cursor/content.tsx deleted file mode 100644 index c6acbea846e..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/cursor/content.tsx +++ /dev/null @@ -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 ( -
    - - -
    - ) - } - - return -} - -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 ( - - ) -} - -export default McpCursorContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/orm-install/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/orm-install/content.tsx deleted file mode 100644 index a07c6b404a5..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/orm-install/content.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { CodeBlock } from 'ui' - -import type { StepContentProps } from '../../../Connect.types' - -const ORM_INSTALL_COMMANDS: Record = { - 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 ( -
    - {commands.map((cmd, index) => ( - - {cmd} - - ))} -
    - ) -} - -export default OrmInstallContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/command/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/command/content.tsx deleted file mode 100644 index c8c15e50f90..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/command/content.tsx +++ /dev/null @@ -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 ( -
    -
    - {command} -
    -
    - -
    -
    - ) -} - -export default ShadcnCommandContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/explore/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/explore/content.tsx deleted file mode 100644 index ee3d7210728..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/explore/content.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { ExternalLink } from 'lucide-react' -import { Button } from 'ui' - -import type { StepContentProps } from '../../../../Connect.types' - -function ShadcnExploreContent(_props: StepContentProps) { - return ( - - ) -} - -export default ShadcnExploreContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx deleted file mode 100644 index 2a0bdfc8f83..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx +++ /dev/null @@ -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 ( -
    -
    - {SKILLS_COMMAND} -
    -
    - -
    -
    - ) -} - -export default SkillsInstallContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/sveltekit/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/sveltekit/supabasejs/content.tsx deleted file mode 100644 index d64fa5d93b7..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/sveltekit/supabasejs/content.tsx +++ /dev/null @@ -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: ` - - -
      - {#each data.countries as country} -
    • {country.name}
    • - {/each} -
    -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/swift/supabaseswift/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/swift/supabaseswift/content.tsx deleted file mode 100644 index b2d2b1fd3f5..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/swift/supabaseswift/content.tsx +++ /dev/null @@ -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 ?? ''}" -) - `, - }, - { - 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 -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/tanstack/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/tanstack/supabasejs/content.tsx deleted file mode 100644 index d1a5ac3e306..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/tanstack/supabasejs/content.tsx +++ /dev/null @@ -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 ( -
      - {todos?.map((todo) => ( -
    • {todo.name}
    • - ))} -
    - ) -} -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/vuejs/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/vuejs/supabasejs/content.tsx deleted file mode 100644 index 11436e665c8..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/vuejs/supabasejs/content.tsx +++ /dev/null @@ -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: ` - - - -`, - }, - ] - - return -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts b/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts deleted file mode 100644 index 6741601c7f6..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts +++ /dev/null @@ -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') - }) - }) -}) diff --git a/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts b/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts deleted file mode 100644 index 95d791815fb..00000000000 --- a/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts +++ /dev/null @@ -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): UseConnectStateReturn { - const [state, setState] = useState(() => { - 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, - } -} diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx index d9a2b644c76..ce8271e5d82 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx @@ -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, '*') diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/FrameworkSelector.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted/FrameworkSelector.tsx index fa1cebec400..4f0fc2fe0ad 100644 --- a/apps/studio/components/interfaces/HomeNew/GettingStarted/FrameworkSelector.tsx +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/FrameworkSelector.tsx @@ -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