mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
## 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 -->
859 lines
35 KiB
TypeScript
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'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>
|
|
</>
|
|
)
|
|
}
|