Files
supabase/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx
T
Danny White b37272cdd7 chore(studio + docs) improve project integrations (#45200)
## What kind of change does this PR introduce?

Docs update and UI copy improvement.

## What is the current behavior?

The GitHub integration working directory helper uses an inline tooltip
for examples. In the form layout, the tooltip alignment and inline code
wrapping can make the helper harder to scan.

The integration settings pages also mix markdown-rendered copy with JSX
copy, duplicate the integration cover image helper, and use slightly
different heading, spacing, and button copy patterns across GitHub,
Vercel, and AWS PrivateLink.

## What is the new behavior?

The GitHub working directory field now keeps the helper copy concise and
links to a dedicated docs section with examples for repository root and
nested `supabase/` directories.

The integration settings sections now use a more consistent JSX
structure and copy treatment across GitHub, Vercel, and AWS PrivateLink,
including shared cover imagery and matching explanatory section
hierarchy.

| Before | After |
| --- | --- |
| <img width="1838" height="696" alt="CleanShot 2026-04-24 at 14 35
32@2x"
src="https://github.com/user-attachments/assets/f9843018-fbf5-4c18-a0c3-1842b665f3fd"
/> | <img width="1842" height="700" alt="CleanShot 2026-04-24 at 14 30
21@2x-64E8726E-0B89-4C5A-8A47-CE1AA24F3D1A"
src="https://github.com/user-attachments/assets/bb017464-2b43-4dfe-94b8-a586aedea318"
/> |
| <img width="1884" height="1432" alt="CleanShot 2026-04-24 at 14 40
19@2x"
src="https://github.com/user-attachments/assets/b34e4573-cd7c-4588-83a0-0e1941019552"
/> | <img width="1844" height="1424" alt="CleanShot 2026-04-24 at 14 39
02@2x"
src="https://github.com/user-attachments/assets/4f82b01c-cb27-4f01-b2da-76b7f6e298a5"
/> |


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

* **Documentation**
* Clarified GitHub integration setup: provide a Working directory value,
how to compute it from the repo root (including "." for root), examples,
and updated “initialize” spelling.

* **Refactor**
* Simplified integration form descriptions (tooltips → inline text +
Learn more link).
* Reworked GitHub and Vercel integration content to use consistent
headings, layout, and shared image handling.
  * Reorganized integration settings rendering for clearer UI states.

* **Style**
  * Minor copy, casing, and button text/spacing adjustments.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-24 15:41:59 +08:00

859 lines
35 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronDown, Loader2, PlusIcon, RefreshCw } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Card,
CardContent,
CardFooter,
cn,
Command_Shadcn_,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
CommandSeparator_Shadcn_,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Switch,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'
import { InlineLink } from '@/components/ui/InlineLink'
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
import { useBranchCreateMutation } from '@/data/branches/branch-create-mutation'
import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation'
import { useBranchesQuery } from '@/data/branches/branches-query'
import { useGitHubAuthorizationQuery } from '@/data/integrations/github-authorization-query'
import { useCheckGithubBranchValidity } from '@/data/integrations/github-branch-check-query'
import { useGitHubConnectionCreateMutation } from '@/data/integrations/github-connection-create-mutation'
import { useGitHubConnectionDeleteMutation } from '@/data/integrations/github-connection-delete-mutation'
import { useGitHubConnectionUpdateMutation } from '@/data/integrations/github-connection-update-mutation'
import { useGitHubRepositoriesQuery } from '@/data/integrations/github-repositories-query'
import type { GitHubConnection } from '@/data/integrations/integrations.types'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
import { openInstallGitHubIntegrationWindow } from '@/lib/github'
import { EMPTY_ARR } from '@/lib/void'
const GITHUB_ICON = (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98 96" className="w-6">
<title>GitHub icon</title>
<path
fill="#ffffff"
fillRule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
clipRule="evenodd"
/>
</svg>
)
interface GitHubIntegrationConnectionFormProps {
connection?: GitHubConnection
}
export const GitHubIntegrationConnectionForm = ({
connection,
}: GitHubIntegrationConnectionFormProps) => {
const { data: selectedProject } = useSelectedProjectQuery()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const [isConfirmingBranchChange, setIsConfirmingBranchChange] = useState(false)
const [isConfirmingRepoChange, setIsConfirmingRepoChange] = useState(false)
const [repoComboBoxOpen, setRepoComboboxOpen] = useState(false)
const isParentProject = !selectedProject?.parent_project_ref
const { hasAccess: hasAccessToGitHubIntegration, isLoading: isLoadingEntitlements } =
useCheckEntitlements('integrations.github_connections')
const { hasAccess: hasAccessToBranching } = useCheckEntitlements('branching_limit')
const { can: canUpdateGitHubConnection } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'integrations.github_connections'
)
const { can: canCreateGitHubConnection } = useAsyncCheckPermissions(
PermissionAction.CREATE,
'integrations.github_connections'
)
const { data: gitHubAuthorization, refetch: refetchGitHubAuthorization } =
useGitHubAuthorizationQuery()
const {
data: githubReposData,
isPending: isLoadingGitHubRepos,
refetch: refetchGitHubRepositories,
} = useGitHubRepositoriesQuery({
enabled: Boolean(gitHubAuthorization),
})
const refetchGitHubAuthorizationAndRepositories = () => {
setTimeout(() => {
refetchGitHubAuthorization()
refetchGitHubRepositories()
}, 2000) // 2 second to delay to let github authorization and repositories to be updated
}
const { mutate: updateBranch } = useBranchUpdateMutation({
onSuccess: () => {
toast.success('Production branch settings successfully updated')
},
})
const { mutate: createBranch } = useBranchCreateMutation({
onSuccess: () => {
toast.success('Production branch settings successfully updated')
},
onError: (error) => {
console.error('Failed to enable branching:', error)
},
})
const { data: existingBranches } = useBranchesQuery(
{ projectRef: selectedProject?.ref },
{ enabled: !!selectedProject?.ref }
)
const { mutateAsync: checkGithubBranchValidity, isPending: isCheckingBranch } =
useCheckGithubBranchValidity({ onError: () => {} })
const { mutate: createConnection, isPending: isCreatingConnection } =
useGitHubConnectionCreateMutation({
onSuccess: () => {
toast.success('GitHub integration successfully updated')
},
onError: (error) => {
// Don't show error toast when connection already exists - the branch
// settings update will still proceed and show its own success toast
if (!error.message?.includes('already exists')) {
toast.error(`Failed to create GitHub connection: ${error.message}`)
}
},
})
const { mutateAsync: deleteConnection, isPending: isDeletingConnection } =
useGitHubConnectionDeleteMutation({
onSuccess: () => {
toast.success('Successfully removed GitHub integration')
},
})
const { mutate: updateConnectionSettings, isPending: isUpdatingConnection } =
useGitHubConnectionUpdateMutation()
const githubRepos = useMemo(
() =>
githubReposData?.repositories?.map((repo) => ({
id: repo.id.toString(),
name: repo.name,
installation_id: repo.installation_id,
default_branch: repo.default_branch || 'main',
})) ?? EMPTY_ARR,
[githubReposData]
)
const hasPartialResponseDueToSSO = githubReposData?.partial_response_due_to_sso ?? false
const prodBranch = existingBranches?.find((branch) => branch.is_default)
// Combined GitHub Settings Form
const GitHubSettingsSchema = z
.object({
repositoryId: z.string().min(1, 'Please select a repository'),
enableProductionSync: z.boolean().default(true),
branchName: z.string().default('main'),
new_branch_per_pr: z.boolean().default(true),
supabaseDirectory: z.string().default('.'),
supabaseChangesOnly: z.boolean().default(true),
branchLimit: z.string().default('50'),
})
.superRefine(async (val, ctx) => {
if (val.enableProductionSync && val.branchName && val.branchName.length > 0) {
const repositoryId = val.repositoryId || connection?.repository.id.toString()
if (repositoryId) {
try {
await checkGithubBranchValidity({
repositoryId: Number(repositoryId),
branchName: val.branchName,
})
} catch {
const selectedRepo = githubRepos.find((repo) => repo.id === repositoryId)
const repoName =
selectedRepo?.name || connection?.repository.name || 'selected repository'
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Branch "${val.branchName}" not found in ${repoName}`,
path: ['branchName'],
})
}
}
}
})
const githubSettingsForm = useForm<z.infer<typeof GitHubSettingsSchema>>({
resolver: zodResolver(GitHubSettingsSchema),
mode: 'onSubmit',
reValidateMode: 'onBlur',
defaultValues: {
repositoryId: connection?.repository.id.toString() || '',
enableProductionSync: true,
branchName: 'main',
new_branch_per_pr: true,
supabaseDirectory: '.',
supabaseChangesOnly: true,
branchLimit: '3',
},
})
const enableProductionSync = githubSettingsForm.watch('enableProductionSync')
const newBranchPerPr = githubSettingsForm.watch('new_branch_per_pr')
const currentRepositoryId = githubSettingsForm.watch('repositoryId')
// Calculate selected repository based on current form value
const selectedRepository = githubRepos.find((repo) => repo.id === currentRepositoryId)
const handleCreateOrUpdateConnection = async (data: z.infer<typeof GitHubSettingsSchema>) => {
if (!selectedProject?.ref || !selectedOrganization?.id) return
try {
if (connection) {
// Check if repository is being changed
if (connection.repository.id.toString() !== data.repositoryId) {
setIsConfirmingRepoChange(true)
return
}
// Update existing connection
await handleUpdateConnection(data, connection)
} else {
// Create new connection
const selectedRepo = githubRepos.find((repo) => repo.id === data.repositoryId)
if (!selectedRepo) {
toast.error('Please select a repository')
return
}
await handleCreateConnection(data, selectedRepo)
}
} catch (error) {
console.error('Error managing connection:', error)
}
}
const handleCreateConnection = async (
data: z.infer<typeof GitHubSettingsSchema>,
selectedRepo: { id: string; installation_id: number }
) => {
if (!selectedProject?.ref || !selectedOrganization?.id) return
createConnection({
organizationId: selectedOrganization.id,
connection: {
installation_id: selectedRepo.installation_id,
project_ref: selectedProject.ref,
repository_id: Number(selectedRepo.id),
workdir: data.supabaseDirectory,
supabase_changes_only: data.supabaseChangesOnly,
branch_limit: Number(data.branchLimit),
new_branch_per_pr: data.new_branch_per_pr,
},
})
if (!prodBranch) {
createBranch({
projectRef: selectedProject.ref,
branchName: 'main',
gitBranch: data.branchName,
is_default: true,
})
} else {
updateBranch({
branchRef: prodBranch.project_ref,
projectRef: selectedProject.ref,
gitBranch: data.branchName,
})
}
}
const handleUpdateConnection = async (
data: z.infer<typeof GitHubSettingsSchema>,
currentConnection: GitHubConnection
) => {
if (!selectedProject?.ref || !selectedOrganization?.id) return
const originalBranchName = prodBranch?.git_branch
if (originalBranchName && data.branchName !== originalBranchName && data.enableProductionSync) {
setIsConfirmingBranchChange(true)
return
}
await executeUpdate(data, currentConnection)
}
const executeUpdate = async (
data: z.infer<typeof GitHubSettingsSchema>,
currentConnection: GitHubConnection
) => {
if (!selectedProject?.ref || !selectedOrganization?.id) return
updateConnectionSettings({
connectionId: currentConnection.id,
organizationId: selectedOrganization.id,
connection: {
workdir: data.supabaseDirectory,
supabase_changes_only: data.supabaseChangesOnly,
branch_limit: Number(data.branchLimit),
new_branch_per_pr: data.new_branch_per_pr,
},
})
if (prodBranch) {
updateBranch({
branchRef: prodBranch.project_ref,
projectRef: selectedProject.ref,
gitBranch: data.enableProductionSync ? data.branchName : '',
branchName: data.branchName || 'main',
})
} else {
// if for some reason, the project doesn't have a default branch yet, create it.
createBranch({
projectRef: selectedProject.ref,
gitBranch: data.enableProductionSync ? data.branchName : '',
branchName: data.branchName || 'main',
is_default: true,
})
}
setIsConfirmingBranchChange(false)
}
const onConfirmBranchChange = async () => {
if (connection) {
await executeUpdate(githubSettingsForm.getValues(), connection)
}
}
const handleRemoveIntegration = async () => {
if (!connection || !selectedOrganization?.id) return
try {
await deleteConnection({
organizationId: selectedOrganization.id,
connectionId: connection.id,
})
githubSettingsForm.reset({
repositoryId: '',
enableProductionSync: true,
branchName: 'main',
new_branch_per_pr: true,
supabaseDirectory: '.',
supabaseChangesOnly: true,
branchLimit: '3',
})
} catch (error) {
console.error('Error removing integration:', error)
toast.error('Failed to remove integration')
}
}
const onConfirmRepoChange = async () => {
const data = githubSettingsForm.getValues()
const selectedRepo = githubRepos.find((repo) => repo.id === data.repositoryId)
if (!selectedRepo || !connection || !selectedOrganization?.id) return
try {
await deleteConnection({
organizationId: selectedOrganization.id,
connectionId: connection.id,
})
await handleCreateConnection(data, selectedRepo)
setIsConfirmingRepoChange(false)
} catch (error) {
console.error('Error changing repository:', error)
toast.error('Failed to change repository')
}
}
useEffect(() => {
if (connection) {
const hasGitBranch = Boolean(prodBranch?.git_branch?.trim())
githubSettingsForm.reset({
repositoryId: connection.repository.id.toString(),
enableProductionSync: hasGitBranch,
branchName: prodBranch?.git_branch || 'main',
new_branch_per_pr: connection.new_branch_per_pr,
supabaseDirectory: connection.workdir || '',
supabaseChangesOnly: connection.supabase_changes_only,
branchLimit: String(connection.branch_limit),
})
}
}, [connection, prodBranch, githubSettingsForm])
// Handle clearing branch name when production sync is disabled
useEffect(() => {
if (!enableProductionSync) {
githubSettingsForm.setValue('branchName', '')
} else if (enableProductionSync && !githubSettingsForm.getValues().branchName) {
githubSettingsForm.setValue('branchName', 'main')
}
}, [enableProductionSync, githubSettingsForm])
// Show authorization prompt if not authorized
if (gitHubAuthorization === null) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<p className="text-sm text-center">Authorize with GitHub</p>
<p className="text-sm text-center text-foreground-light mb-4">
Connect your GitHub account to access and select repositories for integration.
</p>
<Button
onClick={() => {
openInstallGitHubIntegrationWindow(
'authorize',
refetchGitHubAuthorizationAndRepositories
)
}}
>
Authorize GitHub
</Button>
</CardContent>
</Card>
)
}
const isLoading =
isLoadingEntitlements || isCreatingConnection || isUpdatingConnection || isDeletingConnection
return (
<>
<Form_Shadcn_ {...githubSettingsForm}>
<form
onSubmit={githubSettingsForm.handleSubmit(handleCreateOrUpdateConnection)}
className={cn(!isParentProject && 'opacity-25 pointer-events-none')}
>
<Card>
<CardContent className="space-y-6">
{/* Repository Selection */}
<FormField_Shadcn_
control={githubSettingsForm.control}
name="repositoryId"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="GitHub repository"
description={
connection
? 'Change the connected repository'
: 'Select the repository to connect to your project'
}
>
<Popover_Shadcn_ open={repoComboBoxOpen} onOpenChange={setRepoComboboxOpen}>
<PopoverTrigger_Shadcn_ asChild>
<FormControl_Shadcn_>
<Button
type="default"
className="justify-start h-[34px] w-full"
disabled={isLoadingGitHubRepos}
loading={isLoadingGitHubRepos}
icon={
<div className="bg-black shadow rounded p-1 w-6 h-6 flex justify-center items-center">
{GITHUB_ICON}
</div>
}
iconRight={
<span className="grow flex justify-end">
<ChevronDown />
</span>
}
>
{selectedRepository || connection
? selectedRepository?.name || connection?.repository.name
: 'Choose GitHub repository'}
</Button>
</FormControl_Shadcn_>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0 w-80" side="bottom" align="start">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Search repositories..." />
<CommandList_Shadcn_ className="!max-h-[200px]">
<CommandEmpty_Shadcn_>No repositories found.</CommandEmpty_Shadcn_>
{githubRepos.length > 0 ? (
<CommandGroup_Shadcn_>
{githubRepos.map((repo, i) => (
<CommandItem_Shadcn_
key={repo.id}
value={`${repo.name.replaceAll('"', '')}-${i}`}
className="flex gap-2 items-center"
onSelect={() => {
field.onChange(repo.id)
setRepoComboboxOpen(false)
githubSettingsForm.setValue(
'branchName',
repo.default_branch || 'main'
)
}}
>
<div className="bg-black shadow rounded p-1 w-5 h-5 flex justify-center items-center">
{GITHUB_ICON}
</div>
<span className="truncate" title={repo.name}>
{repo.name}
</span>
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
) : null}
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_
className="flex gap-2 items-center cursor-pointer"
onSelect={() =>
openInstallGitHubIntegrationWindow(
'install',
refetchGitHubAuthorizationAndRepositories
)
}
>
<PlusIcon size={16} />
Add GitHub Repositories
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
{hasPartialResponseDueToSSO && (
<>
<CommandSeparator_Shadcn_ />
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_
className="flex gap-2 items-start cursor-pointer"
onSelect={() => {
openInstallGitHubIntegrationWindow(
'authorize',
refetchGitHubAuthorizationAndRepositories
)
}}
>
<RefreshCw size={16} className="mt-0.5 shrink-0" />
<div className="text-xs text-foreground-light">
Re-authorize GitHub with SSO to show all repositories
</div>
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
</>
)}
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<AnimatePresence>
{!!currentRepositoryId && (
<motion.div
initial={{ opacity: 0, y: -16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
>
<CardContent>
<FormField_Shadcn_
control={githubSettingsForm.control}
name="supabaseDirectory"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Working directory"
description={
<>
Relative path to the directory containing your{' '}
<code className="text-code-inline whitespace-nowrap">supabase/</code>{' '}
folder.{' '}
<InlineLink
href={`${DOCS_URL}/guides/deployment/branching/github-integration#set-the-working-directory`}
>
Learn more
</InlineLink>
</>
}
>
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
placeholder="."
autoComplete="off"
disabled={!canUpdateGitHubConnection}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
{/* Production Branch Sync Section */}
<div className="space-y-4">
<FormField_Shadcn_
control={githubSettingsForm.control}
name="enableProductionSync"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Deploy to production"
description="Deploy changes to production on push including PR merges"
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateGitHubConnection}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<div
className={cn(
'space-y-4 pl-6 border-l',
!enableProductionSync && 'opacity-25 pointer-events-none'
)}
>
<FormField_Shadcn_
control={githubSettingsForm.control}
name="branchName"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Production branch name"
description="The GitHub branch to sync with your production database (e.g., main, master)"
>
<div className="relative w-full">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
autoComplete="off"
disabled={!canUpdateGitHubConnection || !enableProductionSync}
/>
</FormControl_Shadcn_>
<div className="absolute top-2.5 right-3 flex items-center gap-2">
{isCheckingBranch && (
<Loader2 size={14} className="animate-spin" />
)}
</div>
</div>
</FormItemLayout>
)}
/>
</div>
</div>
</CardContent>
<CardContent>
{hasAccessToBranching ? (
<Admonition type="warning" title="Branching and billing" className="mb-4">
Branching Compute is not covered by your organization&apos;s Spend Cap.
Costs should be closely monitored, as they may be incurred.{' '}
<InlineLink
href={`${DOCS_URL}/guides/platform/cost-control#usage-items-not-covered-by-the-spend-cap`}
>
Learn more
</InlineLink>
</Admonition>
) : (
<UpgradeToPro
className="mb-4"
layout="vertical"
source="projectIntegrations"
featureProposition="automatically create preview branches from pull requests"
primaryText="Branching with GitHub integration"
secondaryText="Upgrade to the Pro Plan to enable branching and automatically create preview branches for every pull request"
docsUrl={`${DOCS_URL}/guides/deployment/branching`}
/>
)}
{/* Automatic Branching Section */}
<div className="space-y-4">
<FormField_Shadcn_
disabled={!hasAccessToBranching}
control={githubSettingsForm.control}
name="new_branch_per_pr"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Automatic branching"
className={cn(!hasAccessToBranching && 'opacity-25')}
description="Create preview branches for every pull request"
>
<FormControl_Shadcn_>
<Switch
checked={!hasAccessToBranching ? false : field.value}
onCheckedChange={field.onChange}
disabled={!hasAccessToBranching || !canCreateGitHubConnection}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<div
className={cn(
'space-y-4 pl-6 border-l',
(!hasAccessToBranching || !newBranchPerPr) &&
'opacity-25 pointer-events-none'
)}
>
<FormField_Shadcn_
control={githubSettingsForm.control}
name="branchLimit"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Branch limit"
description="Maximum number of preview branches"
>
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
type="number"
autoComplete="off"
value={!hasAccessToBranching ? 0 : field.value}
disabled={
!hasAccessToBranching ||
!newBranchPerPr ||
!canUpdateGitHubConnection
}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
control={githubSettingsForm.control}
name="supabaseChangesOnly"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Supabase changes only"
description="Only create branches when Supabase files change"
>
<FormControl_Shadcn_>
<Switch
checked={!hasAccessToBranching ? false : field.value}
onCheckedChange={(val) => field.onChange(val)}
disabled={
!hasAccessToBranching ||
!newBranchPerPr ||
!canUpdateGitHubConnection
}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between items-center">
<div>
{connection && (
<Button
type="outline"
onClick={handleRemoveIntegration}
disabled={isDeletingConnection || isCheckingBranch}
loading={isDeletingConnection}
>
Disable integration
</Button>
)}
</div>
<div className="flex space-x-2">
{githubSettingsForm.formState.isDirty && (
<Button
type="default"
onClick={() => githubSettingsForm.reset()}
disabled={!canUpdateGitHubConnection || isCheckingBranch}
>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={
!hasAccessToGitHubIntegration ||
(!connection && !canCreateGitHubConnection) ||
(connection && !canUpdateGitHubConnection) ||
isCheckingBranch ||
isLoading ||
(!connection && !githubSettingsForm.getValues().repositoryId) ||
(connection && !githubSettingsForm.formState.isDirty)
}
loading={isLoading}
>
{connection ? 'Save changes' : 'Enable integration'}
</Button>
</div>
</CardFooter>
</motion.div>
)}
</AnimatePresence>
</Card>
</form>
</Form_Shadcn_>
<ConfirmationModal
variant="warning"
visible={isConfirmingBranchChange}
title="Changing production git branch"
confirmLabel="Confirm"
size="medium"
onCancel={() => setIsConfirmingBranchChange(false)}
onConfirm={onConfirmBranchChange}
loading={isUpdatingConnection}
>
<p className="text-sm text-foreground-light">
Open pull requests will only update your Supabase project on merge if the git base branch
matches this new production git branch.
</p>
</ConfirmationModal>
<ConfirmationModal
variant="warning"
visible={isConfirmingRepoChange}
title="Changing GitHub repository"
confirmLabel="Change repository"
size="medium"
onCancel={() => setIsConfirmingRepoChange(false)}
onConfirm={onConfirmRepoChange}
loading={isLoading}
>
<div className="space-y-3">
<p className="text-sm text-foreground-light">
This will disconnect your current repository and create a new connection with the
selected repository. All existing Supabase branches that are connected to the old
repository will no longer be synced.
</p>
</div>
</ConfirmationModal>
</>
)
}