diff --git a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx
index f1a67a24999..c9f752df7aa 100644
--- a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx
+++ b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx
@@ -81,6 +81,10 @@ export const BranchRow = ({
const page = router.pathname.split('/').pop()
const daysFromNow = dayjs().diff(dayjs(branch.updated_at), 'day')
+ const willBeDeletedIn = branch.deletion_scheduled_at
+ ? dayjs(branch.deletion_scheduled_at).diff(dayjs(), 'minutes')
+ : null
+ const isDeletionPending = willBeDeletedIn !== null && willBeDeletedIn < 0
const formattedTimeFromNow = dayjs(branch.updated_at).fromNow()
const formattedUpdatedAt = dayjs(branch.updated_at).format('DD MMM YYYY, HH:mm:ss (ZZ)')
@@ -125,9 +129,19 @@ export const BranchRow = ({
-
- {daysFromNow > 1 ? `Updated on ${formattedUpdatedAt}` : `Updated ${formattedTimeFromNow}`}
-
+ {branch.deletion_scheduled_at ? (
+
+ {isDeletionPending
+ ? 'Deletion pending...'
+ : `Will be deleted in ${willBeDeletedIn} minutes`}
+
+ ) : (
+
+ {daysFromNow > 1
+ ? `Updated on ${formattedUpdatedAt}`
+ : `Updated ${formattedTimeFromNow}`}
+
+ )}
{rowActions}
diff --git a/apps/studio/components/interfaces/BranchManagement/Overview.tsx b/apps/studio/components/interfaces/BranchManagement/Overview.tsx
index aa498cb98ae..20f9e947c25 100644
--- a/apps/studio/components/interfaces/BranchManagement/Overview.tsx
+++ b/apps/studio/components/interfaces/BranchManagement/Overview.tsx
@@ -39,6 +39,7 @@ import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
import { BranchLoader, BranchManagementSection, BranchRow, BranchRowLoader } from './BranchPanels'
import { EditBranchModal } from './EditBranchModal'
import { PreviewBranchesEmptyState } from './EmptyStates'
+import { useBranchRestoreMutation } from 'data/branches/branch-restore-mutation'
interface OverviewProps {
isGithubConnected: boolean
@@ -63,8 +64,12 @@ export const Overview = ({
onSelectDeleteBranch,
generateCreatePullRequestURL,
}: OverviewProps) => {
- const [persistentBranches, ephemeralBranches] = partition(
+ const [scheduledForDeletionBranches, aliveBranches] = partition(
previewBranches,
+ (branch) => branch.deletion_scheduled_at !== undefined
+ )
+ const [persistentBranches, ephemeralBranches] = partition(
+ aliveBranches,
(branch) => branch.persistent
)
const { ref: projectRef } = useParams()
@@ -186,6 +191,35 @@ export const Overview = ({
)
})}
+ {/* Scheduled for deletion branches section */}
+
+ {isLoading && }
+ {isSuccess && scheduledForDeletionBranches.length === 0 && (
+
+
No scheduled for deletion branches
+
+ )}
+ {isSuccess &&
+ scheduledForDeletionBranches.map((branch) => {
+ return (
+ onSelectDeleteBranch(branch)}
+ generateCreatePullRequestURL={generateCreatePullRequestURL}
+ />
+ }
+ />
+ )
+ })}
+
>
)
}
@@ -213,13 +247,14 @@ const PreviewBranchActions = ({
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, isLoading: isLoadingEntitlement } =
- useCheckEntitlements('branching_persistent')
+ const { hasAccess: hasAccessToPersistentBranching } = useCheckEntitlements('branching_persistent')
const [showConfirmResetModal, setShowConfirmResetModal] = useState(false)
const [showBranchModeSwitch, setShowBranchModeSwitch] = useState(false)
@@ -245,6 +280,16 @@ const PreviewBranchActions = ({
}
},
})
+ 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 })
@@ -275,64 +320,66 @@ const PreviewBranchActions = ({
/>
- {
- 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,
- },
- }}
- >
- Reset branch
-
-
- {
- 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'
+ {!branch.deletion_scheduled_at && (
+ {
+ 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,
- },
- }}
- >
- {branch.persistent ? (
- <>
- Switch to preview
- >
- ) : (
- <>
- Switch to persistent
- >
- )}
-
-
+ },
+ }}
+ >
+ Reset branch
+
+ )}
+ {!branch.deletion_scheduled_at && (
+ {
+ 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 ? (
+ <>
+ Switch to preview
+ >
+ ) : (
+ <>
+ Switch to persistent
+ >
+ )}
+
+ )}
{/* Edit Branch (gitless) */}
{gitlessBranching && (
)}
+ {branch.deletion_scheduled_at && (
+ {
+ 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,
+ },
+ }}
+ >
+ Restore branch
+
+ )}
diff --git a/apps/studio/components/layouts/AppLayout/BranchDropdown.tsx b/apps/studio/components/layouts/AppLayout/BranchDropdown.tsx
index 88d26f753c0..3529b54fd67 100644
--- a/apps/studio/components/layouts/AppLayout/BranchDropdown.tsx
+++ b/apps/studio/components/layouts/AppLayout/BranchDropdown.tsx
@@ -33,6 +33,7 @@ import {
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { sanitizeRoute } from './ProjectDropdown'
+import { partition } from 'lodash'
const BranchLink = ({
branch,
@@ -101,7 +102,7 @@ export const BranchDropdown = () => {
const mainBranch = branches?.find((branch) => branch.is_default)
const restOfBranches = branches
?.filter((branch) => !branch.is_default)
- .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
+ ?.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
const sortedBranches =
branches && branches.length > 0
diff --git a/apps/studio/data/branches/branch-restore-mutation.ts b/apps/studio/data/branches/branch-restore-mutation.ts
new file mode 100644
index 00000000000..0ac798a371e
--- /dev/null
+++ b/apps/studio/data/branches/branch-restore-mutation.ts
@@ -0,0 +1,50 @@
+import { type UseMutationOptions, useMutation, useQueryClient } from '@tanstack/react-query'
+import { handleError, post } from 'data/fetchers'
+import { toast } from 'sonner'
+import type { ResponseError } from 'types'
+import { branchKeys } from './keys'
+
+export type BranchRestoreVariables = {
+ branchRef: string
+ projectRef: string
+}
+
+export async function restoreBranch({ branchRef }: BranchRestoreVariables) {
+ const { data, error } = await post('/v1/branches/{branch_id_or_ref}/restore', {
+ params: {
+ path: { branch_id_or_ref: branchRef },
+ },
+ })
+
+ if (error) handleError(error)
+ return data
+}
+
+type BranchRestoreData = Awaited>
+
+export const useBranchRestoreMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: (vars) => restoreBranch(vars),
+ async onSuccess(data, variables, context) {
+ const { projectRef } = variables
+ await queryClient.invalidateQueries({ queryKey: branchKeys.list(projectRef) })
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(data, variables, context) {
+ if (onError === undefined) {
+ toast.error(`Failed to restore branch: ${data.message}`)
+ } else {
+ onError(data, variables, context)
+ }
+ },
+ ...options,
+ })
+}
diff --git a/apps/studio/pages/project/[ref]/branches/index.tsx b/apps/studio/pages/project/[ref]/branches/index.tsx
index 024c45cede2..5e11086f673 100644
--- a/apps/studio/pages/project/[ref]/branches/index.tsx
+++ b/apps/studio/pages/project/[ref]/branches/index.tsx
@@ -95,8 +95,12 @@ const BranchesPage: NextPageWithLayout = () => {
}
const onConfirmDeleteBranch = () => {
- if (selectedBranchToDelete == undefined) return console.error('No branch selected')
- const { project_ref: branchRef, parent_project_ref: projectRef } = selectedBranchToDelete
+ if (selectedBranchToDelete === undefined) return console.error('No branch selected')
+ const {
+ project_ref: branchRef,
+ parent_project_ref: projectRef,
+ persistent,
+ } = selectedBranchToDelete
deleteBranch(
{ branchRef, projectRef },
{
@@ -108,7 +112,7 @@ const BranchesPage: NextPageWithLayout = () => {
sendEvent({
action: 'branch_delete_button_clicked',
properties: {
- branchType: selectedBranchToDelete.persistent ? 'persistent' : 'preview',
+ branchType: persistent ? 'persistent' : 'preview',
origin: 'branches_page',
},
groups: {
@@ -175,7 +179,9 @@ const BranchesPage: NextPageWithLayout = () => {
confirmLabel="Delete branch"
confirmPlaceholder="Type in name of branch"
confirmString={selectedBranchToDelete?.name ?? ''}
- alert={{ title: 'You cannot recover this branch once deleted' }}
+ alert={{
+ title: 'You cannot recover this branch once deleted',
+ }}
text={
<>
This will delete your database preview branch{' '}