index.tsx262 lines · main
| 1 | // @ts-nocheck |
| 2 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 3 | import { useParams } from 'common' |
| 4 | import { partition } from 'lodash' |
| 5 | import { MessageCircle } from 'lucide-react' |
| 6 | import { useRouter } from 'next/router' |
| 7 | import { useState } from 'react' |
| 8 | import { toast } from 'sonner' |
| 9 | import { Button } from 'ui' |
| 10 | |
| 11 | import { Overview } from '@/components/interfaces/BranchManagement/Overview' |
| 12 | import BranchLayout from '@/components/layouts/BranchLayout/BranchLayout' |
| 13 | import { DefaultLayout } from '@/components/layouts/DefaultLayout' |
| 14 | import { PageLayout } from '@/components/layouts/PageLayout/PageLayout' |
| 15 | import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' |
| 16 | import { AlertError } from '@/components/ui/AlertError' |
| 17 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 18 | import { DocsButton } from '@/components/ui/DocsButton' |
| 19 | import { NoPermission } from '@/components/ui/NoPermission' |
| 20 | import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' |
| 21 | import { useBranchDeleteMutation } from '@/data/branches/branch-delete-mutation' |
| 22 | import { Branch, useBranchesQuery } from '@/data/branches/branches-query' |
| 23 | import { useGitHubConnectionsQuery } from '@/data/integrations/github-connections-query' |
| 24 | import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' |
| 25 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 26 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 27 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 28 | import { DOCS_URL } from '@/lib/constants' |
| 29 | import { useAppStateSnapshot } from '@/state/app-state' |
| 30 | import type { NextPageWithLayout } from '@/types' |
| 31 | |
| 32 | const 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 | |
| 197 | BranchesPage.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 | |
| 262 | export default BranchesPage |