| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702 |
- // @ts-nocheck
- 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 { useEffect, useState } from 'react'
- import { useForm } 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 {
- GitHubRepositoryField,
- useGitHubRepositoryOptions,
- } from '@/components/interfaces/Settings/Integrations/GithubIntegration/GitHubRepositoryField'
- 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 {
- gitHubAuthorization,
- githubRepos,
- hasPartialResponseDueToSSO,
- isLoading: isLoadingRepositoryOptions,
- refetch: refetchRepositoryOptions,
- } = useGitHubRepositoryOptions()
- 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 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),
- brivenDirectory: z.string().default('.'),
- brivenChangesOnly: 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 as any),
- mode: 'onSubmit',
- reValidateMode: 'onBlur',
- defaultValues: {
- repositoryId: connection?.repository.id.toString() || '',
- enableProductionSync: true,
- branchName: 'main',
- new_branch_per_pr: true,
- brivenDirectory: '.',
- brivenChangesOnly: true,
- branchLimit: '3',
- },
- })
- const enableProductionSync = githubSettingsForm.watch('enableProductionSync')
- const newBranchPerPr = githubSettingsForm.watch('new_branch_per_pr')
- const currentRepositoryId = githubSettingsForm.watch('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
- createConnection({
- organizationId: selectedOrganization.id,
- connection: {
- installation_id: selectedRepo.installation_id,
- project_ref: selectedProject.ref,
- repository_id: Number(selectedRepo.id),
- workdir: data.brivenDirectory,
- briven_changes_only: data.brivenChangesOnly,
- 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.brivenDirectory,
- briven_changes_only: data.brivenChangesOnly,
- 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,
- brivenDirectory: '.',
- brivenChangesOnly: 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,
- brivenDirectory: connection.workdir || '',
- brivenChangesOnly: connection.briven_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])
- const isLoading =
- isLoadingEntitlements ||
- isCreatingConnection ||
- isUpdatingConnection ||
- isDeletingConnection ||
- isLoadingRepositoryOptions
- return (
- <>
- <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={
- connection
- ? 'Change the connected repository'
- : 'Select the repository to connect to your project'
- }
- disabled={
- (!connection && !canCreateGitHubConnection) ||
- (connection && !canUpdateGitHubConnection)
- }
- 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>
- {gitHubAuthorization !== null && !!currentRepositoryId && (
- <motion.div
- initial={{ opacity: 0, y: -16 }}
- animate={{ opacity: 1, y: 0 }}
- exit={{ opacity: 0, y: -16 }}
- >
- <CardContent>
- <FormField
- control={githubSettingsForm.control}
- name="brivenDirectory"
- 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">briven/</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={!canUpdateGitHubConnection}
- />
- </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="Deploy changes to production on push including PR merges"
- >
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={!canUpdateGitHubConnection}
- />
- </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={!canUpdateGitHubConnection || !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
- checked={!hasAccessToBranching ? false : field.value}
- onCheckedChange={field.onChange}
- disabled={!hasAccessToBranching || !canCreateGitHubConnection}
- />
- </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
- }
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- <FormField
- control={githubSettingsForm.control}
- name="brivenChangesOnly"
- render={({ field }) => (
- <FormItemLayout
- layout="flex-row-reverse"
- label="Briven changes only"
- description="Only create branches when Briven files change"
- >
- <FormControl>
- <Switch
- checked={!hasAccessToBranching ? false : field.value}
- onCheckedChange={(val) => field.onChange(val)}
- disabled={
- !hasAccessToBranching ||
- !newBranchPerPr ||
- !canUpdateGitHubConnection
- }
- />
- </FormControl>
- </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>
- <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 Briven 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 Briven branches that are connected to the old
- repository will no longer be synced.
- </p>
- </div>
- </ConfirmationModal>
- </>
- )
- }
|