merge-requests.tsx414 lines · main
1// @ts-nocheck
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import { partition } from 'lodash'
5import { ArrowRight, GitMerge, MessageCircle, MoreVertical, Shield, X } from 'lucide-react'
6import { useRouter } from 'next/router'
7import { PropsWithChildren } from 'react'
8import { toast } from 'sonner'
9import {
10 Button,
11 DropdownMenu,
12 DropdownMenuContent,
13 DropdownMenuItem,
14 DropdownMenuTrigger,
15 Tooltip,
16} from 'ui'
17import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
18
19import {
20 BranchManagementSection,
21 BranchRow,
22} from '@/components/interfaces/BranchManagement/BranchPanels'
23import { BranchSelector } from '@/components/interfaces/BranchManagement/BranchSelector'
24import { PullRequestsEmptyState } from '@/components/interfaces/BranchManagement/EmptyStates'
25import BranchLayout from '@/components/layouts/BranchLayout/BranchLayout'
26import DefaultLayout from '@/components/layouts/DefaultLayout'
27import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
28import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
29import AlertError from '@/components/ui/AlertError'
30import { DocsButton } from '@/components/ui/DocsButton'
31import NoPermission from '@/components/ui/NoPermission'
32import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation'
33import { Branch, useBranchesQuery } from '@/data/branches/branches-query'
34import { useGitHubConnectionsQuery } from '@/data/integrations/github-connections-query'
35import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
36import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
37import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
38import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
39import { DOCS_URL } from '@/lib/constants'
40import type { NextPageWithLayout } from '@/types'
41
42const MergeRequestsPage: NextPageWithLayout = () => {
43 const router = useRouter()
44 const { ref } = useParams()
45 const { data: project } = useSelectedProjectQuery()
46 const { data: selectedOrg } = useSelectedOrganizationQuery()
47
48 const isBranch = project?.parent_project_ref !== undefined
49 const projectRef =
50 project !== undefined ? (isBranch ? project.parent_project_ref : ref) : undefined
51
52 const { can: canReadBranches, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
53 PermissionAction.READ,
54 'preview_branches'
55 )
56
57 const {
58 data: connections,
59 error: connectionsError,
60 isError: isErrorConnections,
61 } = useGitHubConnectionsQuery({
62 organizationId: selectedOrg?.id,
63 })
64
65 const {
66 data: branches = [],
67 error: branchesError,
68 isPending: isLoadingBranches,
69 isError: isErrorBranches,
70 } = useBranchesQuery({ projectRef })
71 const [[mainBranch], previewBranchesUnsorted] = partition(branches, (branch) => branch.is_default)
72 const previewBranches = previewBranchesUnsorted.sort((a, b) =>
73 new Date(a.updated_at) < new Date(b.updated_at) ? 1 : -1
74 )
75
76 const mergeRequestBranches = previewBranches.filter(
77 (branch) =>
78 branch.pr_number !== undefined ||
79 (branch.review_requested_at !== undefined && branch.review_requested_at !== null)
80 )
81
82 const currentBranch = branches?.find((branch) => branch.project_ref === ref)
83 const isCurrentBranchReadyForReview = !!currentBranch?.review_requested_at
84
85 const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)
86 const repo = githubConnection?.repository.name ?? ''
87
88 const isError = isErrorConnections || isErrorBranches
89
90 const isGithubConnected = githubConnection !== undefined
91
92 const { mutate: sendEvent } = useSendEventMutation()
93
94 const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({
95 onError: () => {
96 toast.error(`Failed to update the branch`)
97 },
98 })
99
100 const handleMarkBranchForReview = ({
101 project_ref: branchRef,
102 parent_project_ref: projectRef,
103 persistent,
104 }: Branch) => {
105 updateBranch(
106 {
107 branchRef,
108 projectRef,
109 requestReview: true,
110 },
111 {
112 onSuccess: () => {
113 toast.success('Merge request created')
114
115 // Track merge request creation
116 sendEvent({
117 action: 'branch_create_merge_request_button_clicked',
118 properties: {
119 branchType: persistent ? 'persistent' : 'preview',
120 origin: 'merge_page',
121 },
122 groups: {
123 project: projectRef ?? 'Unknown',
124 organization: selectedOrg?.slug ?? 'Unknown',
125 },
126 })
127
128 router.push(`/project/${branchRef}/merge`)
129 },
130 }
131 )
132 }
133
134 const handleCloseMergeRequest = ({
135 project_ref: branchRef,
136 parent_project_ref: projectRef,
137 }: Branch) => {
138 updateBranch(
139 {
140 branchRef,
141 projectRef,
142 requestReview: false,
143 },
144 {
145 onSuccess: () => {
146 toast.success('Merge request closed')
147
148 // Track merge request closed
149 sendEvent({
150 action: 'branch_close_merge_request_button_clicked',
151 groups: {
152 project: projectRef ?? 'Unknown',
153 organization: selectedOrg?.slug ?? 'Unknown',
154 },
155 })
156 },
157 }
158 )
159 }
160
161 const generateCreatePullRequestURL = (branch?: string) => {
162 if (githubConnection === undefined) return 'https://github.com'
163
164 return branch !== undefined
165 ? `https://github.com/${githubConnection.repository.name}/compare/${mainBranch?.git_branch}...${branch}`
166 : `https://github.com/${githubConnection.repository.name}/compare`
167 }
168
169 return (
170 <ScaffoldContainer>
171 <ScaffoldSection>
172 <div className="col-span-12">
173 <div className="space-y-4">
174 {isPermissionsLoaded && !canReadBranches ? (
175 <NoPermission resourceText="view this project's branches" />
176 ) : (
177 <>
178 {isErrorConnections && (
179 <AlertError
180 error={connectionsError}
181 subject="Failed to retrieve GitHub integration connection"
182 />
183 )}
184
185 {isErrorBranches && (
186 <AlertError error={branchesError} subject="Failed to retrieve preview branches" />
187 )}
188
189 {!isError && (
190 <div className="space-y-4">
191 {isBranch && !isCurrentBranchReadyForReview && currentBranch && (
192 <div className="rounded-sm border rounded-lg bg-background px-6 py-4">
193 <div className="flex items-center justify-between">
194 <div className="flex items-center gap-2 text-sm text-foreground-light">
195 <GitMerge strokeWidth={1.5} size={16} className="text-brand" />
196 <span className="text-foreground">{currentBranch.name}</span>
197 last viewed
198 </div>
199 <Button
200 type="primary"
201 size="tiny"
202 loading={currentBranch && isUpdating}
203 onClick={() =>
204 currentBranch && handleMarkBranchForReview(currentBranch)
205 }
206 >
207 Create merge request
208 </Button>
209 </div>
210 </div>
211 )}
212 <BranchManagementSection
213 header={`${mergeRequestBranches.length} merge requests`}
214 >
215 {isLoadingBranches ? (
216 <div className="p-4">
217 <GenericSkeletonLoader />
218 </div>
219 ) : mergeRequestBranches.length > 0 ? (
220 mergeRequestBranches.map((branch) => {
221 const isPR = branch.pr_number !== undefined
222 const rowLink = isPR
223 ? `https://github.com/${repo}/pull/${branch.pr_number}`
224 : `/project/${branch.project_ref}/merge`
225 return (
226 <BranchRow
227 isGithubConnected={isGithubConnected}
228 key={branch.id}
229 label={
230 <div className="flex items-center gap-x-4">
231 {branch.name}
232 <ArrowRight
233 size={14}
234 strokeWidth={1.5}
235 className="text-foreground-lighter"
236 />
237 <div className="flex items-center gap-x-2">
238 {branch.pr_number ? (
239 <p className="text-foreground-lighter">#{branch.pr_number}</p>
240 ) : (
241 <>
242 <Shield
243 size={14}
244 strokeWidth={1.5}
245 className="text-warning"
246 />
247 <p className="text-foreground-lighter">{mainBranch.name}</p>
248 </>
249 )}
250 </div>
251 </div>
252 }
253 repo={repo}
254 branch={branch}
255 rowLink={rowLink}
256 external={isPR}
257 rowActions={
258 // We always want to show the action button to close a merge request
259 // when user has requested review from dashboard. It doesn't matter
260 // whether the branch is linked to a GitHub PR.
261 branch.review_requested_at && (
262 <DropdownMenu>
263 <DropdownMenuTrigger asChild>
264 <Button
265 type="text"
266 icon={<MoreVertical />}
267 className="px-1"
268 onClick={(e) => e.stopPropagation()}
269 />
270 </DropdownMenuTrigger>
271 <DropdownMenuContent className="w-56" side="bottom" align="end">
272 <Tooltip>
273 <DropdownMenuItem
274 className="gap-x-2"
275 disabled={isUpdating}
276 onSelect={(e) => {
277 e.stopPropagation()
278 handleCloseMergeRequest(branch)
279 }}
280 >
281 <X size={14} /> Close this merge request
282 </DropdownMenuItem>
283 </Tooltip>
284 </DropdownMenuContent>
285 </DropdownMenu>
286 )
287 }
288 />
289 )
290 })
291 ) : (
292 <PullRequestsEmptyState
293 url={generateCreatePullRequestURL()}
294 projectRef={projectRef ?? '_'}
295 branches={previewBranches}
296 onBranchSelected={handleMarkBranchForReview}
297 isUpdating={isUpdating}
298 hasGithubConnection={!!githubConnection}
299 />
300 )}
301 </BranchManagementSection>
302 </div>
303 )}
304 </>
305 )}
306 </div>
307 </div>
308 </ScaffoldSection>
309 </ScaffoldContainer>
310 )
311}
312
313const MergeRequestsPageWrapper = ({ children }: PropsWithChildren<{}>) => {
314 const router = useRouter()
315 const { ref } = useParams()
316 const { data: project } = useSelectedProjectQuery()
317 const { data: selectedOrg } = useSelectedOrganizationQuery()
318
319 const isBranch = project?.parent_project_ref !== undefined
320 const projectRef =
321 project !== undefined ? (isBranch ? project.parent_project_ref : ref) : undefined
322
323 const { data: branches } = useBranchesQuery({ projectRef })
324 const previewBranches = (branches || []).filter((b) => !b.is_default)
325
326 const { mutate: sendEvent } = useSendEventMutation()
327
328 const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({
329 onError: () => {
330 toast.error(`Failed to update the branch`)
331 },
332 })
333
334 const handleMarkBranchForReview = ({
335 project_ref: branchRef,
336 parent_project_ref: projectRef,
337 persistent,
338 }: Branch) => {
339 updateBranch(
340 {
341 branchRef,
342 projectRef,
343 requestReview: true,
344 },
345 {
346 onSuccess: () => {
347 toast.success('Merge request created')
348
349 // Track merge request creation
350 sendEvent({
351 action: 'branch_create_merge_request_button_clicked',
352 properties: {
353 branchType: persistent ? 'persistent' : 'preview',
354 origin: 'branch_selector',
355 },
356 groups: {
357 project: projectRef ?? 'Unknown',
358 organization: selectedOrg?.slug ?? 'Unknown',
359 },
360 })
361
362 router.push(`/project/${branchRef}/merge`)
363 },
364 }
365 )
366 }
367
368 return (
369 <PageLayout
370 title="Merge requests"
371 subtitle="Review and merge changes from one branch into another"
372 primaryActions={
373 <BranchSelector
374 branches={previewBranches}
375 onBranchSelected={handleMarkBranchForReview}
376 disabled={!projectRef}
377 isUpdating={isUpdating}
378 />
379 }
380 secondaryActions={
381 <div className="flex items-center gap-x-2">
382 <Button
383 asChild
384 type="text"
385 icon={<MessageCircle className="text-muted" strokeWidth={1} />}
386 >
387 <a
388 target="_blank"
389 rel="noreferrer"
390 href="https://github.com/orgs/briven/discussions/18937"
391 >
392 Branching feedback
393 </a>
394 </Button>
395 <DocsButton href={`${DOCS_URL}/guides/platform/branching`} />
396 </div>
397 }
398 >
399 {children}
400 </PageLayout>
401 )
402}
403
404MergeRequestsPage.getLayout = (page) => {
405 return (
406 <DefaultLayout>
407 <BranchLayout>
408 <MergeRequestsPageWrapper>{page}</MergeRequestsPageWrapper>
409 </BranchLayout>
410 </DefaultLayout>
411 )
412}
413
414export default MergeRequestsPage