| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useQueryClient } from '@tanstack/react-query'
- import { useParams } from 'common'
- import { partition } from 'lodash'
- import {
- Clock,
- ExternalLink,
- Infinity,
- MoreVertical,
- Pencil,
- RefreshCw,
- Shield,
- Trash2,
- } from 'lucide-react'
- import Link from 'next/link'
- import { useState } from 'react'
- import { toast } from 'sonner'
- import {
- Button,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
- } from 'ui'
- import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
- import { BranchLoader, BranchManagementSection, BranchRow, BranchRowLoader } from './BranchPanels'
- import { EditBranchModal } from './EditBranchModal'
- import { PreviewBranchesEmptyState } from './EmptyStates'
- import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
- import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
- import { useBranchQuery } from '@/data/branches/branch-query'
- import { useBranchResetMutation } from '@/data/branches/branch-reset-mutation'
- import { useBranchRestoreMutation } from '@/data/branches/branch-restore-mutation'
- import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation'
- import type { Branch } from '@/data/branches/branches-query'
- import { branchKeys } from '@/data/branches/keys'
- import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { IS_PLATFORM } from '@/lib/constants'
- interface OverviewProps {
- isGithubConnected: boolean
- isLoading: boolean
- isSuccess: boolean
- repo: string
- mainBranch: Branch
- previewBranches: Branch[]
- onSelectCreateBranch: () => void
- onSelectDeleteBranch: (branch: Branch) => void
- generateCreatePullRequestURL: (branchName?: string) => string
- }
- export const Overview = ({
- isGithubConnected,
- isLoading,
- isSuccess,
- repo,
- mainBranch,
- previewBranches,
- onSelectCreateBranch,
- onSelectDeleteBranch,
- generateCreatePullRequestURL,
- }: OverviewProps) => {
- const [scheduledForDeletionBranches, aliveBranches] = partition(
- previewBranches,
- (branch) => branch.deletion_scheduled_at !== undefined
- )
- const [persistentBranches, ephemeralBranches] = partition(
- aliveBranches,
- (branch) => branch.persistent
- )
- const { ref: projectRef } = useParams()
- const { data: selectedOrg } = useSelectedOrganizationQuery()
- const { hasAccess: hasAccessToPersistentBranching, isLoading: isLoadingEntitlement } =
- useCheckEntitlements('branching_persistent')
- return (
- <>
- <BranchManagementSection header="Production branch">
- {isLoading && <BranchRowLoader />}
- {isSuccess && mainBranch !== undefined && (
- <BranchRow
- branch={mainBranch}
- isGithubConnected={isGithubConnected}
- label={
- <div className="flex items-center gap-x-2">
- <Shield size={14} strokeWidth={1.5} className="text-warning" />
- {mainBranch.name}
- </div>
- }
- repo={repo}
- rowActions={<MainBranchActions branch={mainBranch} repo={repo} />}
- />
- )}
- {isSuccess && mainBranch === undefined && (
- <div className="w-full flex items-center justify-between px-4 py-2.5 hover:bg-surface-100">
- <Link href={`/project/${projectRef}`} className="text-foreground block w-full">
- <div className="flex items-center gap-x-3">
- <Shield size={14} strokeWidth={1.5} className="text-warning" />
- main
- </div>
- </Link>
- </div>
- )}
- </BranchManagementSection>
- {/* Persistent Branches Section */}
- <BranchManagementSection header="Persistent branches">
- {(isLoading || isLoadingEntitlement) && <BranchLoader />}
- {isSuccess &&
- !isLoadingEntitlement &&
- !hasAccessToPersistentBranching &&
- IS_PLATFORM &&
- persistentBranches.length === 0 && (
- <div className="px-6 py-10 flex items-center justify-between">
- <div className="flex flex-col gap-0.5">
- <p className="text-sm">Upgrade to unlock persistent branches</p>
- <p className="text-sm text-foreground-lighter text-balance">
- Persistent branches are long-lived, cannot be reset, and are ideal for staging
- environments.
- </p>
- </div>
- <Button type="primary" asChild>
- <Link href={`/org/${selectedOrg?.slug}/billing?panel=subscriptionPlan`}>
- Upgrade
- </Link>
- </Button>
- </div>
- )}
- {isSuccess &&
- !isLoadingEntitlement &&
- hasAccessToPersistentBranching &&
- persistentBranches.length === 0 && (
- <div className="flex items-center flex-col gap-0.5 justify-center w-full py-10">
- <p>No persistent branches</p>
- <p className="text-foreground-lighter text-center text-balance">
- Persistent branches are long-lived, cannot be reset, and are ideal for staging
- environments.
- </p>
- </div>
- )}
- {isSuccess &&
- !isLoadingEntitlement &&
- persistentBranches.map((branch) => {
- return (
- <BranchRow
- isGithubConnected={isGithubConnected}
- key={branch.id}
- repo={repo}
- branch={branch}
- rowActions={
- <PreviewBranchActions
- branch={branch}
- repo={repo}
- onSelectDeleteBranch={() => onSelectDeleteBranch(branch)}
- generateCreatePullRequestURL={generateCreatePullRequestURL}
- />
- }
- />
- )
- })}
- </BranchManagementSection>
- {/* Ephemeral/Preview Branches Section */}
- <BranchManagementSection header="Preview branches">
- {isLoading && <BranchLoader />}
- {isSuccess && ephemeralBranches.length === 0 && (
- <PreviewBranchesEmptyState onSelectCreateBranch={onSelectCreateBranch} />
- )}
- {isSuccess &&
- ephemeralBranches.map((branch) => {
- return (
- <BranchRow
- isGithubConnected={isGithubConnected}
- key={branch.id}
- repo={repo}
- branch={branch}
- rowActions={
- <PreviewBranchActions
- branch={branch}
- repo={repo}
- onSelectDeleteBranch={() => onSelectDeleteBranch(branch)}
- generateCreatePullRequestURL={generateCreatePullRequestURL}
- />
- }
- />
- )
- })}
- </BranchManagementSection>
- {/* Scheduled for deletion branches section */}
- <BranchManagementSection header="Scheduled for deletion branches">
- {isLoading && <BranchLoader />}
- {isSuccess && scheduledForDeletionBranches.length === 0 && (
- <div className="flex items-center flex-col gap-0.5 justify-center w-full py-10">
- <p className="text-foreground-lighter">No branches scheduled for deletion</p>
- </div>
- )}
- {isSuccess &&
- scheduledForDeletionBranches.map((branch) => {
- return (
- <BranchRow
- isGithubConnected={isGithubConnected}
- key={branch.id}
- repo={repo}
- branch={branch}
- rowActions={
- <PreviewBranchActions
- branch={branch}
- repo={repo}
- // If a scheduled for deletion branch is deleted, we force the deletion
- onSelectDeleteBranch={() => onSelectDeleteBranch(branch)}
- generateCreatePullRequestURL={generateCreatePullRequestURL}
- />
- }
- />
- )
- })}
- </BranchManagementSection>
- </>
- )
- }
- // Row actions for preview branches (non-main)
- const PreviewBranchActions = ({
- branch,
- onSelectDeleteBranch,
- generateCreatePullRequestURL,
- }: {
- branch: Branch
- repo: string
- onSelectDeleteBranch: () => void
- generateCreatePullRequestURL: (branchName?: string) => string
- }) => {
- const queryClient = useQueryClient()
- const { project_ref: branchRef, parent_project_ref: projectRef } = branch
- const { can: canDeleteBranches } = useAsyncCheckPermissions(
- PermissionAction.DELETE,
- 'preview_branches'
- )
- const { can: canUpdateBranches } = useAsyncCheckPermissions(
- PermissionAction.UPDATE,
- 'preview_branches'
- )
- // If user can update branches, they can restore branches
- const canRestoreBranches = canUpdateBranches
- const { data } = useBranchQuery({ projectRef, branchRef })
- const isBranchActiveHealthy = data?.status === 'ACTIVE_HEALTHY'
- const isPersistentBranch = branch.persistent
- const { hasAccess: hasAccessToPersistentBranching } = useCheckEntitlements('branching_persistent')
- const [showConfirmResetModal, setShowConfirmResetModal] = useState(false)
- const [showBranchModeSwitch, setShowBranchModeSwitch] = useState(false)
- const [
- showPersistentBranchDeleteConfirmationModal,
- setShowPersistentBranchDeleteConfirmationModal,
- ] = useState(false)
- const [showEditBranchModal, setShowEditBranchModal] = useState(false)
- const { mutate: resetBranch, isPending: isResetting } = useBranchResetMutation({
- onSuccess() {
- toast.success('Success! Please allow a few seconds for the branch to reset.')
- setShowConfirmResetModal(false)
- },
- })
- const { mutate: updateBranch, isPending: isUpdatingBranch } = useBranchUpdateMutation({
- onSuccess() {
- toast.success('Successfully updated branch')
- setShowBranchModeSwitch(false)
- if (projectRef) {
- queryClient.invalidateQueries({ queryKey: branchKeys.list(projectRef) })
- }
- },
- })
- const { mutate: restoreBranch } = useBranchRestoreMutation({
- onSuccess() {
- toast.success('Success! Please allow a few minutes for the branch to restore.')
- setShowBranchModeSwitch(false)
- },
- })
- const onRestoreBranch = () => {
- restoreBranch({ branchRef, projectRef })
- }
- const onConfirmReset = () => {
- resetBranch({ branchRef, projectRef })
- }
- const onTogglePersistent = () => {
- updateBranch({ branchRef, projectRef, persistent: !branch.persistent })
- }
- const onDeleteBranch = (e: Event | React.MouseEvent<HTMLDivElement>) => {
- if (isPersistentBranch) {
- setShowPersistentBranchDeleteConfirmationModal(true)
- } else {
- e.stopPropagation()
- onSelectDeleteBranch()
- }
- }
- return (
- <>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- type="text"
- icon={<MoreVertical />}
- className="px-1"
- onClick={(e) => e.stopPropagation()}
- />
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-56" side="bottom" align="end">
- <DropdownMenuItemTooltip
- className="gap-x-2"
- disabled={!canUpdateBranches || !isBranchActiveHealthy || isUpdatingBranch}
- onSelect={(e) => {
- e.stopPropagation()
- setShowEditBranchModal(true)
- }}
- onClick={(e) => {
- e.stopPropagation()
- setShowEditBranchModal(true)
- }}
- tooltip={{
- content: {
- side: 'left',
- text: !canUpdateBranches
- ? 'You need additional permissions to edit branches'
- : !isBranchActiveHealthy
- ? 'Branch is still initializing. Please wait for it to become healthy before editing.'
- : undefined,
- },
- }}
- >
- <Pencil size={14} /> Edit branch
- </DropdownMenuItemTooltip>
- {!branch.deletion_scheduled_at && (
- <DropdownMenuItemTooltip
- className="gap-x-2"
- disabled={isResetting || !isBranchActiveHealthy}
- onSelect={(e) => {
- e.stopPropagation()
- setShowConfirmResetModal(true)
- }}
- onClick={(e) => {
- e.stopPropagation()
- setShowConfirmResetModal(true)
- }}
- tooltip={{
- content: {
- side: 'left',
- text: !isBranchActiveHealthy
- ? 'Branch is still initializing. Please wait for it to become healthy before resetting.'
- : undefined,
- },
- }}
- >
- <RefreshCw size={14} /> Reset branch
- </DropdownMenuItemTooltip>
- )}
- {!branch.deletion_scheduled_at && (
- <DropdownMenuItemTooltip
- className="gap-x-2"
- disabled={
- !isBranchActiveHealthy || (!branch.persistent && !hasAccessToPersistentBranching)
- }
- onSelect={(e) => {
- e.stopPropagation()
- setShowBranchModeSwitch(true)
- }}
- onClick={(e) => {
- e.stopPropagation()
- setShowBranchModeSwitch(true)
- }}
- tooltip={{
- content: {
- side: 'left',
- text: !isBranchActiveHealthy
- ? 'Branch is still initializing. Please wait for it to become healthy before switching.'
- : !branch.persistent && !hasAccessToPersistentBranching
- ? 'Upgrade your plan to access persistent branches'
- : undefined,
- },
- }}
- >
- {branch.persistent ? (
- <>
- <Clock size={14} /> Switch to preview
- </>
- ) : (
- <>
- <Infinity size={14} className="scale-110" /> Switch to persistent
- </>
- )}
- </DropdownMenuItemTooltip>
- )}
- {/* Create PR if applicable */}
- {branch.git_branch && branch.pr_number === undefined && (
- <DropdownMenuItem asChild className="gap-x-2">
- <a
- target="_blank"
- rel="noreferrer"
- href={generateCreatePullRequestURL(branch.git_branch)}
- onClick={(e) => e.stopPropagation()}
- >
- <ExternalLink size={14} /> Create pull request
- </a>
- </DropdownMenuItem>
- )}
- {branch.deletion_scheduled_at && (
- <DropdownMenuItemTooltip
- className="gap-x-2"
- disabled={!canRestoreBranches || branch.preview_project_status !== 'INACTIVE'}
- onSelect={(e) => {
- e.stopPropagation()
- onRestoreBranch()
- }}
- onClick={(e) => {
- e.stopPropagation()
- onRestoreBranch()
- }}
- tooltip={{
- content: {
- side: 'left',
- text: !canRestoreBranches
- ? 'You need additional permissions to restore branches'
- : branch.preview_project_status !== 'INACTIVE'
- ? 'Preview project is not fully paused or already coming up. Please wait for it to become fully paused before restoring.'
- : undefined,
- },
- }}
- >
- <Clock size={14} /> Restore branch
- </DropdownMenuItemTooltip>
- )}
- <DropdownMenuSeparator />
- <DropdownMenuItemTooltip
- className="gap-x-2"
- disabled={!canDeleteBranches}
- onSelect={onDeleteBranch}
- onClick={onDeleteBranch}
- tooltip={{
- content: {
- side: 'left',
- text: !canDeleteBranches
- ? 'You need additional permissions to delete branches'
- : undefined,
- },
- }}
- >
- <Trash2 size={14} /> Delete branch
- </DropdownMenuItemTooltip>
- </DropdownMenuContent>
- </DropdownMenu>
- <TextConfirmModal
- variant="warning"
- visible={showConfirmResetModal}
- onCancel={() => setShowConfirmResetModal(false)}
- onConfirm={onConfirmReset}
- loading={isResetting}
- title="Reset branch"
- confirmLabel="Reset branch"
- confirmPlaceholder="Type in name of branch"
- confirmString={branch?.name ?? ''}
- alert={{
- title: `Are you sure you want to reset the "${branch.name}" branch? All data will be deleted.`,
- }}
- />
- <ConfirmationModal
- variant="default"
- visible={showBranchModeSwitch}
- confirmLabel={branch.persistent ? 'Switch to preview' : 'Switch to persistent'}
- title="Confirm branch mode switch"
- loading={isUpdatingBranch}
- onCancel={() => setShowBranchModeSwitch(false)}
- onConfirm={onTogglePersistent}
- >
- <p className="text-sm text-foreground-light">
- Are you sure you want to switch the branch "{branch.name}" to{' '}
- {branch.persistent ? 'preview' : 'persistent'}?
- </p>
- </ConfirmationModal>
- <ConfirmationModal
- variant="warning"
- visible={showPersistentBranchDeleteConfirmationModal}
- confirmLabel={'Switch to preview'}
- title="Branch must be switched to preview before deletion"
- loading={isUpdatingBranch}
- onCancel={() => setShowPersistentBranchDeleteConfirmationModal(false)}
- onConfirm={onTogglePersistent}
- >
- <p className="text-sm text-foreground-light">
- You must switch the branch "{branch.name}" to preview before deleting it.
- </p>
- </ConfirmationModal>
- <EditBranchModal
- branch={branch}
- visible={showEditBranchModal}
- onClose={() => setShowEditBranchModal(false)}
- />
- </>
- )
- }
- // Actions for main (production) branch
- const MainBranchActions = ({ branch, repo }: { branch: Branch; repo: string }) => {
- const { ref: projectRef } = useParams()
- const { can: canUpdateBranches } = useAsyncCheckPermissions(
- PermissionAction.UPDATE,
- 'preview_branches'
- )
- const [showEditBranchModal, setShowEditBranchModal] = useState(false)
- return (
- <>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button type="text" icon={<MoreVertical />} className="px-1" />
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-56" side="bottom" align="end">
- {repo ? (
- <Link passHref href={`/project/${projectRef}/settings/integrations`}>
- <DropdownMenuItem asChild className="gap-x-2">
- <a>Change production branch</a>
- </DropdownMenuItem>
- </Link>
- ) : (
- <DropdownMenuItem
- className="gap-x-2"
- disabled={!canUpdateBranches}
- onSelect={() => setShowEditBranchModal(true)}
- onClick={() => setShowEditBranchModal(true)}
- >
- <Pencil size={14} /> Edit Branch
- </DropdownMenuItem>
- )}
- </DropdownMenuContent>
- </DropdownMenu>
- <EditBranchModal
- branch={branch}
- visible={showEditBranchModal}
- onClose={() => setShowEditBranchModal(false)}
- />
- </>
- )
- }
|