mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 21:59:16 +03:00
refactor
This commit is contained in:
1 parent
2b7fe2e4aa
commit
e8a6df356b
4 files changed
+659
-699
No files matched your search
@@ -266,7 +266,7 @@ export const CreateBranchModal = () => {
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Automatic branching</Label>
|
||||
<Label>Git branch</Label>
|
||||
{!gitlessBranching && (
|
||||
<Badge variant="warning" size="small">
|
||||
Required
|
||||
@@ -274,8 +274,7 @@ export const CreateBranchModal = () => {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Automatically create, sync, and merge branches in Supabase when you make
|
||||
changes to your GitHub repository.
|
||||
Keep this Supabase branch in sync with a Git branch
|
||||
</p>
|
||||
</div>
|
||||
<Button type="default" icon={<Github />} onClick={openLinkerPanel}>
|
||||
|
||||
+113
-589
@@ -1,68 +1,40 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Check, ChevronDown, DollarSign, Github, Loader2 } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useMemo } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { ForeignProject } from 'components/interfaces/Integrations/VercelGithub/ProjectLinker'
|
||||
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
|
||||
import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import ProjectLinker from 'components/interfaces/Integrations/VercelGithub/ProjectLinker'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
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 { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { useGitHubRepositoriesQuery } from 'data/integrations/github-repositories-query'
|
||||
import type { IntegrationConnectionsCreateVariables } from 'data/integrations/integrations.types'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { openInstallGitHubIntegrationWindow } from 'lib/github'
|
||||
import { EMPTY_ARR } from 'lib/void'
|
||||
import { useSidePanelsStateSnapshot } from 'state/side-panels'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Command_Shadcn_,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormMessage_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
SheetTitle,
|
||||
Switch,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { Button, SidePanel } from 'ui'
|
||||
|
||||
const GITHUB_ICON = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98 96" className="w-6">
|
||||
<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>
|
||||
)
|
||||
|
||||
export type SidePanelGitHubRepoLinkerProps = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
const SidePanelGitHubRepoLinker = ({ projectRef }: SidePanelGitHubRepoLinkerProps) => {
|
||||
const selectedProject = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const sidePanelStateSnapshot = useSidePanelsStateSnapshot()
|
||||
|
||||
const [foreignProjectsComboBoxOpen, setForeignProjectsComboboxOpen] = useState(false)
|
||||
const [selectedRepoId, setSelectedRepoId] = useState<string | undefined>()
|
||||
const [isConfirmingBranchChange, setIsConfirmingBranchChange] = useState(false)
|
||||
|
||||
const visible = sidePanelStateSnapshot.githubConnectionsOpen
|
||||
|
||||
const { data: gitHubAuthorization, isLoading: isLoadingGitHubAuthorization } =
|
||||
@@ -75,20 +47,22 @@ const SidePanelGitHubRepoLinker = ({ projectRef }: SidePanelGitHubRepoLinkerProp
|
||||
enabled: visible && Boolean(gitHubAuthorization),
|
||||
})
|
||||
|
||||
const { data: connections } = useGitHubConnectionsQuery(
|
||||
{ organizationId: selectedOrganization?.id },
|
||||
{ enabled: !!projectRef && visible }
|
||||
/**
|
||||
* Supabase projects available
|
||||
*/
|
||||
const { data: supabaseProjectsData, isLoading: isLoadingSupabaseProjects } = useProjectsQuery({
|
||||
enabled: visible,
|
||||
})
|
||||
|
||||
const supabaseProjects = useMemo(
|
||||
() =>
|
||||
supabaseProjectsData
|
||||
?.filter((project) => project.organization_id === selectedOrganization?.id)
|
||||
.map((project) => ({ name: project.name, ref: project.ref })) ?? EMPTY_ARR,
|
||||
[selectedOrganization?.id, supabaseProjectsData]
|
||||
)
|
||||
|
||||
const existingConnection = useMemo(
|
||||
() => connections?.find((c) => c.project.ref === projectRef),
|
||||
[connections, projectRef]
|
||||
)
|
||||
|
||||
const { mutate: createBranch } = useBranchCreateMutation()
|
||||
const { mutate: updateBranch } = useBranchUpdateMutation()
|
||||
|
||||
const githubRepos: ForeignProject[] = useMemo(
|
||||
const githubRepos = useMemo(
|
||||
() =>
|
||||
githubReposData?.map((repo: any) => ({
|
||||
id: repo.id.toString(),
|
||||
@@ -98,558 +72,108 @@ const SidePanelGitHubRepoLinker = ({ projectRef }: SidePanelGitHubRepoLinkerProp
|
||||
[githubReposData]
|
||||
)
|
||||
|
||||
const selectedRepo = useMemo(() => {
|
||||
return (githubReposData as any[])?.find((r) => r.id.toString() === selectedRepoId)
|
||||
}, [githubReposData, selectedRepoId])
|
||||
|
||||
const { data: existingBranches } = useBranchesQuery(
|
||||
{ projectRef },
|
||||
{ enabled: !!projectRef && visible }
|
||||
const { data: connections } = useGitHubConnectionsQuery(
|
||||
{
|
||||
organizationId: selectedOrganization?.id,
|
||||
},
|
||||
{
|
||||
enabled: visible,
|
||||
}
|
||||
)
|
||||
|
||||
const isBranchingEnabled = selectedProject?.is_branch_enabled ?? false
|
||||
|
||||
const { mutate: createConnection, isLoading: isCreatingConnection } =
|
||||
useGitHubConnectionCreateMutation({})
|
||||
useGitHubConnectionCreateMutation({
|
||||
onSuccess() {
|
||||
toast.success('Successfully linked project to repository!')
|
||||
sidePanelStateSnapshot.setGithubConnectionsOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync: deleteConnection } = useGitHubConnectionDeleteMutation()
|
||||
|
||||
// Form and config state
|
||||
const { mutateAsync: checkGithubBranchValidity, isLoading: isCheckingBranch } =
|
||||
useCheckGithubBranchValidity({ onError: () => {} })
|
||||
const createGithubConnection = async (variables: IntegrationConnectionsCreateVariables) => {
|
||||
if (!selectedOrganization?.id) {
|
||||
throw new Error('No organization id')
|
||||
}
|
||||
if (!variables.new) {
|
||||
throw new Error('No new connection')
|
||||
}
|
||||
|
||||
const { mutate: updateConnectionSettings, isLoading: isUpdatingConnection } =
|
||||
useGitHubConnectionUpdateMutation()
|
||||
const existingConnection = connections?.find(
|
||||
(connection) => connection.project.ref === projectRef
|
||||
)
|
||||
|
||||
const FormSchema = z
|
||||
.object({
|
||||
repo: z.string().optional(),
|
||||
autoBranchingEnabled: z.boolean().default(false),
|
||||
branchName: z.string(),
|
||||
supabaseDirectory: z.string().default(''),
|
||||
supabaseChangesOnly: z.boolean().default(false),
|
||||
branchLimit: z.string().default('50'),
|
||||
})
|
||||
.superRefine(async (val, ctx) => {
|
||||
if (val.autoBranchingEnabled) {
|
||||
if (!selectedRepo) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Please select a repository',
|
||||
path: ['repo'],
|
||||
})
|
||||
}
|
||||
if (val.branchName.length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Branch name cannot be empty',
|
||||
path: ['branchName'],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedRepo?.id && val.branchName && val.branchName.length > 0) {
|
||||
try {
|
||||
await checkGithubBranchValidity({
|
||||
repositoryId: Number(selectedRepo.id),
|
||||
branchName: val.branchName,
|
||||
})
|
||||
} catch (error) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Branch "${val.branchName}" not found in repository`,
|
||||
path: ['branchName'],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onBlur',
|
||||
defaultValues: {
|
||||
autoBranchingEnabled: false,
|
||||
branchName: 'main',
|
||||
supabaseDirectory: '',
|
||||
supabaseChangesOnly: false,
|
||||
branchLimit: '50',
|
||||
},
|
||||
})
|
||||
|
||||
const autoBranchingEnabled = form.watch('autoBranchingEnabled')
|
||||
const prodBranch = existingBranches?.find((b: any) => b.is_default)
|
||||
const isCurrentlyEnabled = Boolean(existingConnection)
|
||||
|
||||
const submitButtonText =
|
||||
autoBranchingEnabled !== isCurrentlyEnabled
|
||||
? autoBranchingEnabled
|
||||
? 'Enable'
|
||||
: 'Disable'
|
||||
: 'Save'
|
||||
|
||||
// This saves connection settings and creates/updates branches which also enables branching if it's not already enabled
|
||||
const executeSave = async (data: z.infer<typeof FormSchema>) => {
|
||||
if (!projectRef) return console.error('Project ref missing')
|
||||
if (!selectedOrganization) return console.error('Organization not selected')
|
||||
|
||||
const defaultBranch = data.branchName
|
||||
|
||||
if (data.autoBranchingEnabled) {
|
||||
if (!selectedRepo) return toast.error('Please select a repository to connect to.')
|
||||
|
||||
if (!isBranchingEnabled) {
|
||||
// create production branch (enables branching)
|
||||
createBranch({
|
||||
projectRef,
|
||||
branchName: defaultBranch,
|
||||
gitBranch: defaultBranch,
|
||||
})
|
||||
} else {
|
||||
if (prodBranch?.id) {
|
||||
updateBranch({
|
||||
id: prodBranch.id,
|
||||
projectRef,
|
||||
branchName: defaultBranch,
|
||||
gitBranch: defaultBranch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (existingConnection) {
|
||||
if (existingConnection.repository.id.toString() !== selectedRepo.id.toString()) {
|
||||
await deleteConnection({
|
||||
organizationId: selectedOrganization.id,
|
||||
connectionId: existingConnection.id,
|
||||
})
|
||||
createConnection({
|
||||
organizationId: selectedOrganization.id,
|
||||
connection: {
|
||||
installation_id: selectedRepo.installation_id!,
|
||||
project_ref: projectRef,
|
||||
repository_id: Number(selectedRepo.id),
|
||||
workdir: data.supabaseDirectory,
|
||||
supabase_changes_only: data.supabaseChangesOnly,
|
||||
branch_limit: Number(data.branchLimit),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
updateConnectionSettings({
|
||||
connectionId: existingConnection.id,
|
||||
organizationId: selectedOrganization.id,
|
||||
workdir: data.supabaseDirectory,
|
||||
supabaseChangesOnly: data.supabaseChangesOnly,
|
||||
branchLimit: Number(data.branchLimit),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
createConnection({
|
||||
organizationId: selectedOrganization.id,
|
||||
connection: {
|
||||
installation_id: selectedRepo.installation_id!,
|
||||
project_ref: projectRef,
|
||||
repository_id: Number(selectedRepo.id),
|
||||
workdir: data.supabaseDirectory,
|
||||
supabase_changes_only: data.supabaseChangesOnly,
|
||||
branch_limit: Number(data.branchLimit),
|
||||
},
|
||||
})
|
||||
}
|
||||
toast.success('Automatic branching enabled')
|
||||
} else {
|
||||
if (existingConnection) {
|
||||
if (existingConnection) {
|
||||
// remove existing connection so we can recreate it or update it
|
||||
try {
|
||||
await deleteConnection({
|
||||
organizationId: selectedOrganization.id,
|
||||
connectionId: existingConnection.id,
|
||||
})
|
||||
toast.success('Automatic branching disabled')
|
||||
} catch (error) {
|
||||
// ignore the error to let createConnection handle it
|
||||
}
|
||||
}
|
||||
setIsConfirmingBranchChange(false)
|
||||
sidePanelStateSnapshot.setGithubConnectionsOpen(false)
|
||||
|
||||
createConnection({
|
||||
organizationId: selectedOrganization.id,
|
||||
connection: variables.new,
|
||||
})
|
||||
}
|
||||
|
||||
const handleSave = (data: z.infer<typeof FormSchema>) => {
|
||||
const originalBranchName = prodBranch?.git_branch
|
||||
|
||||
if (
|
||||
existingConnection &&
|
||||
originalBranchName &&
|
||||
data.branchName !== originalBranchName &&
|
||||
data.autoBranchingEnabled
|
||||
) {
|
||||
setIsConfirmingBranchChange(true)
|
||||
} else {
|
||||
executeSave(data)
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirmBranchChange = async () => {
|
||||
await executeSave(form.getValues())
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (existingConnection) {
|
||||
setSelectedRepoId(existingConnection.repository.id.toString())
|
||||
}
|
||||
}, [existingConnection])
|
||||
|
||||
// Reset form whenever the repo / connection / branches change
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const isAutoBranchingEnabled = Boolean(existingConnection)
|
||||
|
||||
if (existingConnection) {
|
||||
const defaults = {
|
||||
autoBranchingEnabled: isAutoBranchingEnabled,
|
||||
repo: existingConnection.repository.name,
|
||||
branchName: prodBranch?.git_branch ?? 'main',
|
||||
supabaseDirectory:
|
||||
(existingConnection as any)?.workdir ??
|
||||
(existingConnection as any)?.metadata?.supabaseConfig?.supabaseDirectory ??
|
||||
'',
|
||||
supabaseChangesOnly:
|
||||
(existingConnection as any)?.supabase_changes_only ??
|
||||
(existingConnection as any)?.metadata?.supabaseConfig?.supabaseChangesOnly ??
|
||||
false,
|
||||
branchLimit: String(
|
||||
((existingConnection as any)?.branch_limit as number | undefined) ??
|
||||
(existingConnection as any)?.metadata?.supabaseConfig?.branchLimit ??
|
||||
50
|
||||
),
|
||||
}
|
||||
|
||||
form.reset(defaults)
|
||||
} else {
|
||||
form.reset({
|
||||
autoBranchingEnabled: isAutoBranchingEnabled,
|
||||
repo: '',
|
||||
branchName: 'main',
|
||||
supabaseDirectory: '',
|
||||
supabaseChangesOnly: false,
|
||||
branchLimit: '50',
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [existingConnection?.id, existingBranches, visible, prodBranch])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRepo && !existingConnection) {
|
||||
form.setValue('repo', selectedRepo.name)
|
||||
form.setValue('branchName', selectedRepo.default_branch ?? 'main')
|
||||
}
|
||||
}, [selectedRepo, existingConnection, form])
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
open={visible}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) sidePanelStateSnapshot.setGithubConnectionsOpen(false)
|
||||
}}
|
||||
<SidePanel
|
||||
header={'Add GitHub repository'}
|
||||
size="large"
|
||||
visible={visible}
|
||||
hideFooter
|
||||
onCancel={() => sidePanelStateSnapshot.setGithubConnectionsOpen(false)}
|
||||
>
|
||||
<SheetContent side="right" size="content" showClose className="flex flex-col gap-0">
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSave)}
|
||||
className="flex-1 overflow-hidden flex flex-col"
|
||||
>
|
||||
<SheetHeader className="shrink-0">
|
||||
<SheetTitle>Automatic Branching</SheetTitle>
|
||||
<SheetDescription>
|
||||
Create a Supabase branch for every GitHub pull request and sync them on every commit
|
||||
and merge.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="py-10 flex flex-col gap-6 bg-studio h-full">
|
||||
<SidePanel.Content className="flex flex-col gap-4">
|
||||
<Markdown
|
||||
content={`
|
||||
### Choose repository to connect to
|
||||
|
||||
<SheetSection className="py-6 flex-1 overflow-y-auto">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="autoBranchingEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
className={'mb-4'}
|
||||
layout="flex-row-reverse"
|
||||
label="Enable automatic branching"
|
||||
description="Once enabled, new branches will be created for every new GitHub branch"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={!gitHubAuthorization}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
Your access to repositories depends on your GitHub x Supabase integration setup. You can access repositories that you own, those where you're a collaborator, and those available through organizations you're a member of. Access is determined by both your personal installation and any installations made by the organizations you belong to.
|
||||
`}
|
||||
/>
|
||||
|
||||
{/* GitHub authorization prompt if needed */}
|
||||
{gitHubAuthorization === null ? (
|
||||
<div className="flex flex-col items-center justify-center relative border rounded-lg py-12 px-4 bg shadow px-20s">
|
||||
<p className="text-sm text-center">
|
||||
Connect your Supabase projects with your GitHub repositories
|
||||
</p>
|
||||
<p className="text-sm text-center text-foreground-light">
|
||||
Authorize with GitHub to retrieve your GitHub repositories
|
||||
</p>
|
||||
<Button
|
||||
className="w-min mt-3"
|
||||
onClick={() => {
|
||||
openInstallGitHubIntegrationWindow('authorize')
|
||||
}}
|
||||
>
|
||||
Authorize GitHub
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-4',
|
||||
!autoBranchingEnabled && 'opacity-25 pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="repo"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Connected Repo"
|
||||
description="The GitHub repository you want to connect to."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Popover_Shadcn_
|
||||
open={foreignProjectsComboBoxOpen}
|
||||
onOpenChange={setForeignProjectsComboboxOpen}
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="default"
|
||||
block
|
||||
disabled={isLoadingGitHubRepos || !autoBranchingEnabled}
|
||||
loading={isLoadingGitHubRepos}
|
||||
className="justify-start h-[34px]"
|
||||
icon={<Github size={16} className="text-foreground-light" />}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
<ChevronDown />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{selectedRepo?.name ?? 'Choose a repository'}
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0 !w-72"
|
||||
side="bottom"
|
||||
align="center"
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search for a repository" />
|
||||
<CommandList_Shadcn_ className="!max-h-[170px]">
|
||||
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{githubRepos.map((repo) => (
|
||||
<CommandItem_Shadcn_
|
||||
key={repo.id}
|
||||
value={repo.name}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={() => {
|
||||
if (repo.id === selectedRepoId) {
|
||||
setForeignProjectsComboboxOpen(false)
|
||||
return
|
||||
}
|
||||
const newRepoData = (githubReposData as any[])?.find(
|
||||
(r) => r.id.toString() === repo.id
|
||||
)
|
||||
form.reset({
|
||||
autoBranchingEnabled:
|
||||
form.getValues('autoBranchingEnabled'),
|
||||
repo: newRepoData.name,
|
||||
branchName: newRepoData.default_branch ?? 'main',
|
||||
supabaseDirectory: '',
|
||||
supabaseChangesOnly: false,
|
||||
branchLimit: '50',
|
||||
})
|
||||
setSelectedRepoId(repo.id)
|
||||
setForeignProjectsComboboxOpen(false)
|
||||
}}
|
||||
>
|
||||
<Github size={16} className="text-foreground-light" />
|
||||
<span className="truncate" title={repo.name}>
|
||||
{repo.name}
|
||||
</span>
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="branchName"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Production git branch name"
|
||||
description="Sync a git branch to your production Supabase branch"
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
disabled={!autoBranchingEnabled}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<div className="absolute top-2.5 right-3 flex items-center gap-2">
|
||||
{isCheckingBranch && <Loader2 size={14} className="animate-spin" />}
|
||||
{field.value &&
|
||||
!isCheckingBranch &&
|
||||
!form.formState.errors.branchName &&
|
||||
autoBranchingEnabled && (
|
||||
<Check size={14} className="text-brand" strokeWidth={2} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="supabaseDirectory"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Supabase directory"
|
||||
description="Relative path to your supabase directory."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
placeholder="supabase"
|
||||
autoComplete="off"
|
||||
disabled={!autoBranchingEnabled}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="branchLimit"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Branch limit"
|
||||
description="Maximum preview branches that can be created automatically."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
disabled={!autoBranchingEnabled}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="supabaseChangesOnly"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Supabase changes only"
|
||||
description="Only trigger branch creation when files inside the Supabase directory change."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(val) => field.onChange(val)}
|
||||
disabled={!autoBranchingEnabled}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SheetSection>
|
||||
<SheetFooter className="shrink-0 block p-0">
|
||||
{autoBranchingEnabled && (
|
||||
<div className="flex flex-row items-center gap-4 p-4 bg border-b">
|
||||
<figure className="w-10 h-10 rounded-md border flex items-center justify-center">
|
||||
<DollarSign className="text-info" size={20} strokeWidth={2} />
|
||||
</figure>
|
||||
<div>
|
||||
<p className="text-sm text-foreground">
|
||||
Preview branches are billed $0.32 per day
|
||||
</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
This cost will continue for as long as the branch is active
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 p-4">
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
onClick={() => sidePanelStateSnapshot.setGithubConnectionsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
htmlType="submit"
|
||||
loading={isUpdatingConnection || isCreatingConnection}
|
||||
disabled={
|
||||
isUpdatingConnection ||
|
||||
isCreatingConnection ||
|
||||
isCheckingBranch ||
|
||||
isLoadingGitHubAuthorization ||
|
||||
isLoadingGitHubRepos
|
||||
}
|
||||
>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
<ConfirmationModal
|
||||
variant="warning"
|
||||
visible={isConfirmingBranchChange}
|
||||
title="Changing production git branch"
|
||||
confirmLabel="Confirm"
|
||||
size="medium"
|
||||
onCancel={() => setIsConfirmingBranchChange(false)}
|
||||
onConfirm={onConfirmBranchChange}
|
||||
loading={isUpdatingConnection || isCreatingConnection}
|
||||
>
|
||||
<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>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
{gitHubAuthorization === null ? (
|
||||
<div className="flex flex-col items-center justify-center mt-8 relative border rounded-lg p-12 bg shadow px-20s">
|
||||
<p className="text-sm text-center">
|
||||
Connect your Supabase projects with your GitHub repositories
|
||||
</p>
|
||||
<p className="text-sm text-center text-foreground-light">
|
||||
Authorize with GitHub to retrieve your GitHub repositories
|
||||
</p>
|
||||
<Button
|
||||
className="w-min mt-3"
|
||||
onClick={() => {
|
||||
openInstallGitHubIntegrationWindow('authorize')
|
||||
}}
|
||||
>
|
||||
Authorize GitHub
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<ProjectLinker
|
||||
defaultSupabaseProjectRef={projectRef}
|
||||
foreignProjects={githubRepos}
|
||||
supabaseProjects={supabaseProjects}
|
||||
onCreateConnections={createGithubConnection}
|
||||
isLoading={isCreatingConnection}
|
||||
loadingForeignProjects={isLoadingGitHubRepos}
|
||||
loadingSupabaseProjects={isLoadingSupabaseProjects}
|
||||
integrationIcon={GITHUB_ICON}
|
||||
choosePrompt="Choose GitHub Repo"
|
||||
showNoEntitiesState={false}
|
||||
mode="GitHub"
|
||||
/>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { useSidePanelsStateSnapshot } from 'state/side-panels'
|
||||
import { Badge, Button, Card, CardContent } from 'ui'
|
||||
|
||||
export const AutomaticBranchingRow = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const project = useSelectedProject()
|
||||
const org = useSelectedOrganization()
|
||||
const sidePanelsStateSnapshot = useSidePanelsStateSnapshot()
|
||||
|
||||
const isBranch = project?.parent_project_ref !== undefined
|
||||
|
||||
// Permissions for reading and creating GitHub connections
|
||||
const canReadGitHubConnection = useCheckPermissions(
|
||||
PermissionAction.READ,
|
||||
'integrations.github_connections'
|
||||
)
|
||||
const canCreateGitHubConnection = useCheckPermissions(
|
||||
PermissionAction.CREATE,
|
||||
'integrations.github_connections'
|
||||
)
|
||||
|
||||
const { data: allConnections } = useGitHubConnectionsQuery({ organizationId: org?.id })
|
||||
|
||||
// Determine automatic branching status
|
||||
const baseProjectRef = isBranch ? project?.parent_project_ref : projectRef
|
||||
const { data: branches } = useBranchesQuery(
|
||||
{ projectRef: baseProjectRef },
|
||||
{ enabled: Boolean(baseProjectRef) }
|
||||
)
|
||||
|
||||
const prodBranch = branches?.find((b) => b.is_default) ?? branches?.[0]
|
||||
|
||||
const { data: projects } = useProjectsQuery()
|
||||
const baseProject = isBranch
|
||||
? projects?.find((p) => p.ref === project?.parent_project_ref)
|
||||
: project
|
||||
|
||||
// Filter connections for current (base) project
|
||||
const connections =
|
||||
allConnections?.filter((connection) =>
|
||||
isBranch
|
||||
? connection.project.ref === project?.parent_project_ref
|
||||
: connection.project.ref === projectRef
|
||||
) ?? []
|
||||
|
||||
const autoBranchingEnabled =
|
||||
connections.length > 0 &&
|
||||
Boolean(baseProject?.is_branch_enabled) &&
|
||||
Boolean(prodBranch?.git_branch && prodBranch.git_branch.trim().length > 0)
|
||||
|
||||
/*
|
||||
If the user does not have permission to read GitHub connections, we simply do not
|
||||
render the row. The surrounding page should handle showing a more global
|
||||
NoPermission component if needed.
|
||||
*/
|
||||
if (!canReadGitHubConnection) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-between gap-6">
|
||||
{/* Left: label and description */}
|
||||
<div className="flex-1">
|
||||
<h5 className="text-foreground mb-1 text-sm">Automatic Branching</h5>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Create a Supabase branch for every GitHub pull request and sync them on every commit and
|
||||
merge.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right: status badge + configure button */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{autoBranchingEnabled ? (
|
||||
<Badge variant="brand">Enabled</Badge>
|
||||
) : (
|
||||
<Badge variant="default">Disabled</Badge>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => sidePanelsStateSnapshot.setGithubConnectionsOpen(true)}
|
||||
disabled={isBranch || (!canCreateGitHubConnection && connections.length === 0)}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
+544
-7
@@ -1,15 +1,51 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Check, Github, Loader2 } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldSection,
|
||||
ScaffoldSectionContent,
|
||||
ScaffoldSectionDetail,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import { EmptyIntegrationConnection } from 'components/interfaces/Integrations/VercelGithub/IntegrationPanels'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
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 { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import type { IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { AutomaticBranchingRow } from './AutomaticBranchingRow'
|
||||
import { useSidePanelsStateSnapshot } from 'state/side-panels'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
cn,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormMessage_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
Switch,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { IntegrationConnectionItem } from 'components/interfaces/Integrations/VercelGithub/IntegrationConnection'
|
||||
|
||||
const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' }) => {
|
||||
return (
|
||||
@@ -22,13 +58,260 @@ const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' }) => {
|
||||
}
|
||||
|
||||
const GitHubSection = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const selectedProject = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const sidePanelStateSnapshot = useSidePanelsStateSnapshot()
|
||||
|
||||
const [isConfirmingBranchChange, setIsConfirmingBranchChange] = useState(false)
|
||||
|
||||
const canReadGitHubConnection = useCheckPermissions(
|
||||
PermissionAction.READ,
|
||||
'integrations.github_connections'
|
||||
)
|
||||
const canCreateGitHubConnection = useCheckPermissions(
|
||||
PermissionAction.CREATE,
|
||||
'integrations.github_connections'
|
||||
)
|
||||
const canUpdateGitHubConnection = useCheckPermissions(
|
||||
PermissionAction.UPDATE,
|
||||
'integrations.github_connections'
|
||||
)
|
||||
|
||||
const { data: connections } = useGitHubConnectionsQuery(
|
||||
{ organizationId: selectedOrganization?.id },
|
||||
{ enabled: !!projectRef && !!selectedOrganization?.id }
|
||||
)
|
||||
|
||||
const existingConnection = useMemo(
|
||||
() => connections?.find((c) => c.project.ref === projectRef),
|
||||
[connections, projectRef]
|
||||
)
|
||||
|
||||
const { mutate: createBranch } = useBranchCreateMutation()
|
||||
const { mutate: updateBranch } = useBranchUpdateMutation()
|
||||
|
||||
const { data: existingBranches } = useBranchesQuery({ projectRef }, { enabled: !!projectRef })
|
||||
|
||||
const isBranchingEnabled = selectedProject?.is_branch_enabled ?? false
|
||||
|
||||
const { mutate: createConnection, isLoading: isCreatingConnection } =
|
||||
useGitHubConnectionCreateMutation({})
|
||||
|
||||
const { mutate: deleteConnection } = useGitHubConnectionDeleteMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('Successfully deleted GitHub connection')
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync: checkGithubBranchValidity, isLoading: isCheckingBranch } =
|
||||
useCheckGithubBranchValidity({ onError: () => {} })
|
||||
|
||||
const { mutate: updateConnectionSettings, isLoading: isUpdatingConnection } =
|
||||
useGitHubConnectionUpdateMutation()
|
||||
|
||||
const prodBranch = existingBranches?.find((b: any) => b.is_default)
|
||||
const isConnected = Boolean(existingConnection)
|
||||
|
||||
const GitHubTitle = `GitHub Integration`
|
||||
|
||||
// Production Branch Form
|
||||
const ProductionBranchSchema = z
|
||||
.object({
|
||||
branchName: z.string(),
|
||||
})
|
||||
.superRefine(async (val, ctx) => {
|
||||
if (isConnected && existingConnection && val.branchName && val.branchName.length > 0) {
|
||||
try {
|
||||
await checkGithubBranchValidity({
|
||||
repositoryId: existingConnection.repository.id,
|
||||
branchName: val.branchName,
|
||||
})
|
||||
} catch (error) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Branch "${val.branchName}" not found in repository`,
|
||||
path: ['branchName'],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const productionBranchForm = useForm<z.infer<typeof ProductionBranchSchema>>({
|
||||
resolver: zodResolver(ProductionBranchSchema),
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onBlur',
|
||||
defaultValues: {
|
||||
branchName: 'main',
|
||||
},
|
||||
})
|
||||
|
||||
// Automatic Branching Form
|
||||
const AutomaticBranchingSchema = z.object({
|
||||
autoBranchingEnabled: z.boolean().default(false),
|
||||
supabaseDirectory: z.string().default(''),
|
||||
supabaseChangesOnly: z.boolean().default(false),
|
||||
branchLimit: z.string().default('50'),
|
||||
})
|
||||
|
||||
const automaticBranchingForm = useForm<z.infer<typeof AutomaticBranchingSchema>>({
|
||||
resolver: zodResolver(AutomaticBranchingSchema),
|
||||
defaultValues: {
|
||||
autoBranchingEnabled: false,
|
||||
supabaseDirectory: '',
|
||||
supabaseChangesOnly: false,
|
||||
branchLimit: '50',
|
||||
},
|
||||
})
|
||||
|
||||
const autoBranchingEnabled = automaticBranchingForm.watch('autoBranchingEnabled')
|
||||
|
||||
const onSubmitProductionBranch = async (data: z.infer<typeof ProductionBranchSchema>) => {
|
||||
const originalBranchName = prodBranch?.git_branch
|
||||
|
||||
if (originalBranchName && data.branchName !== originalBranchName) {
|
||||
setIsConfirmingBranchChange(true)
|
||||
} else {
|
||||
await executeProductionBranchSave(data)
|
||||
}
|
||||
}
|
||||
|
||||
const executeProductionBranchSave = async (data: z.infer<typeof ProductionBranchSchema>) => {
|
||||
if (!projectRef) return
|
||||
|
||||
if (!isBranchingEnabled) {
|
||||
// create production branch (enables branching)
|
||||
createBranch({
|
||||
projectRef,
|
||||
branchName: data.branchName,
|
||||
gitBranch: data.branchName,
|
||||
})
|
||||
} else {
|
||||
if (prodBranch?.id) {
|
||||
updateBranch({
|
||||
id: prodBranch.id,
|
||||
projectRef,
|
||||
branchName: prodBranch.name,
|
||||
gitBranch: data.branchName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('Production branch configuration updated')
|
||||
setIsConfirmingBranchChange(false)
|
||||
}
|
||||
|
||||
const onConfirmBranchChange = async () => {
|
||||
await executeProductionBranchSave(productionBranchForm.getValues())
|
||||
}
|
||||
|
||||
const onSubmitAutomaticBranching = async (data: z.infer<typeof AutomaticBranchingSchema>) => {
|
||||
if (!projectRef || !selectedOrganization) return console.error('Missing required data')
|
||||
if (!existingConnection) return toast.error('Please connect to a repository first.')
|
||||
|
||||
if (data.autoBranchingEnabled) {
|
||||
const defaultBranch = productionBranchForm.getValues().branchName
|
||||
|
||||
if (!isBranchingEnabled) {
|
||||
// create production branch (enables branching)
|
||||
createBranch({
|
||||
projectRef,
|
||||
branchName: defaultBranch,
|
||||
gitBranch: defaultBranch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateConnectionSettings({
|
||||
connectionId: existingConnection.id,
|
||||
organizationId: selectedOrganization.id,
|
||||
workdir: data.supabaseDirectory,
|
||||
supabaseChangesOnly: data.supabaseChangesOnly,
|
||||
branchLimit: Number(data.branchLimit),
|
||||
})
|
||||
|
||||
if (data.autoBranchingEnabled) {
|
||||
toast.success('Automatic branching enabled')
|
||||
} else {
|
||||
toast.success('Automatic branching disabled')
|
||||
}
|
||||
}
|
||||
|
||||
const openLinkerPanel = () => {
|
||||
sidePanelStateSnapshot.setGithubConnectionsOpen(true)
|
||||
}
|
||||
|
||||
const onDeleteGitHubConnection = useCallback(
|
||||
async (connection: IntegrationProjectConnection) => {
|
||||
if (!selectedOrganization?.id) {
|
||||
toast.error('Organization not found')
|
||||
return
|
||||
}
|
||||
|
||||
deleteConnection({
|
||||
connectionId: Number(connection.id),
|
||||
organizationId: selectedOrganization.id,
|
||||
})
|
||||
},
|
||||
[deleteConnection, selectedOrganization?.id]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (existingConnection) {
|
||||
automaticBranchingForm.reset({
|
||||
autoBranchingEnabled: true,
|
||||
supabaseDirectory:
|
||||
(existingConnection as any)?.workdir ??
|
||||
(existingConnection as any)?.metadata?.supabaseConfig?.supabaseDirectory ??
|
||||
'',
|
||||
supabaseChangesOnly:
|
||||
(existingConnection as any)?.supabase_changes_only ??
|
||||
(existingConnection as any)?.metadata?.supabaseConfig?.supabaseChangesOnly ??
|
||||
false,
|
||||
branchLimit: String(
|
||||
((existingConnection as any)?.branch_limit as number | undefined) ??
|
||||
(existingConnection as any)?.metadata?.supabaseConfig?.branchLimit ??
|
||||
50
|
||||
),
|
||||
})
|
||||
} else {
|
||||
automaticBranchingForm.reset({
|
||||
autoBranchingEnabled: false,
|
||||
supabaseDirectory: '',
|
||||
supabaseChangesOnly: false,
|
||||
branchLimit: '50',
|
||||
})
|
||||
}
|
||||
}, [existingConnection, automaticBranchingForm])
|
||||
|
||||
useEffect(() => {
|
||||
if (prodBranch?.git_branch) {
|
||||
productionBranchForm.reset({
|
||||
branchName: prodBranch.git_branch,
|
||||
})
|
||||
} else {
|
||||
productionBranchForm.reset({
|
||||
branchName: 'main',
|
||||
})
|
||||
}
|
||||
}, [prodBranch, productionBranchForm])
|
||||
|
||||
if (!canReadGitHubConnection) {
|
||||
return (
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<ScaffoldSectionDetail title={GitHubTitle}>
|
||||
<p>Connect any of your GitHub repositories to a project.</p>
|
||||
<IntegrationImageHandler title="github" />
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent>
|
||||
<NoPermission resourceText="view this organization's GitHub connections" />
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
@@ -37,14 +320,268 @@ const GitHubSection = () => {
|
||||
<IntegrationImageHandler title="github" />
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent>
|
||||
{!canReadGitHubConnection ? (
|
||||
<NoPermission resourceText="view this organization's GitHub connections" />
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Section 1: GitHub Connection */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg">Integration features</h3>
|
||||
<AutomaticBranchingRow />
|
||||
<h5 className="text-foreground mb-3 text-sm">GitHub Repository</h5>
|
||||
{existingConnection ? (
|
||||
<ul className="flex flex-col gap-y-2">
|
||||
<IntegrationConnectionItem
|
||||
key={existingConnection.id}
|
||||
disabled={!canUpdateGitHubConnection}
|
||||
connection={{
|
||||
id: String(existingConnection.id),
|
||||
added_by: {
|
||||
id: String(existingConnection.user?.id),
|
||||
primary_email: existingConnection.user?.primary_email ?? '',
|
||||
username: existingConnection.user?.username ?? '',
|
||||
},
|
||||
foreign_project_id: String(existingConnection.repository.id),
|
||||
supabase_project_ref: existingConnection.project.ref,
|
||||
organization_integration_id: 'unused',
|
||||
inserted_at: existingConnection.inserted_at,
|
||||
updated_at: existingConnection.updated_at,
|
||||
metadata: {
|
||||
name: existingConnection.repository.name,
|
||||
} as any,
|
||||
}}
|
||||
type="GitHub"
|
||||
onDeleteConnection={onDeleteGitHubConnection}
|
||||
/>
|
||||
</ul>
|
||||
) : (
|
||||
<EmptyIntegrationConnection
|
||||
onClick={openLinkerPanel}
|
||||
showNode={false}
|
||||
disabled={!canCreateGitHubConnection}
|
||||
>
|
||||
Connect repository
|
||||
</EmptyIntegrationConnection>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 2: Production Branch Configuration */}
|
||||
<Form_Shadcn_ {...productionBranchForm}>
|
||||
<form onSubmit={productionBranchForm.handleSubmit(onSubmitProductionBranch)}>
|
||||
<Card className={cn(!isConnected && 'opacity-50')}>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={productionBranchForm.control}
|
||||
name="branchName"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Production git branch name"
|
||||
description="Sync a git branch to your production Supabase branch"
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
disabled={!isConnected || !canUpdateGitHubConnection}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<div className="absolute top-2.5 right-3 flex items-center gap-2">
|
||||
{isCheckingBranch && <Loader2 size={14} className="animate-spin" />}
|
||||
{field.value &&
|
||||
!isCheckingBranch &&
|
||||
!productionBranchForm.formState.errors.branchName &&
|
||||
isConnected && (
|
||||
<Check size={14} className="text-brand" strokeWidth={2} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{productionBranchForm.formState.isDirty && (
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => productionBranchForm.reset()}
|
||||
disabled={!isConnected || !canUpdateGitHubConnection}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
!isConnected ||
|
||||
!canUpdateGitHubConnection ||
|
||||
isCheckingBranch ||
|
||||
!productionBranchForm.formState.isDirty
|
||||
}
|
||||
loading={isCheckingBranch}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
|
||||
{/* Section 3: Automatic Branching Configuration */}
|
||||
<Form_Shadcn_ {...automaticBranchingForm}>
|
||||
<form onSubmit={automaticBranchingForm.handleSubmit(onSubmitAutomaticBranching)}>
|
||||
<Card className={cn(!isConnected && 'opacity-50')}>
|
||||
<CardContent className="space-y-4">
|
||||
<FormField_Shadcn_
|
||||
control={automaticBranchingForm.control}
|
||||
name="autoBranchingEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Enable automatic branching"
|
||||
description="New branches will be created for every GitHub pull request"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={!isConnected || !canCreateGitHubConnection}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'space-y-4',
|
||||
(!autoBranchingEnabled || !isConnected) && 'opacity-25 pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<FormField_Shadcn_
|
||||
control={automaticBranchingForm.control}
|
||||
name="supabaseDirectory"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Supabase directory"
|
||||
description="Relative path to your supabase directory."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
placeholder="supabase"
|
||||
autoComplete="off"
|
||||
disabled={
|
||||
!autoBranchingEnabled ||
|
||||
!isConnected ||
|
||||
!canUpdateGitHubConnection
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={automaticBranchingForm.control}
|
||||
name="branchLimit"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Branch limit"
|
||||
description="Maximum preview branches that can be created automatically."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
disabled={
|
||||
!autoBranchingEnabled ||
|
||||
!isConnected ||
|
||||
!canUpdateGitHubConnection
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={automaticBranchingForm.control}
|
||||
name="supabaseChangesOnly"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Supabase changes only"
|
||||
description="Only trigger branch creation when files inside the Supabase directory change."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(val) => field.onChange(val)}
|
||||
disabled={
|
||||
!autoBranchingEnabled ||
|
||||
!isConnected ||
|
||||
!canUpdateGitHubConnection
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{automaticBranchingForm.formState.isDirty && (
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => automaticBranchingForm.reset()}
|
||||
disabled={!isConnected || !canCreateGitHubConnection}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
!isConnected ||
|
||||
!canCreateGitHubConnection ||
|
||||
isUpdatingConnection ||
|
||||
isCreatingConnection ||
|
||||
!automaticBranchingForm.formState.isDirty
|
||||
}
|
||||
loading={isUpdatingConnection || isCreatingConnection}
|
||||
>
|
||||
{automaticBranchingForm.watch('autoBranchingEnabled') !== isConnected
|
||||
? automaticBranchingForm.watch('autoBranchingEnabled')
|
||||
? 'Enable'
|
||||
: 'Disable'
|
||||
: 'Save'}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
variant="warning"
|
||||
visible={isConfirmingBranchChange}
|
||||
title="Changing production git branch"
|
||||
confirmLabel="Confirm"
|
||||
size="medium"
|
||||
onCancel={() => setIsConfirmingBranchChange(false)}
|
||||
onConfirm={onConfirmBranchChange}
|
||||
loading={isUpdatingConnection || isCreatingConnection}
|
||||
>
|
||||
<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>
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
|
||||
Reference in new issue
Block a user