mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 14:19:11 +03:00
* remove enable branching modal * re-add github linker * abstract away enable and disable * toggle fixes * update logic to lean on connection status * update form logic * sheet layout * gitless flag * fix side panel size * copy changes * remove import * add cost * allow connection details on create * Fix TS issues * Fix TS issues * Couple of clean ups * Revert hardcode in useFlag * Fix TS * layout issues and github check * refactor * refactor to use new field * cleanup * style * clarification with github integration * replace branch dropdown button * update repo picker * remove modal * change defaults * disable if not gitless and no connection * fixes * prevent editing on child branch --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Kevin Grüneberg <k.grueneberg1994@gmail.com>
352 lines
14 KiB
TypeScript
352 lines
14 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { partition } from 'lodash'
|
|
import { MessageCircle } from 'lucide-react'
|
|
import { useRouter } from 'next/router'
|
|
import { useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import { useParams } from 'common'
|
|
import { GitHubStatus } from 'components/interfaces/Settings/Integrations/GithubIntegration/GitHubStatus'
|
|
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
|
import AlertError from 'components/ui/AlertError'
|
|
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
|
import { DocsButton } from 'components/ui/DocsButton'
|
|
import NoPermission from 'components/ui/NoPermission'
|
|
import { useBranchDeleteMutation } from 'data/branches/branch-delete-mutation'
|
|
import { useBranchesDisableMutation } from 'data/branches/branches-disable-mutation'
|
|
import { Branch, useBranchesQuery } from 'data/branches/branches-query'
|
|
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
|
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 { useAppStateSnapshot } from 'state/app-state'
|
|
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 { PreviewBranchesEmptyState, PullRequestsEmptyState } from './EmptyStates'
|
|
import { Overview } from './Overview'
|
|
|
|
type Tab = 'overview' | 'prs' | 'branches'
|
|
|
|
const BranchManagement = () => {
|
|
const router = useRouter()
|
|
const { ref } = useParams()
|
|
const project = useSelectedProject()
|
|
const selectedOrg = useSelectedOrganization()
|
|
const gitlessBranching = useFlag('gitlessBranching')
|
|
|
|
const hasBranchEnabled = project?.is_branch_enabled
|
|
|
|
const isBranch = project?.parent_project_ref !== undefined
|
|
const projectRef =
|
|
project !== undefined ? (isBranch ? project.parent_project_ref : ref) : undefined
|
|
|
|
const [urlParams, setParams] = useUrlState<{ tab: Tab }>()
|
|
const tab = urlParams.tab ?? 'overview'
|
|
const setTab = (tab: Tab) => setParams({ tab })
|
|
|
|
const snap = useAppStateSnapshot()
|
|
|
|
const [showDisableBranching, setShowDisableBranching] = useState(false)
|
|
const [selectedBranchToDelete, setSelectedBranchToDelete] = useState<Branch>()
|
|
|
|
const canReadBranches = useCheckPermissions(PermissionAction.READ, 'preview_branches')
|
|
const canCreateBranches = useCheckPermissions(PermissionAction.CREATE, 'preview_branches', {
|
|
resource: { is_default: false },
|
|
})
|
|
|
|
const {
|
|
data: connections,
|
|
error: connectionsError,
|
|
isLoading: isLoadingConnections,
|
|
isSuccess: isSuccessConnections,
|
|
isError: isErrorConnections,
|
|
} = useGitHubConnectionsQuery({
|
|
organizationId: selectedOrg?.id,
|
|
})
|
|
|
|
const {
|
|
data: branches,
|
|
error: branchesError,
|
|
isLoading: isLoadingBranches,
|
|
isError: isErrorBranches,
|
|
isSuccess: isSuccessBranches,
|
|
} = useBranchesQuery({ projectRef })
|
|
const [[mainBranch], previewBranchesUnsorted] = partition(branches, (branch) => branch.is_default)
|
|
const previewBranches = previewBranchesUnsorted.sort((a, b) =>
|
|
new Date(a.updated_at) < new Date(b.updated_at) ? 1 : -1
|
|
)
|
|
const branchesWithPRs = previewBranches.filter((branch) => branch.pr_number !== undefined)
|
|
|
|
const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)
|
|
const repo = githubConnection?.repository.name ?? ''
|
|
|
|
const isError = isErrorConnections || isErrorBranches
|
|
const isLoading = isLoadingConnections || isLoadingBranches
|
|
const isSuccess = isSuccessConnections && isSuccessBranches
|
|
|
|
const { mutate: deleteBranch, isLoading: isDeleting } = useBranchDeleteMutation({
|
|
onSuccess: () => {
|
|
if (selectedBranchToDelete?.project_ref === ref) {
|
|
toast.success(
|
|
'Successfully deleted branch. You are now currently on the main branch of your project.'
|
|
)
|
|
router.push(`/project/${projectRef}/branches`)
|
|
} else {
|
|
toast.success('Successfully deleted branch')
|
|
}
|
|
setSelectedBranchToDelete(undefined)
|
|
},
|
|
})
|
|
|
|
const { mutate: disableBranching, isLoading: isDisabling } = useBranchesDisableMutation({
|
|
onSuccess: () => {
|
|
toast.success('Successfully disabled branching for project')
|
|
setShowDisableBranching(false)
|
|
},
|
|
})
|
|
|
|
const generateCreatePullRequestURL = (branch?: string) => {
|
|
if (githubConnection === undefined) return 'https://github.com'
|
|
|
|
return branch !== undefined
|
|
? `https://github.com/${githubConnection.repository.name}/compare/${mainBranch?.git_branch}...${branch}`
|
|
: `https://github.com/${githubConnection.repository.name}/compare`
|
|
}
|
|
|
|
const onConfirmDeleteBranch = () => {
|
|
if (selectedBranchToDelete == undefined) return console.error('No branch selected')
|
|
if (projectRef == undefined) return console.error('Project ref is required')
|
|
deleteBranch({ id: selectedBranchToDelete?.id, projectRef })
|
|
}
|
|
|
|
const onConfirmDisableBranching = () => {
|
|
if (projectRef == undefined) return console.error('Project ref is required')
|
|
if (!previewBranches) return console.error('No branches available')
|
|
disableBranching({ projectRef, branchIds: previewBranches?.map((branch) => branch.id) })
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<ScaffoldContainer>
|
|
<ScaffoldSection>
|
|
<div className="col-span-12">
|
|
<h3 className="text-xl mb-8">Branches</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center -space-x-px">
|
|
<Button
|
|
type="default"
|
|
className={`rounded-r-none transition hover:opacity-90 ${
|
|
tab === 'overview' ? 'opacity-100' : 'opacity-60'
|
|
}`}
|
|
onClick={() => setTab('overview')}
|
|
>
|
|
Overview
|
|
</Button>
|
|
<Button
|
|
type="default"
|
|
className={`rounded-none transition hover:opacity-90 ${
|
|
tab === 'prs' ? 'opacity-100' : 'opacity-60'
|
|
}`}
|
|
onClick={() => setTab('prs')}
|
|
>
|
|
Pull requests
|
|
</Button>
|
|
<Button
|
|
type="default"
|
|
className={`rounded-l-none transition hover:opacity-90 ${
|
|
tab === 'branches' ? 'opacity-100' : 'opacity-60'
|
|
}`}
|
|
onClick={() => setTab('branches')}
|
|
>
|
|
All branches
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-x-2">
|
|
<Button
|
|
asChild
|
|
type="text"
|
|
icon={<MessageCircle className="text-muted" strokeWidth={1} />}
|
|
>
|
|
<a
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
href="https://github.com/orgs/supabase/discussions/18937"
|
|
>
|
|
Branching Feedback
|
|
</a>
|
|
</Button>
|
|
<DocsButton href="https://supabase.com/docs/guides/platform/branching" />
|
|
<ButtonTooltip
|
|
type="primary"
|
|
disabled={!canCreateBranches}
|
|
onClick={() => snap.setShowCreateBranchModal(true)}
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
text: !canCreateBranches
|
|
? 'You need additional permissions to create branches'
|
|
: undefined,
|
|
},
|
|
}}
|
|
>
|
|
Create branch
|
|
</ButtonTooltip>
|
|
</div>
|
|
</div>
|
|
|
|
{!canReadBranches ? (
|
|
<NoPermission resourceText="view this project's branches" />
|
|
) : (
|
|
<>
|
|
{isErrorConnections && (
|
|
<AlertError
|
|
error={connectionsError}
|
|
subject="Failed to retrieve GitHub integration connection"
|
|
/>
|
|
)}
|
|
|
|
{isSuccessConnections && hasBranchEnabled && <GitHubStatus />}
|
|
|
|
{isErrorBranches && tab === 'overview' && (
|
|
<AlertError
|
|
error={branchesError}
|
|
subject="Failed to retrieve preview branches"
|
|
/>
|
|
)}
|
|
|
|
{!isError && (
|
|
<>
|
|
{tab === 'overview' && (
|
|
<Overview
|
|
isLoading={isLoading}
|
|
isSuccess={isSuccess}
|
|
repo={repo}
|
|
mainBranch={mainBranch}
|
|
previewBranches={previewBranches}
|
|
onViewAllBranches={() => setTab('branches')}
|
|
onSelectCreateBranch={() => snap.setShowCreateBranchModal(true)}
|
|
onSelectDeleteBranch={setSelectedBranchToDelete}
|
|
generateCreatePullRequestURL={generateCreatePullRequestURL}
|
|
showProductionBranch={hasBranchEnabled}
|
|
/>
|
|
)}
|
|
{tab === 'prs' && (
|
|
<BranchManagementSection
|
|
header={`${branchesWithPRs.length} branches with pull requests found`}
|
|
>
|
|
{branchesWithPRs.length > 0 ? (
|
|
branchesWithPRs.map((branch) => {
|
|
return (
|
|
<BranchRow
|
|
key={branch.id}
|
|
repo={repo}
|
|
branch={branch}
|
|
generateCreatePullRequestURL={generateCreatePullRequestURL}
|
|
onSelectDeleteBranch={() => setSelectedBranchToDelete(branch)}
|
|
/>
|
|
)
|
|
})
|
|
) : (
|
|
<PullRequestsEmptyState
|
|
url={generateCreatePullRequestURL()}
|
|
hasBranches={previewBranches.length > 0}
|
|
githubConnection={githubConnection}
|
|
gitlessBranching={gitlessBranching}
|
|
/>
|
|
)}
|
|
</BranchManagementSection>
|
|
)}
|
|
{tab === 'branches' && (
|
|
<BranchManagementSection
|
|
header={`${previewBranches.length} branches found`}
|
|
>
|
|
{isLoadingBranches && <BranchLoader />}
|
|
{isErrorBranches && (
|
|
<AlertError
|
|
error={branchesError}
|
|
subject="Failed to retrieve preview branches"
|
|
/>
|
|
)}
|
|
{isSuccessBranches && previewBranches.length === 0 && (
|
|
<PreviewBranchesEmptyState
|
|
onSelectCreateBranch={() => snap.setShowCreateBranchModal(true)}
|
|
/>
|
|
)}
|
|
{isSuccessBranches &&
|
|
previewBranches.map((branch) => {
|
|
return (
|
|
<BranchRow
|
|
key={branch.id}
|
|
repo={repo}
|
|
branch={branch}
|
|
generateCreatePullRequestURL={generateCreatePullRequestURL}
|
|
onSelectDeleteBranch={() => setSelectedBranchToDelete(branch)}
|
|
/>
|
|
)
|
|
})}
|
|
</BranchManagementSection>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ScaffoldSection>
|
|
</ScaffoldContainer>
|
|
|
|
<TextConfirmModal
|
|
variant={'warning'}
|
|
visible={selectedBranchToDelete !== undefined}
|
|
onCancel={() => setSelectedBranchToDelete(undefined)}
|
|
onConfirm={() => onConfirmDeleteBranch()}
|
|
loading={isDeleting}
|
|
title="Delete branch"
|
|
confirmLabel="Delete branch"
|
|
confirmPlaceholder="Type in name of branch"
|
|
confirmString={selectedBranchToDelete?.name ?? ''}
|
|
alert={{ title: 'You cannot recover this branch once deleted' }}
|
|
text={
|
|
<>
|
|
This will delete your database preview branch{' '}
|
|
<span className="text-bold text-foreground">{selectedBranchToDelete?.name}</span>.
|
|
</>
|
|
}
|
|
/>
|
|
|
|
<ConfirmationModal
|
|
variant={'destructive'}
|
|
size="medium"
|
|
loading={isDisabling}
|
|
visible={showDisableBranching}
|
|
title="Confirm disable branching for project"
|
|
confirmLabel="Confirm disable branching"
|
|
confirmLabelLoading="Disabling branching..."
|
|
onConfirm={() => onConfirmDisableBranching()}
|
|
onCancel={() => setShowDisableBranching(false)}
|
|
alert={{
|
|
title: 'This action cannot be undone',
|
|
description:
|
|
'All database preview branches will be removed upon disabling branching. You may still re-enable branching again thereafter, but your existing preview branches will not be restored.',
|
|
}}
|
|
>
|
|
<p className="text-sm">Before you disable branching, consider:</p>
|
|
<ul className="space-y-2 mt-2 text-sm text-foreground-light">
|
|
<li className="list-disc ml-6">Your project no longer requires database previews.</li>
|
|
<li className="list-disc ml-6">
|
|
None of your database previews are currently being used in any app.
|
|
</li>
|
|
</ul>
|
|
</ConfirmationModal>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default BranchManagement
|