mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
## Context A project could already have a GitHub integration set up by another member, which will show up in the branch management side navigation as such: <img width="259" height="255" alt="image" src="https://github.com/user-attachments/assets/8bc12ef2-5f25-4245-9c89-4563b08a42a8" /> However if the user hasn't authenticated with GitHub, the project settings -> integrations page will obfuscate the integration details with a "Connect GitHub" CTA, which is a confusing UX as it hints that there's no integration set up + conflicts with what's on the branch management page <img width="768" height="318" alt="image" src="https://github.com/user-attachments/assets/a68409c0-f2db-407d-9bac-c109a0d14214" /> ## Changes involved Opting to show the integration details if there's one set up irregardless of whether the user has authenticated with GH, but we'll require the user to complete the authentication to manage the integration settings as such (all input fields will also be disabled until the auth is complete): <img width="1031" height="511" alt="image" src="https://github.com/user-attachments/assets/6fad387b-6e76-475e-b4e6-76972d8f76b6" /> Note that you can still disable the integration nonetheless irregardless of the GH connection status Also fixes the "Remove connection" CTA from Account Preferences - we were invalidating the GH authorization query cache but the GH authorization requests normally fails with a 404 if there's no auth set up, so the query cache would remain stale. Opting to clear the query cache instead of invalidating ## To test - [ ] Set up a GH integration for your project under project settings -> integrations - [ ] Head to account preferences to remove your GH connection - [ ] Go back to the project settings -> integrations page, should still see the GH integration details, just unable to edit - [ ] Connect with GH, and you should be able to edit the integration details thereafter
754 lines
30 KiB
TypeScript
754 lines
30 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { AnimatePresence, motion } from 'framer-motion'
|
|
import { Loader2 } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import { useForm, useWatch } from 'react-hook-form'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Button,
|
|
Card,
|
|
CardContent,
|
|
CardFooter,
|
|
cn,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
Input,
|
|
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 { ConnectGitHubButton } from './ConnectGitHubButton'
|
|
import {
|
|
GitHubRepositoryField,
|
|
useGitHubRepositoryOptions,
|
|
} from '@/components/interfaces/Settings/Integrations/GithubIntegration/GitHubRepositoryField'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
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 { 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 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'
|
|
|
|
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 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 canManageGitHubConnection = canCreateGitHubConnection && canUpdateGitHubConnection
|
|
|
|
const {
|
|
gitHubAuthorization,
|
|
githubRepos,
|
|
hasPartialResponseDueToSSO,
|
|
isLoading: isLoadingRepositoryOptions,
|
|
refetch: refetchRepositoryOptions,
|
|
} = useGitHubRepositoryOptions()
|
|
|
|
const { mutateAsync: updateBranch } = useBranchUpdateMutation({
|
|
onSuccess: () => {
|
|
toast.success('Production branch settings successfully updated')
|
|
},
|
|
})
|
|
const { mutateAsync: 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 { mutateAsync: createConnection, isPending: isCreatingConnection } =
|
|
useGitHubConnectionCreateMutation({
|
|
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('GitHub connection removed')
|
|
},
|
|
})
|
|
|
|
const { mutateAsync: updateConnectionSettings, isPending: isUpdatingConnection } =
|
|
useGitHubConnectionUpdateMutation()
|
|
|
|
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 hasGitBranch = Boolean(prodBranch?.git_branch?.trim())
|
|
const connectionValues: z.infer<typeof GitHubSettingsSchema> | undefined = connection
|
|
? {
|
|
repositoryId: connection.repository.id.toString(),
|
|
enableProductionSync: hasGitBranch,
|
|
branchName: hasGitBranch ? (prodBranch?.git_branch ?? '') : '',
|
|
new_branch_per_pr: connection.new_branch_per_pr,
|
|
supabaseDirectory: connection.workdir || '',
|
|
supabaseChangesOnly: connection.supabase_changes_only,
|
|
branchLimit: String(connection.branch_limit),
|
|
}
|
|
: undefined
|
|
|
|
const githubSettingsForm = useForm<z.infer<typeof GitHubSettingsSchema>>({
|
|
resolver: zodResolver(GitHubSettingsSchema),
|
|
mode: 'onSubmit',
|
|
reValidateMode: 'onBlur',
|
|
defaultValues: {
|
|
repositoryId: connection?.repository.id.toString() || '',
|
|
branchName: prodBranch?.git_branch || 'main',
|
|
enableProductionSync: true,
|
|
new_branch_per_pr: true,
|
|
supabaseDirectory: '.',
|
|
supabaseChangesOnly: true,
|
|
branchLimit: '3',
|
|
},
|
|
values: connectionValues,
|
|
resetOptions: { keepDirtyValues: true },
|
|
})
|
|
|
|
const enableProductionSync = useWatch({
|
|
control: githubSettingsForm.control,
|
|
name: 'enableProductionSync',
|
|
})
|
|
const newBranchPerPr = useWatch({
|
|
control: githubSettingsForm.control,
|
|
name: 'new_branch_per_pr',
|
|
})
|
|
const currentRepositoryId = useWatch({
|
|
control: githubSettingsForm.control,
|
|
name: 'repositoryId',
|
|
})
|
|
|
|
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
|
|
|
|
const connectionRequest = createConnection({
|
|
organizationId: selectedOrganization.id,
|
|
connection: {
|
|
installation_id: selectedRepo.installation_id,
|
|
project_ref: selectedProject.ref,
|
|
repository_id: Number(selectedRepo.id),
|
|
workdir: data.supabaseDirectory,
|
|
...(hasAccessToBranching && {
|
|
supabase_changes_only: data.supabaseChangesOnly,
|
|
branch_limit: Number(data.branchLimit),
|
|
new_branch_per_pr: data.new_branch_per_pr ?? false,
|
|
}),
|
|
},
|
|
})
|
|
|
|
const branchRequest = prodBranch
|
|
? updateBranch({
|
|
branchRef: prodBranch.project_ref,
|
|
projectRef: selectedProject.ref,
|
|
gitBranch: data.branchName,
|
|
})
|
|
: createBranch({
|
|
projectRef: selectedProject.ref,
|
|
branchName: 'main',
|
|
gitBranch: data.branchName,
|
|
is_default: true,
|
|
})
|
|
|
|
// Only re-baseline the form once both requests succeed, so a failure keeps the edits unsaved
|
|
const results = await Promise.allSettled([connectionRequest, branchRequest])
|
|
if (results.every((result) => result.status === 'fulfilled')) {
|
|
toast.success('GitHub connection updated')
|
|
githubSettingsForm.reset(data, { keepDirtyValues: false })
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
const connectionRequest = updateConnectionSettings({
|
|
connectionId: currentConnection.id,
|
|
organizationId: selectedOrganization.id,
|
|
connection: {
|
|
workdir: data.supabaseDirectory,
|
|
...(hasAccessToBranching && {
|
|
supabase_changes_only: data.supabaseChangesOnly,
|
|
branch_limit: Number(data.branchLimit),
|
|
new_branch_per_pr: data.new_branch_per_pr ?? false,
|
|
}),
|
|
},
|
|
})
|
|
|
|
// if for some reason, the project doesn't have a default branch yet, create it.
|
|
const branchRequest = prodBranch
|
|
? updateBranch({
|
|
branchRef: prodBranch.project_ref,
|
|
projectRef: selectedProject.ref,
|
|
gitBranch: data.enableProductionSync ? data.branchName : '',
|
|
branchName: data.branchName || 'main',
|
|
})
|
|
: createBranch({
|
|
projectRef: selectedProject.ref,
|
|
gitBranch: data.enableProductionSync ? data.branchName : '',
|
|
branchName: data.branchName || 'main',
|
|
is_default: true,
|
|
})
|
|
|
|
setIsConfirmingBranchChange(false)
|
|
|
|
// Only re-baseline the form once both requests succeed, so a failure keeps the edits unsaved
|
|
const results = await Promise.allSettled([connectionRequest, branchRequest])
|
|
if (results.every((result) => result.status === 'fulfilled')) {
|
|
githubSettingsForm.reset(data, { keepDirtyValues: 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',
|
|
},
|
|
{ keepDirtyValues: false }
|
|
)
|
|
} 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')
|
|
}
|
|
}
|
|
|
|
const handleToggleProductionSync = (isEnabled: boolean) => {
|
|
githubSettingsForm.setValue('enableProductionSync', isEnabled, { shouldDirty: true })
|
|
githubSettingsForm.setValue('branchName', isEnabled ? prodBranch?.git_branch || 'main' : '', {
|
|
shouldDirty: true,
|
|
})
|
|
}
|
|
|
|
const isLoading =
|
|
isLoadingEntitlements ||
|
|
isCreatingConnection ||
|
|
isUpdatingConnection ||
|
|
isDeletingConnection ||
|
|
(!!gitHubAuthorization && isLoadingRepositoryOptions)
|
|
|
|
const isFieldDisabled = !canUpdateGitHubConnection || !gitHubAuthorization
|
|
|
|
let repositoryDescription = 'Select the repository to connect to your project'
|
|
if (connection) {
|
|
repositoryDescription = 'Change the connected repository'
|
|
} else if (gitHubAuthorization === null) {
|
|
repositoryDescription = 'Connect GitHub to link a repository to this project'
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{!gitHubAuthorization && !!currentRepositoryId && (
|
|
<Admonition
|
|
className="mb-4"
|
|
layout="responsive"
|
|
title="Authorize GitHub to manage integration settings"
|
|
description="Required to list your repositories and change this project's connection"
|
|
actions={<ConnectGitHubButton refetch={refetchRepositoryOptions} />}
|
|
/>
|
|
)}
|
|
|
|
<Form {...githubSettingsForm}>
|
|
<form
|
|
onSubmit={githubSettingsForm.handleSubmit(handleCreateOrUpdateConnection)}
|
|
className={cn(!isParentProject && 'opacity-25 pointer-events-none')}
|
|
>
|
|
<Card>
|
|
<CardContent className="space-y-6">
|
|
<GitHubRepositoryField
|
|
form={githubSettingsForm}
|
|
name="repositoryId"
|
|
label="GitHub repository"
|
|
layout="flex-row-reverse"
|
|
description={repositoryDescription}
|
|
disabled={!gitHubAuthorization || !canManageGitHubConnection}
|
|
selectedRepositoryName={connection?.repository.name}
|
|
repositories={githubRepos}
|
|
gitHubAuthorization={gitHubAuthorization}
|
|
hasPartialResponseDueToSSO={hasPartialResponseDueToSSO}
|
|
isLoading={isLoadingRepositoryOptions}
|
|
refetch={refetchRepositoryOptions}
|
|
onRepositorySelect={(repo) => {
|
|
githubSettingsForm.setValue('branchName', repo.default_branch || 'main')
|
|
}}
|
|
/>
|
|
</CardContent>
|
|
|
|
<AnimatePresence>
|
|
{!!currentRepositoryId && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -16 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -16 }}
|
|
>
|
|
<CardContent>
|
|
<FormField
|
|
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>
|
|
<Input
|
|
{...field}
|
|
placeholder="."
|
|
autoComplete="off"
|
|
disabled={isFieldDisabled}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</CardContent>
|
|
<CardContent>
|
|
{/* Production Branch Sync Section */}
|
|
<div className="space-y-4">
|
|
<FormField
|
|
control={githubSettingsForm.control}
|
|
name="enableProductionSync"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="flex-row-reverse"
|
|
label="Deploy to production"
|
|
description="Apply changes to your production database when you merge into your configured production GitHub branch"
|
|
>
|
|
<FormControl>
|
|
<Switch
|
|
aria-label="Toggle deploy to production"
|
|
checked={field.value}
|
|
onCheckedChange={handleToggleProductionSync}
|
|
disabled={isFieldDisabled}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<div
|
|
className={cn(
|
|
'space-y-4 pl-6 border-l',
|
|
!enableProductionSync && 'opacity-25 pointer-events-none'
|
|
)}
|
|
>
|
|
<FormField
|
|
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>
|
|
<Input
|
|
{...field}
|
|
autoComplete="off"
|
|
disabled={isFieldDisabled || !enableProductionSync}
|
|
/>
|
|
</FormControl>
|
|
<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
|
|
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>
|
|
<Switch
|
|
aria-label="Toggle automatic branching"
|
|
checked={!hasAccessToBranching ? false : field.value}
|
|
onCheckedChange={field.onChange}
|
|
disabled={
|
|
!hasAccessToBranching ||
|
|
!canCreateGitHubConnection ||
|
|
isFieldDisabled
|
|
}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<div
|
|
className={cn(
|
|
'space-y-4 pl-6 border-l',
|
|
(!hasAccessToBranching || !newBranchPerPr) &&
|
|
'opacity-25 pointer-events-none'
|
|
)}
|
|
>
|
|
<FormField
|
|
control={githubSettingsForm.control}
|
|
name="branchLimit"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
layout="flex-row-reverse"
|
|
label="Branch limit"
|
|
description="Maximum number of preview branches"
|
|
>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
type="number"
|
|
autoComplete="off"
|
|
value={!hasAccessToBranching ? 0 : field.value}
|
|
disabled={
|
|
!hasAccessToBranching ||
|
|
!newBranchPerPr ||
|
|
!canUpdateGitHubConnection ||
|
|
isFieldDisabled
|
|
}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
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>
|
|
<Switch
|
|
aria-label="Toggle Supabase changes only"
|
|
checked={!hasAccessToBranching ? false : field.value}
|
|
onCheckedChange={(val) => field.onChange(val)}
|
|
disabled={
|
|
!hasAccessToBranching ||
|
|
!newBranchPerPr ||
|
|
!canUpdateGitHubConnection ||
|
|
isFieldDisabled
|
|
}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter className="flex justify-between items-center">
|
|
<div>
|
|
{connection && (
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleRemoveIntegration}
|
|
disabled={isDeletingConnection || isCheckingBranch}
|
|
loading={isDeletingConnection}
|
|
>
|
|
Disable integration
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
{githubSettingsForm.formState.isDirty && (
|
|
<Button
|
|
onClick={() =>
|
|
githubSettingsForm.reset(connectionValues, { keepDirtyValues: false })
|
|
}
|
|
disabled={!canUpdateGitHubConnection || isCheckingBranch}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
)}
|
|
<ButtonTooltip
|
|
variant="primary"
|
|
type="submit"
|
|
disabled={
|
|
!hasAccessToGitHubIntegration ||
|
|
isCheckingBranch ||
|
|
isLoading ||
|
|
!canManageGitHubConnection ||
|
|
(!connection && !githubSettingsForm.getValues().repositoryId) ||
|
|
(connection && !githubSettingsForm.formState.isDirty)
|
|
}
|
|
loading={isLoading}
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
text: !canManageGitHubConnection
|
|
? "You need additional permissions to update this project's GitHub integration settings"
|
|
: undefined,
|
|
},
|
|
}}
|
|
>
|
|
{connection ? 'Save changes' : 'Enable integration'}
|
|
</ButtonTooltip>
|
|
</div>
|
|
</CardFooter>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</Card>
|
|
</form>
|
|
</Form>
|
|
|
|
<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>
|
|
</>
|
|
)
|
|
}
|