index.tsx262 lines · main
1// @ts-nocheck
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import { partition } from 'lodash'
5import { MessageCircle } from 'lucide-react'
6import { useRouter } from 'next/router'
7import { useState } from 'react'
8import { toast } from 'sonner'
9import { Button } from 'ui'
10
11import { Overview } from '@/components/interfaces/BranchManagement/Overview'
12import BranchLayout from '@/components/layouts/BranchLayout/BranchLayout'
13import { DefaultLayout } from '@/components/layouts/DefaultLayout'
14import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
15import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
16import { AlertError } from '@/components/ui/AlertError'
17import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
18import { DocsButton } from '@/components/ui/DocsButton'
19import { NoPermission } from '@/components/ui/NoPermission'
20import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
21import { useBranchDeleteMutation } from '@/data/branches/branch-delete-mutation'
22import { Branch, useBranchesQuery } from '@/data/branches/branches-query'
23import { useGitHubConnectionsQuery } from '@/data/integrations/github-connections-query'
24import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
25import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
26import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
27import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
28import { DOCS_URL } from '@/lib/constants'
29import { useAppStateSnapshot } from '@/state/app-state'
30import type { NextPageWithLayout } from '@/types'
31
32const BranchesPage: NextPageWithLayout = () => {
33 const router = useRouter()
34 const { ref } = useParams()
35 const snap = useAppStateSnapshot()
36 const { data: project } = useSelectedProjectQuery()
37 const { data: selectedOrg } = useSelectedOrganizationQuery()
38
39 const [selectedBranchToDelete, setSelectedBranchToDelete] = useState<Branch>()
40
41 const { mutate: sendEvent } = useSendEventMutation()
42
43 const isBranch = project?.parent_project_ref !== undefined
44 const projectRef =
45 project !== undefined ? (isBranch ? project.parent_project_ref : ref) : undefined
46
47 const { can: canReadBranches, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
48 PermissionAction.READ,
49 'preview_branches'
50 )
51
52 const {
53 data: connections,
54 error: connectionsError,
55 isPending: isLoadingConnections,
56 isSuccess: isSuccessConnections,
57 isError: isErrorConnections,
58 } = useGitHubConnectionsQuery({
59 organizationId: selectedOrg?.id,
60 })
61
62 const {
63 data: branches,
64 error: branchesError,
65 isPending: isLoadingBranches,
66 isError: isErrorBranches,
67 isSuccess: isSuccessBranches,
68 } = useBranchesQuery({ projectRef })
69 const [[mainBranch], previewBranchesUnsorted] = partition(branches, (branch) => branch.is_default)
70 const previewBranches = previewBranchesUnsorted.sort((a, b) =>
71 new Date(a.updated_at) < new Date(b.updated_at) ? 1 : -1
72 )
73
74 const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)
75 const repo = githubConnection?.repository.name ?? ''
76
77 const isError = isErrorConnections || isErrorBranches
78 const isLoading = isLoadingConnections || isLoadingBranches
79 const isSuccess = isSuccessConnections && isSuccessBranches
80
81 const isGithubConnected = githubConnection !== undefined
82
83 const { mutate: deleteBranch, isPending: isDeleting } = useBranchDeleteMutation({
84 onSuccess: () => {
85 toast.success('Successfully deleted branch')
86 setSelectedBranchToDelete(undefined)
87 },
88 })
89
90 const generateCreatePullRequestURL = (branch?: string) => {
91 if (githubConnection === undefined) return 'https://github.com'
92
93 return branch !== undefined
94 ? `https://github.com/${githubConnection.repository.name}/compare/${mainBranch?.git_branch}...${branch}`
95 : `https://github.com/${githubConnection.repository.name}/compare`
96 }
97
98 const onConfirmDeleteBranch = () => {
99 if (selectedBranchToDelete === undefined) return console.error('No branch selected')
100 const {
101 project_ref: branchRef,
102 parent_project_ref: projectRef,
103 persistent,
104 } = selectedBranchToDelete
105 deleteBranch(
106 { branchRef, projectRef },
107 {
108 onSuccess: () => {
109 if (branchRef === ref) {
110 router.push(`/project/${projectRef}/branches`)
111 }
112 // Track delete button click
113 sendEvent({
114 action: 'branch_delete_button_clicked',
115 properties: {
116 branchType: persistent ? 'persistent' : 'preview',
117 origin: 'branches_page',
118 },
119 groups: {
120 project: projectRef ?? 'Unknown',
121 organization: selectedOrg?.slug ?? 'Unknown',
122 },
123 })
124 },
125 }
126 )
127 }
128
129 return (
130 <>
131 <ScaffoldContainer>
132 <ScaffoldSection>
133 <div className="col-span-12">
134 <div className="space-y-4">
135 {isPermissionsLoaded && !canReadBranches ? (
136 <NoPermission resourceText="view this project's branches" />
137 ) : (
138 <>
139 {isErrorConnections && (
140 <AlertError
141 error={connectionsError}
142 subject="Failed to retrieve GitHub integration connection"
143 />
144 )}
145
146 {isErrorBranches && (
147 <AlertError
148 error={branchesError}
149 subject="Failed to retrieve preview branches"
150 />
151 )}
152
153 {!isError && (
154 <Overview
155 isGithubConnected={isGithubConnected}
156 isLoading={isLoading}
157 isSuccess={isSuccess}
158 repo={repo}
159 mainBranch={mainBranch}
160 previewBranches={previewBranches}
161 onSelectCreateBranch={() => snap.setShowCreateBranchModal(true)}
162 onSelectDeleteBranch={setSelectedBranchToDelete}
163 generateCreatePullRequestURL={generateCreatePullRequestURL}
164 />
165 )}
166 </>
167 )}
168 </div>
169 </div>
170 </ScaffoldSection>
171 </ScaffoldContainer>
172
173 <TextConfirmModal
174 variant="warning"
175 visible={selectedBranchToDelete !== undefined}
176 onCancel={() => setSelectedBranchToDelete(undefined)}
177 onConfirm={() => onConfirmDeleteBranch()}
178 loading={isDeleting}
179 title="Delete branch"
180 confirmLabel="Delete branch"
181 confirmPlaceholder="Type in name of branch"
182 confirmString={selectedBranchToDelete?.name ?? ''}
183 alert={{
184 title: 'You cannot recover this branch once deleted',
185 }}
186 text={
187 <>
188 This will delete your database preview branch{' '}
189 <span className="text-bold text-foreground">{selectedBranchToDelete?.name}</span>.
190 </>
191 }
192 />
193 </>
194 )
195}
196
197BranchesPage.getLayout = (page) => {
198 const BranchesPageWrapper = () => {
199 const snap = useAppStateSnapshot()
200 const { can: canCreateBranches } = useAsyncCheckPermissions(
201 PermissionAction.CREATE,
202 'preview_branches',
203 {
204 resource: { is_default: false },
205 }
206 )
207
208 const primaryActions = (
209 <ButtonTooltip
210 type="primary"
211 disabled={!canCreateBranches}
212 onClick={() => snap.setShowCreateBranchModal(true)}
213 tooltip={{
214 content: {
215 side: 'bottom',
216 text: !canCreateBranches
217 ? 'You need additional permissions to create branches'
218 : undefined,
219 },
220 }}
221 >
222 Create branch
223 </ButtonTooltip>
224 )
225
226 const secondaryActions = (
227 <div className="flex items-center gap-x-2">
228 <Button asChild type="text" icon={<MessageCircle className="text-muted" strokeWidth={1} />}>
229 <a
230 target="_blank"
231 rel="noreferrer"
232 href="https://github.com/orgs/briven/discussions/18937"
233 >
234 Branching feedback
235 </a>
236 </Button>
237 <DocsButton href={`${DOCS_URL}/guides/platform/branching`} />
238 </div>
239 )
240
241 return (
242 <PageLayout
243 title="Branches"
244 subtitle="Manage your database preview branches and deployments"
245 primaryActions={primaryActions}
246 secondaryActions={secondaryActions}
247 >
248 {page}
249 </PageLayout>
250 )
251 }
252
253 return (
254 <DefaultLayout>
255 <BranchLayout>
256 <BranchesPageWrapper />
257 </BranchLayout>
258 </DefaultLayout>
259 )
260}
261
262export default BranchesPage