diff --git a/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx b/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx
new file mode 100644
index 00000000000..3e586cf972d
--- /dev/null
+++ b/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx
@@ -0,0 +1,82 @@
+import { Github } from 'lucide-react'
+import Image from 'next/image'
+
+import Panel from 'components/ui/Panel'
+import { useGitHubAuthorizationQuery } from 'data/integrations/github-authorization-query'
+import { BASE_PATH } from 'lib/constants'
+import { openInstallGitHubIntegrationWindow } from 'lib/github'
+import { Badge, Button, cn } from 'ui'
+import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
+
+const AccountConnections = () => {
+ const {
+ data: gitHubAuthorization,
+ isLoading,
+ isSuccess,
+ isError,
+ error,
+ } = useGitHubAuthorizationQuery()
+
+ const isConnected = gitHubAuthorization !== null
+
+ const handleConnect = () => {
+ openInstallGitHubIntegrationWindow('authorize')
+ }
+
+ return (
+
+ Connections
+
+ Connect your Supabase account with other services
+
+
+ }
+ >
+ {isLoading && (
+
+
+
+ )}
+ {isError && (
+
+
+ Failed to load GitHub connection status: {error?.message}
+
+
+ )}
+ {isSuccess && (
+
+
+
+
+
GitHub
+
+ Sync GitHub repos to Supabase projects for automatic branch creation and merging
+
+
+
+
+ {isConnected ? (
+ Connected
+ ) : (
+
+ Connect
+
+ )}
+
+
+ )}
+
+ )
+}
+
+export { AccountConnections }
diff --git a/apps/studio/components/interfaces/BranchManagement/BranchManagement.tsx b/apps/studio/components/interfaces/BranchManagement/BranchManagement.tsx
index 3928bea2096..4c15961ef16 100644
--- a/apps/studio/components/interfaces/BranchManagement/BranchManagement.tsx
+++ b/apps/studio/components/interfaces/BranchManagement/BranchManagement.tsx
@@ -19,12 +19,13 @@ import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
+import { useFlag } from 'hooks/ui/useFlag'
import { useUrlState } from 'hooks/ui/useUrlState'
import { Button } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
import { BranchLoader, BranchManagementSection, BranchRow } from './BranchPanels'
-import CreateBranchModal from './CreateBranchModal'
+import { CreateBranchModal } from './CreateBranchModal'
import {
BranchingEmptyState,
PreviewBranchesEmptyState,
@@ -39,6 +40,7 @@ const BranchManagement = () => {
const { ref } = useParams()
const project = useSelectedProject()
const selectedOrg = useSelectedOrganization()
+ const gitlessBranching = useFlag('gitlessBranching')
const hasBranchEnabled = project?.is_branch_enabled
@@ -217,7 +219,7 @@ const BranchManagement = () => {
/>
)}
- {isSuccessConnections && (
+ {isSuccessConnections && !gitlessBranching && (
@@ -305,6 +307,8 @@ const BranchManagement = () => {
0}
+ githubConnection={githubConnection}
+ gitlessBranching={gitlessBranching}
/>
)}
diff --git a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx
index 84e378c3653..258abac83f3 100644
--- a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx
+++ b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx
@@ -5,8 +5,10 @@ import {
Clock,
ExternalLink,
GitPullRequest,
+ Github,
Infinity,
MoreVertical,
+ Pencil,
RefreshCw,
Shield,
Trash2,
@@ -24,6 +26,7 @@ import { useBranchResetMutation } from 'data/branches/branch-reset-mutation'
import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
import type { Branch } from 'data/branches/branches-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
+import { useFlag } from 'hooks/ui/useFlag'
import {
Badge,
Button,
@@ -37,6 +40,7 @@ import {
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import BranchStatusBadge from './BranchStatusBadge'
+import { EditBranchModal } from './EditBranchModal'
import WorkflowLogs from './WorkflowLogs'
interface BranchManagementSectionProps {
@@ -104,8 +108,10 @@ export const BranchRow = ({
}: BranchRowProps) => {
const { ref: projectRef } = useParams()
const isActive = projectRef === branch?.project_ref
+ const gitlessBranching = useFlag('gitlessBranching')
const canDeleteBranches = useCheckPermissions(PermissionAction.DELETE, 'preview_branches')
+ const canUpdateBranches = useCheckPermissions(PermissionAction.UPDATE, 'preview_branches')
const daysFromNow = dayjs().diff(dayjs(branch.updated_at), 'day')
const formattedTimeFromNow = dayjs(branch.updated_at).fromNow()
@@ -133,6 +139,7 @@ export const BranchRow = ({
const [showConfirmResetModal, setShowConfirmResetModal] = useState(false)
const [showBranchModeSwitch, setShowBranchModeSwitch] = useState(false)
+ const [showEditBranchModal, setShowEditBranchModal] = useState(false)
const { mutate: updateBranch, isLoading: isUpdating } = useBranchUpdateMutation({
onSuccess() {
@@ -178,7 +185,7 @@ export const BranchRow = ({
text: branch.persistent
? `${branch.name} is a persistent branch and will remain active even after the
underlying PR is closed`
- : undefined,
+ : 'Switch to branch',
},
}}
>
@@ -187,6 +194,8 @@ export const BranchRow = ({
+ {branch.git_branch && }
+
{isActive && Current }
) : (
- {branch.pr_number === undefined ? (
+ {branch.git_branch && branch.pr_number === undefined ? (
}>
Create Pull Request
- ) : (
+ ) : branch.pr_number !== undefined ? (
- )}
+ ) : null}
@@ -285,32 +294,60 @@ export const BranchRow = ({
)}
-
-
- setShowBranchModeSwitch(true)}
- onClick={() => setShowBranchModeSwitch(true)}
- disabled={!isBranchActiveHealthy}
+ {branch.git_branch && (
+
+
+ setShowBranchModeSwitch(true)}
+ onClick={() => setShowBranchModeSwitch(true)}
+ disabled={!isBranchActiveHealthy}
+ >
+ {branch.persistent ? (
+ <>
+ Switch to ephemeral
+ >
+ ) : (
+ <>
+ Switch to persistent
+ >
+ )}
+
+
+ {!isBranchActiveHealthy && (
+
+ Branch is still initializing. Please wait for the branch to become healthy
+ before switching modes
+
+ )}
+
+ )}
+
+ {gitlessBranching && (
+
+
- {branch.persistent ? (
- <>
- Switch to ephemeral
- >
- ) : (
- <>
- Switch to persistent
- >
- )}
-
-
- {!isBranchActiveHealthy && (
-
- Branch is still initializing. Please wait for the branch to become healthy
- before switching modes
-
- )}
-
+ setShowEditBranchModal(true)}
+ onClick={() => setShowEditBranchModal(true)}
+ >
+
+ Edit Branch
+
+
+ {(!canUpdateBranches || !isBranchActiveHealthy) && (
+
+ {!canUpdateBranches
+ ? 'You need additional permissions to edit branches'
+ : 'Branch is still initializing. Please wait for the branch to become healthy before editing.'}
+
+ )}
+
+ )}
@@ -373,6 +410,12 @@ export const BranchRow = ({
)}
+
+
setShowEditBranchModal(false)}
+ />
)
}
diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx
index db71fa16422..fd981309aa0 100644
--- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx
+++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx
@@ -1,6 +1,7 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useParams } from 'common'
-import { Check, ExternalLink, Loader2 } from 'lucide-react'
+import { Check, DollarSign, Github, Loader2 } from 'lucide-react'
+import Image from 'next/image'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
@@ -15,32 +16,42 @@ import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-ch
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
+import { useFlag } from 'hooks/ui/useFlag'
+import { BASE_PATH } from 'lib/constants'
+import { sidePanelsState } from 'state/side-panels'
import {
+ Badge,
Button,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogSectionSeparator,
+ DialogTitle,
FormControl_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormMessage_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
- Modal,
+ Label_Shadcn_ as Label,
+ cn,
} from 'ui'
-import { Admonition } from 'ui-patterns'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
interface CreateBranchModalProps {
visible: boolean
onClose: () => void
}
-const CreateBranchModal = ({ visible, onClose }: CreateBranchModalProps) => {
+export const CreateBranchModal = ({ visible, onClose }: CreateBranchModalProps) => {
const { ref } = useParams()
const projectDetails = useSelectedProject()
const selectedOrg = useSelectedOrganization()
+ const gitlessBranching = useFlag('gitlessBranching')
- // [Joshen] There's something weird with RHF that I can't figure out atm
- // but calling form.formState.isValid somehow removes the onBlur check,
- // and makes the validation run onChange instead. This is a workaround
- const [isValid, setIsValid] = useState(false)
+ const [isGitBranchValid, setIsGitBranchValid] = useState(false)
const isBranch = projectDetails?.parent_project_ref !== undefined
const projectRef =
@@ -63,174 +74,233 @@ const CreateBranchModal = ({ visible, onClose }: CreateBranchModalProps) => {
})
const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({
- onSuccess: () => {
- toast.success('Successfully created new branch')
+ onSuccess: (data) => {
+ toast.success(`Successfully created preview branch "${data.name}"`)
onClose()
},
+ onError: (error) => {
+ toast.error(`Failed to create branch: ${error.message}`)
+ },
})
- const githubConnection = connections?.find(
- (connection) => connection.project.ref === projectDetails?.parentRef
- )
+ const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)
const [repoOwner, repoName] = githubConnection?.repository.name.split('/') ?? []
+ const isBranchingEnabled = gitlessBranching || !!githubConnection
+
const formId = 'create-branch-form'
- const FormSchema = z.object({
- branchName: z.string().superRefine(async (val, ctx) => {
- if ((branches ?? []).some((branch) => branch.git_branch === val)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: 'This branch already has a Preview Branch',
- })
- return
- }
-
- if (val.length > 0) {
+ const FormSchema = z
+ .object({
+ branchName: z
+ .string()
+ .min(1, 'Branch name cannot be empty')
+ .refine(
+ (val) => /^[a-zA-Z0-9\-_]+$/.test(val),
+ 'Branch name can only contain alphanumeric characters, hyphens, and underscores.'
+ )
+ .refine(
+ (val) => (branches ?? []).every((branch) => branch.name !== val),
+ 'A branch with this name already exists'
+ ),
+ gitBranchName: z
+ .string()
+ .refine(
+ (val) => !githubConnection?.id || (val && val.length > 0),
+ 'Git branch name is required when Git is connected'
+ ),
+ })
+ .superRefine(async (val, ctx) => {
+ if (val.gitBranchName && val.gitBranchName.length > 0 && githubConnection?.id) {
try {
- if (!githubConnection?.id) throw new Error('No GitHub connection found')
-
await checkGithubBranchValidity({
connectionId: githubConnection.id,
- branchName: val,
+ branchName: val.gitBranchName,
})
- setIsValid(true)
+ setIsGitBranchValid(true)
} catch (error) {
+ setIsGitBranchValid(false)
ctx.addIssue({
code: z.ZodIssueCode.custom,
- message: `Unable to find branch from ${repoOwner}/${repoName}`,
+ message: `Unable to find branch "${val.gitBranchName}" in ${repoOwner}/${repoName}`,
+ path: ['gitBranchName'],
})
- setIsValid(false)
- return
}
+ } else {
+ setIsGitBranchValid(!val.gitBranchName || val.gitBranchName.length === 0)
}
- }),
- })
+ })
+
const form = useForm
>({
mode: 'onBlur',
- reValidateMode: 'onBlur',
+ reValidateMode: 'onChange',
resolver: zodResolver(FormSchema),
- defaultValues: { branchName: '' },
+ defaultValues: { branchName: '', gitBranchName: '' },
})
- const canSubmit = form.getValues('branchName').length > 0 && !isChecking && isValid
+ const isFormValid =
+ form.formState.isValid && (!form.getValues('gitBranchName') || isGitBranchValid)
+ const canSubmit = isFormValid && !isCreating && !isChecking && isBranchingEnabled
+
const onSubmit = (data: z.infer) => {
if (!projectRef) return console.error('Project ref is required')
- createBranch({ projectRef, branchName: data.branchName, gitBranch: data.branchName })
+ createBranch({
+ projectRef,
+ branchName: data.branchName,
+ ...(data.gitBranchName && isGitBranchValid ? { gitBranch: data.gitBranchName } : {}),
+ })
}
useEffect(() => {
if (form && visible) {
- setIsValid(false)
+ setIsGitBranchValid(false)
form.reset()
}
}, [form, visible])
+ useEffect(() => {
+ setIsGitBranchValid(
+ !form.getValues('gitBranchName') || form.getValues('gitBranchName')?.length === 0
+ )
+ }, [githubConnection?.id, form.getValues('gitBranchName')])
+
+ const openLinkerPanel = () => {
+ onClose()
+ sidePanelsState.setGithubConnectionsOpen(true)
+ }
+
return (
-
-
-
+
+
+
+ Each preview branch costs $0.32 per day
+
+
+
+ Cancel
+
+
+ Create branch
+
+
+
+
+
+
+
)
}
-
-export default CreateBranchModal
diff --git a/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx
new file mode 100644
index 00000000000..1f058c0ee99
--- /dev/null
+++ b/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx
@@ -0,0 +1,306 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import { Check, Github, Loader2 } from 'lucide-react'
+import Image from 'next/image'
+import Link from 'next/link'
+import { useEffect, useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import * as z from 'zod'
+
+import { useParams } from 'common'
+import AlertError from 'components/ui/AlertError'
+import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
+import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
+import { Branch, useBranchesQuery } from 'data/branches/branches-query'
+import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-check-query'
+import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
+import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
+import { useSelectedProject } from 'hooks/misc/useSelectedProject'
+import { BASE_PATH } from 'lib/constants'
+import { sidePanelsState } from 'state/side-panels'
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogSectionSeparator,
+ DialogTitle,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ FormItem_Shadcn_,
+ FormMessage_Shadcn_,
+ Form_Shadcn_,
+ Input_Shadcn_,
+ Label_Shadcn_ as Label,
+ cn,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+
+interface EditBranchModalProps {
+ branch?: Branch
+ visible: boolean
+ onClose: () => void
+}
+
+export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalProps) => {
+ const { ref } = useParams()
+ const projectDetails = useSelectedProject()
+ const selectedOrg = useSelectedOrganization()
+
+ const [isGitBranchValid, setIsGitBranchValid] = useState(false)
+
+ const isBranch = projectDetails?.parent_project_ref !== undefined
+ const projectRef =
+ projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
+
+ const {
+ data: connections,
+ error: connectionsError,
+ isLoading: isLoadingConnections,
+ isSuccess: isSuccessConnections,
+ isError: isErrorConnections,
+ } = useGitHubConnectionsQuery({
+ organizationId: selectedOrg?.id,
+ })
+
+ const { data: branches } = useBranchesQuery({ projectRef })
+ const { mutateAsync: checkGithubBranchValidity, isLoading: isChecking } =
+ useCheckGithubBranchValidity({
+ onError: () => {},
+ })
+
+ const { mutate: updateBranch, isLoading: isUpdating } = useBranchUpdateMutation({
+ onSuccess: (data) => {
+ toast.success(`Successfully updated branch "${data.name}"`)
+ onClose()
+ },
+ onError: (error) => {
+ toast.error(`Failed to update branch: ${error.message}`)
+ },
+ })
+
+ const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)
+ const [repoOwner, repoName] = githubConnection?.repository.name.split('/') ?? []
+
+ const formId = 'edit-branch-form'
+ const FormSchema = z
+ .object({
+ branchName: z
+ .string()
+ .min(1, 'Branch name cannot be empty')
+ .refine(
+ (val) => /^[a-zA-Z0-9\-_]+$/.test(val),
+ 'Branch name can only contain alphanumeric characters, hyphens, and underscores.'
+ )
+ .refine(
+ (val) =>
+ // Allow the current branch name during edit
+ val === branch?.name || (branches ?? []).every((b) => b.name !== val),
+ 'A branch with this name already exists'
+ ),
+ gitBranchName: z.string().optional(),
+ })
+ .superRefine(async (val, ctx) => {
+ if (val.gitBranchName && val.gitBranchName.length > 0 && githubConnection?.id) {
+ try {
+ await checkGithubBranchValidity({
+ connectionId: githubConnection.id,
+ branchName: val.gitBranchName,
+ })
+ setIsGitBranchValid(true)
+ } catch (error) {
+ setIsGitBranchValid(false)
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `Unable to find branch "${val.gitBranchName}" in ${repoOwner}/${repoName}`,
+ path: ['gitBranchName'],
+ })
+ }
+ } else {
+ // If git branch is empty or removed, it's valid
+ setIsGitBranchValid(!val.gitBranchName || val.gitBranchName.length === 0)
+ }
+ })
+
+ const form = useForm>({
+ mode: 'onBlur',
+ reValidateMode: 'onChange',
+ resolver: zodResolver(FormSchema),
+ defaultValues: { branchName: '', gitBranchName: '' },
+ })
+
+ const isFormValid =
+ form.formState.isValid && (!form.getValues('gitBranchName') || isGitBranchValid)
+ const canSubmit = isFormValid && !isUpdating && !isChecking
+
+ const onSubmit = (data: z.infer) => {
+ if (!projectRef) return console.error('Project ref is required')
+ if (!branch?.id) return console.error('Branch ID is required')
+
+ const payload: {
+ projectRef: string
+ id: string
+ branchName: string
+ gitBranch?: string
+ } = {
+ projectRef,
+ id: branch.id,
+ branchName: data.branchName,
+ }
+
+ // Only add gitBranch to the payload if it is present and valid
+ // If gitBranchName is empty or invalid, gitBranch remains undefined in the payload
+ if (data.gitBranchName && isGitBranchValid) {
+ payload.gitBranch = data.gitBranchName
+ }
+
+ updateBranch(payload)
+ }
+
+ // Pre-fill form when the modal becomes visible and branch data is available
+ useEffect(() => {
+ if (visible && branch) {
+ setIsGitBranchValid(!!branch.git_branch) // Initial validity based on existing link
+ form.reset({
+ branchName: branch.name ?? '',
+ gitBranchName: branch.git_branch ?? '',
+ })
+ }
+ }, [branch, visible, form])
+
+ // Handle initial state and changes for git branch validity
+ useEffect(() => {
+ setIsGitBranchValid(
+ !form.getValues('gitBranchName') || form.getValues('gitBranchName')?.length === 0
+ )
+ // Trigger validation if a git branch name exists initially or is entered
+ if (form.getValues('gitBranchName')) {
+ form.trigger('gitBranchName')
+ }
+ }, [githubConnection?.id, form.getValues('gitBranchName'), form.trigger, visible, branch])
+
+ const openLinkerPanel = () => {
+ onClose()
+ sidePanelsState.setGithubConnectionsOpen(true)
+ }
+
+ return (
+ !open && onClose()}>
+
+
+ Edit branch "{branch?.name}" {/* Update title */}
+
+
+
+
+
+ (
+
+
+
+
+
+ )}
+ />
+
+ {githubConnection && (
+ (
+
+
+
Link to Git Branch (Optional)
+
+
+
+ {repoOwner}/{repoName}
+
+
+
+
+
+
+
+
+ {isChecking && }
+ {field.value && !isChecking && isGitBranchValid && (
+
+ )}
+
+
+
+ If linked, migrations from this Git branch will be automatically deployed.
+
+
+
+ )}
+ />
+ )}
+ {isLoadingConnections && }
+ {isErrorConnections && (
+
+ )}
+ {isSuccessConnections && !githubConnection && (
+
+
+
GitHub Repository
+
+ Optionally connect to a GitHub repository to manage migrations automatically
+ for this branch.
+
+
+
} onClick={openLinkerPanel}>
+ Connect to GitHub
+
+
+ )}
+
+
+
+
+ Cancel
+
+
+ Update branch
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/BranchManagement/EmptyStates.tsx b/apps/studio/components/interfaces/BranchManagement/EmptyStates.tsx
index 264af8aa091..329ebaeeb70 100644
--- a/apps/studio/components/interfaces/BranchManagement/EmptyStates.tsx
+++ b/apps/studio/components/interfaces/BranchManagement/EmptyStates.tsx
@@ -1,11 +1,12 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { ExternalLink, GitBranch, GitPullRequest } from 'lucide-react'
+import { ExternalLink, GitBranch, GitPullRequest, Github } from 'lucide-react'
import Link from 'next/link'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAppStateSnapshot } from 'state/app-state'
+import { sidePanelsState } from 'state/side-panels'
import { Button } from 'ui'
export const BranchingEmptyState = () => {
@@ -55,10 +56,35 @@ export const BranchingEmptyState = () => {
export const PullRequestsEmptyState = ({
url,
hasBranches,
+ githubConnection,
+ gitlessBranching,
}: {
url: string
hasBranches: boolean
+ githubConnection?: any
+ gitlessBranching?: boolean
}) => {
+ // Show GitHub connection message if GitHub is not connected and gitless branching is enabled
+ if (!githubConnection && gitlessBranching) {
+ return (
+
+
+
Connect to GitHub for seamless branching
+
+ Sync GitHub repos to Supabase projects for automatic branch creation and merging
+
+
+
}
+ onClick={() => sidePanelsState.setGithubConnectionsOpen(true)}
+ >
+ Connect to GitHub
+
+
+ )
+ }
+
return (
No pull requests made yet for this repository
@@ -106,36 +132,24 @@ export const PreviewBranchesEmptyState = ({
}) => {
return (
-
No database preview branches
-
Database preview branches will be shown here
-
-
-
-
-
-
Create a preview branch
-
Start developing in preview
-
-
-
onSelectCreateBranch()}>
- Create preview branch
-
-
-
-
-
Not sure what to do?
-
Browse our documentation
-
-
}>
-
- Docs
-
-
-
+
Create your first preview branch
+
+ Preview branches are used to experiment with changes to your database schema in a safe,
+ non-destructive environment.
+
+
+ }>
+
+ Docs
+
+
+ onSelectCreateBranch()}>
+ Create your first branch
+
)
diff --git a/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationConnection.tsx b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationConnection.tsx
index 93bcb2eed06..a89becda650 100644
--- a/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationConnection.tsx
+++ b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationConnection.tsx
@@ -154,18 +154,10 @@ const IntegrationConnectionItem = forwardRef
- This action cannot be undone. Are you sure you want to delete this {type} connection?
+ Deleting this GitHub connection will stop automatic creation and merging of preview
+ branches. Existing preview branches will remain unchanged.
>
diff --git a/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice.tsx b/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice.tsx
index a314423b39c..b7e0a95ea7a 100644
--- a/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice.tsx
+++ b/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice.tsx
@@ -1,13 +1,14 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { Clock } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAppStateSnapshot } from 'state/app-state'
-import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
+import { Button } from 'ui'
-const BranchingPITRNotice = () => {
+export const BranchingPITRNotice = () => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
@@ -17,41 +18,44 @@ const BranchingPITRNotice = () => {
)
return (
-
-
- We strongly encourage enabling Point in Time Recovery (PITR)
-
-
- This is to ensure that you can always recover data if you make a "bad migration". For
- example, if you accidentally delete a column or some of your production data.
-
- {!canUpdateSubscription ? (
-
- Enable PITR add-on
-
- ) : (
-
- snap.setShowEnableBranchingModal(false)}
+
+
+
+
+
+
+
+
+
Consider enabling Point in Time Recovery (PITR)
+
+ This ensures you can recover data if you make a bad migration (e.g. delete a column).
+
+
+ {!canUpdateSubscription ? (
+
Enable PITR add-on
-
-
- )}
-
+
+ ) : (
+
+ snap.setShowEnableBranchingModal(false)}
+ >
+ Enable PITR
+
+
+ )}
+
+
)
}
-
-export default BranchingPITRNotice
diff --git a/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPlanNotice.tsx b/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPlanNotice.tsx
index 6d671823230..714b3e038ab 100644
--- a/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPlanNotice.tsx
+++ b/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPlanNotice.tsx
@@ -5,12 +5,12 @@ import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useAppStateSnapshot } from 'state/app-state'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
-const BranchingPlanNotice = () => {
+export const BranchingPlanNotice = () => {
const snap = useAppStateSnapshot()
const selectedOrg = useSelectedOrganization()
return (
-
+
Database branching is only available on the Pro Plan and above
@@ -32,5 +32,3 @@ const BranchingPlanNotice = () => {
)
}
-
-export default BranchingPlanNotice
diff --git a/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPostgresVersionNotice.tsx b/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPostgresVersionNotice.tsx
index cc70fbd08cf..f1b1ef709d1 100644
--- a/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPostgresVersionNotice.tsx
+++ b/apps/studio/components/layouts/AppLayout/EnableBranchingButton/BranchingPostgresVersionNotice.tsx
@@ -1,11 +1,11 @@
-import { useParams } from 'common'
+import { AlertCircleIcon } from 'lucide-react'
import Link from 'next/link'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
-import { AlertCircleIcon } from 'lucide-react'
+import { useParams } from 'common'
import { useAppStateSnapshot } from 'state/app-state'
-const BranchingPostgresVersionNotice = () => {
+export const BranchingPostgresVersionNotice = () => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
@@ -32,5 +32,3 @@ const BranchingPostgresVersionNotice = () => {
)
}
-
-export default BranchingPostgresVersionNotice
diff --git a/apps/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx b/apps/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx
index 251cf2c2cae..d0f59229663 100644
--- a/apps/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx
+++ b/apps/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx
@@ -1,11 +1,14 @@
import { zodResolver } from '@hookform/resolvers/zod'
-import { useParams } from 'common'
import { last } from 'lodash'
+import { Check, DollarSign, ExternalLink, FileText, GitBranch, Github, Loader2 } from 'lucide-react'
+import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'
+import { useQueryClient } from '@tanstack/react-query'
+import { useParams } from 'common'
import SidePanelGitHubRepoLinker from 'components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoLinker'
import AlertError from 'components/ui/AlertError'
import { DocsButton } from 'components/ui/DocsButton'
@@ -13,27 +16,49 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-check-query'
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
+import { projectKeys } from 'data/projects/keys'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
-import { DollarSign, FileText, GitBranch } from 'lucide-react'
+import { useFlag } from 'hooks/ui/useFlag'
+import { useRouter } from 'next/router'
import { useAppStateSnapshot } from 'state/app-state'
-import { Button, Form_Shadcn_, Modal } from 'ui'
-import BranchingPITRNotice from './BranchingPITRNotice'
-import BranchingPlanNotice from './BranchingPlanNotice'
-import BranchingPostgresVersionNotice from './BranchingPostgresVersionNotice'
-import GithubRepositorySelection from './GithubRepositorySelection'
+import { sidePanelsState } from 'state/side-panels'
+import {
+ Badge,
+ Button,
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogSectionSeparator,
+ DialogTitle,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ FormItem_Shadcn_,
+ FormMessage_Shadcn_,
+ Input_Shadcn_,
+ Label_Shadcn_ as Label,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { BranchingPITRNotice } from './BranchingPITRNotice'
+import { BranchingPlanNotice } from './BranchingPlanNotice'
+import { BranchingPostgresVersionNotice } from './BranchingPostgresVersionNotice'
-const EnableBranchingModal = () => {
+export const EnableBranchingModal = () => {
+ const router = useRouter()
const { ref } = useParams()
const snap = useAppStateSnapshot()
+ const queryClient = useQueryClient()
+ const project = useSelectedProject()
const selectedOrg = useSelectedOrganization()
+ const gitlessBranching = useFlag('gitlessBranching')
- // [Joshen] There's something weird with RHF that I can't figure out atm
- // but calling form.formState.isValid somehow removes the onBlur check,
- // and makes the validation run onChange instead. This is a workaround
- const [isValid, setIsValid] = useState(false)
+ const [isGitBranchValid, setIsGitBranchValid] = useState(false)
const {
data: connections,
@@ -48,7 +73,6 @@ const EnableBranchingModal = () => {
{ enabled: snap.showEnableBranchingModal }
)
- const project = useSelectedProject()
const hasMinimumPgVersion =
Number(last(project?.dbVersion?.split('-') ?? [])?.split('.')[0] ?? 0) >= 15
@@ -66,195 +90,321 @@ const EnableBranchingModal = () => {
useCheckGithubBranchValidity({ onError: () => {} })
const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({
- onSuccess: () => {
- toast.success(`Successfully created new branch`)
+ onSuccess: async () => {
+ await Promise.all([
+ queryClient.invalidateQueries(projectKeys.detail(ref)),
+ queryClient.invalidateQueries(projectKeys.list()),
+ ])
+ toast.success(`Successfully enabled branching`)
snap.setShowEnableBranchingModal(false)
+ router.push(`/project/${ref}/branches`)
+ },
+ onError: (error) => {
+ toast.error(`Failed to enable branching: ${error.message}`)
},
})
const formId = 'enable-branching-form'
- const FormSchema = z.object({
- branchName: z
- .string()
- .refine((val) => val.length > 1, `Please enter a branch name from ${repoOwner}/${repoName}`)
- .refine(async (val) => {
- try {
- if (val.length > 0) {
- if (!githubConnection?.id) {
- throw new Error('No GitHub connection found')
- }
+ const FormSchema = z
+ .object({
+ productionBranchName: z
+ .string()
+ .min(1, 'Production branch name cannot be empty')
+ .refine(
+ (val) => /^[a-zA-Z0-9\-_]+$/.test(val),
+ 'Branch name can only contain alphanumeric characters, hyphens, and underscores.'
+ ),
+ branchName: z.string().optional(),
+ })
+ .superRefine(async (val, ctx) => {
+ if (githubConnection && (!val.branchName || val.branchName.length === 0)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'GitHub branch is required when a repository is connected.',
+ path: ['branchName'],
+ })
+ setIsGitBranchValid(false)
+ return
+ }
- await checkGithubBranchValidity({
- connectionId: githubConnection.id,
- branchName: val,
- })
- setIsValid(true)
- }
- return true
+ if (githubConnection && val.branchName && val.branchName.length > 0) {
+ try {
+ await checkGithubBranchValidity({
+ connectionId: githubConnection.id,
+ branchName: val.branchName,
+ })
+ setIsGitBranchValid(true)
} catch (error) {
- setIsValid(false)
- return false
+ setIsGitBranchValid(false)
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `Unable to find branch "${val.branchName}" in ${repoOwner}/${repoName}`,
+ path: ['branchName'],
+ })
}
- }, `Unable to find branch from ${repoOwner}/${repoName}`),
- })
+ } else {
+ setIsGitBranchValid(true)
+ }
+ })
+
const form = useForm>({
mode: 'onBlur',
reValidateMode: 'onChange',
resolver: zodResolver(FormSchema),
- defaultValues: { branchName: '' },
+ defaultValues: { productionBranchName: 'main', branchName: '' },
})
const isLoading = isLoadingConnections
const isError = isErrorConnections
const isSuccess = isSuccessConnections
- const canSubmit = form.getValues('branchName').length > 0 && !isChecking && isValid
+ const isFormValid = form.formState.isValid
+ const canSubmit =
+ isFormValid && !isCreating && !isChecking && (gitlessBranching || !!githubConnection)
+
const onSubmit = (data: z.infer) => {
if (!ref) return console.error('Project ref is required')
- createBranch({ projectRef: ref, branchName: data.branchName, gitBranch: data.branchName })
+ createBranch({
+ projectRef: ref,
+ branchName: data.productionBranchName,
+ ...(data.branchName && isGitBranchValid ? { gitBranch: data.branchName } : {}),
+ })
+ }
+
+ const openLinkerPanel = () => {
+ snap.setShowEnableBranchingModal(false)
+ sidePanelsState.setGithubConnectionsOpen(true)
}
useEffect(() => {
- if (form && snap.showEnableBranchingModal) {
- setIsValid(false)
- form.reset()
+ if (snap.showEnableBranchingModal) {
+ form.reset({ productionBranchName: 'main', branchName: '' })
+ setIsGitBranchValid(false)
}
}, [form, snap.showEnableBranchingModal])
+ useEffect(() => {
+ setIsGitBranchValid(!form.getValues('branchName') || form.getValues('branchName')?.length === 0)
+ }, [githubConnection?.id, form.getValues('branchName')])
+
return (
<>
- snap.setShowEnableBranchingModal(false)}
- className="block"
- size="medium"
- hideClose
+ !open && snap.setShowEnableBranchingModal(false)}
>
-
- setIsValid(false)}
- >
-
-
-
-
-
Enable database branching
-
Manage environments in Supabase
+
+
+
+
+
+
+
+
+ Enable database branching
+ Manage environments in Supabase
+
+
+
-
-
-
+
- {isLoading && (
- <>
-
-
-
-
-
- >
- )}
- {isError && (
- <>
-
-
- {isErrorConnections ? (
+ {isLoading && (
+
+
+
+
+
+
+
+ )}
+ {isError && (
+
+
+
- ) : null}
-
-
- >
- )}
- {isSuccess && (
- <>
- {isFreePlan ? (
-
- ) : !hasMinimumPgVersion ? (
-
- ) : (
- <>
-
- {!hasPitrEnabled && }
- >
- )}
-
-
- Please keep in mind the following:
-
-
-
-
-
-
-
-
-
- Preview branches are billed $0.32 per day
-
-
- This cost will continue for as long as the branch has not been removed.
-
-
-
-
-
-
-
-
-
-
-
- Migrations are applied from your GitHub repository
-
-
- Migration files in your ./supabase{' '}
- directory will run on both Preview Branches and Production when pushing and
- merging branches.
-
-
-
-
-
- >
- )}
+
+
+
+ )}
+ {isSuccess && (
+ <>
+ {isFreePlan ? (
+
+
+
+ ) : !hasMinimumPgVersion ? (
+
+
+
+ ) : (
+ <>
+
+
+ (
+
+
+
+
+
+ )}
+ />
+ {githubConnection ? (
+ <>
+ (
+
+
+
+ Link GitHub Branch{' '}
+ {githubConnection && (
+ *
+ )}
+
+
+
+
+ {repoOwner}/{repoName}
+
+
+
+
+
+
+
+
+
+
+ {isChecking && }
+ {field.value && !isChecking && isGitBranchValid && (
+
+ )}
+
+
+
+ )}
+ />
+ >
+ ) : (
+
+
+
+ GitHub Repository
+ {!gitlessBranching && (
+
+ Required
+
+ )}
+
+
+ {gitlessBranching
+ ? 'Optionally connect to a GitHub repository to enable deploying previews on Pull Requests and manage migrations automatically.'
+ : 'Connect to a GitHub repository to enable database branching. This allows you to deploy previews on Pull Requests and manage migrations automatically.'}
+
+
+
} onClick={openLinkerPanel}>
+ Connect to GitHub
+
+
+ )}
+
+ >
+ )}
+
-
- snap.setShowEnableBranchingModal(false)}
- >
- Cancel
-
-
- I understand, enable branching
-
-
-
-
-
+
+ Please keep in mind the following:
+
+ {githubConnection && (
+
+
+
+
+
+
+
+
+ Migrations are applied from your GitHub repository
+
+
+ Migration files in your ./supabase{' '}
+ directory will run on both Preview Branches and Production when pushing
+ and merging branches.
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ Preview branches are billed $0.32 per day
+
+
+ This cost will continue for as long as the branch has not been removed.
+
+
+
+ {!hasPitrEnabled && }
+
+
+ >
+ )}
+
+
+ snap.setShowEnableBranchingModal(false)}
+ >
+ Cancel
+
+
+ I understand, enable branching
+
+
+
+
+
+
>
)
}
-
-export default EnableBranchingModal
diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx
index 7f1b25d8ed5..8cc5e099791 100644
--- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx
@@ -23,7 +23,7 @@ import { useAppStateSnapshot } from 'state/app-state'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav'
-import EnableBranchingModal from '../AppLayout/EnableBranchingButton/EnableBranchingModal'
+import { EnableBranchingModal } from '../AppLayout/EnableBranchingButton/EnableBranchingModal'
import { useEditorType } from '../editors/EditorsLayout.hooks'
import BuildingState from './BuildingState'
import ConnectingState from './ConnectingState'
diff --git a/apps/studio/data/branches/branch-create-mutation.ts b/apps/studio/data/branches/branch-create-mutation.ts
index 874f0bf102f..d32400c906a 100644
--- a/apps/studio/data/branches/branch-create-mutation.ts
+++ b/apps/studio/data/branches/branch-create-mutation.ts
@@ -2,7 +2,6 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react
import { toast } from 'sonner'
import { handleError, post } from 'data/fetchers'
-import { projectKeys } from 'data/projects/keys'
import type { ResponseError } from 'types'
import { branchKeys } from './keys'
@@ -51,8 +50,6 @@ export const useBranchCreateMutation = ({
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(branchKeys.list(projectRef))
- await queryClient.invalidateQueries(projectKeys.detail(projectRef))
- await queryClient.invalidateQueries(projectKeys.list())
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
diff --git a/apps/studio/pages/account/me.tsx b/apps/studio/pages/account/me.tsx
index 5b09d1a7998..d842fba7788 100644
--- a/apps/studio/pages/account/me.tsx
+++ b/apps/studio/pages/account/me.tsx
@@ -1,6 +1,7 @@
import { AccountDeletion } from 'components/interfaces/Account/Preferences/AccountDeletion'
import { AccountIdentities } from 'components/interfaces/Account/Preferences/AccountIdentities'
import { AnalyticsSettings } from 'components/interfaces/Account/Preferences/AnalyticsSettings'
+import { AccountConnections } from 'components/interfaces/Account/Preferences/AccountConnections'
import { ProfileInformation } from 'components/interfaces/Account/Preferences/ProfileInformation'
import { ThemeSettings } from 'components/interfaces/Account/Preferences/ThemeSettings'
import AccountLayout from 'components/layouts/AccountLayout/AccountLayout'
@@ -60,6 +61,10 @@ const ProfileCard = () => {
>
)}
+
+