Overview.tsx565 lines · main
| 1 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 2 | import { useQueryClient } from '@tanstack/react-query' |
| 3 | import { useParams } from 'common' |
| 4 | import { partition } from 'lodash' |
| 5 | import { |
| 6 | Clock, |
| 7 | ExternalLink, |
| 8 | Infinity, |
| 9 | MoreVertical, |
| 10 | Pencil, |
| 11 | RefreshCw, |
| 12 | Shield, |
| 13 | Trash2, |
| 14 | } from 'lucide-react' |
| 15 | import Link from 'next/link' |
| 16 | import { useState } from 'react' |
| 17 | import { toast } from 'sonner' |
| 18 | import { |
| 19 | Button, |
| 20 | DropdownMenu, |
| 21 | DropdownMenuContent, |
| 22 | DropdownMenuItem, |
| 23 | DropdownMenuSeparator, |
| 24 | DropdownMenuTrigger, |
| 25 | } from 'ui' |
| 26 | import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' |
| 27 | |
| 28 | import { BranchLoader, BranchManagementSection, BranchRow, BranchRowLoader } from './BranchPanels' |
| 29 | import { EditBranchModal } from './EditBranchModal' |
| 30 | import { PreviewBranchesEmptyState } from './EmptyStates' |
| 31 | import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip' |
| 32 | import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' |
| 33 | import { useBranchQuery } from '@/data/branches/branch-query' |
| 34 | import { useBranchResetMutation } from '@/data/branches/branch-reset-mutation' |
| 35 | import { useBranchRestoreMutation } from '@/data/branches/branch-restore-mutation' |
| 36 | import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation' |
| 37 | import type { Branch } from '@/data/branches/branches-query' |
| 38 | import { branchKeys } from '@/data/branches/keys' |
| 39 | import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' |
| 40 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 41 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 42 | import { IS_PLATFORM } from '@/lib/constants' |
| 43 | |
| 44 | interface OverviewProps { |
| 45 | isGithubConnected: boolean |
| 46 | isLoading: boolean |
| 47 | isSuccess: boolean |
| 48 | repo: string |
| 49 | mainBranch: Branch |
| 50 | previewBranches: Branch[] |
| 51 | onSelectCreateBranch: () => void |
| 52 | onSelectDeleteBranch: (branch: Branch) => void |
| 53 | generateCreatePullRequestURL: (branchName?: string) => string |
| 54 | } |
| 55 | |
| 56 | export const Overview = ({ |
| 57 | isGithubConnected, |
| 58 | isLoading, |
| 59 | isSuccess, |
| 60 | repo, |
| 61 | mainBranch, |
| 62 | previewBranches, |
| 63 | onSelectCreateBranch, |
| 64 | onSelectDeleteBranch, |
| 65 | generateCreatePullRequestURL, |
| 66 | }: OverviewProps) => { |
| 67 | const [scheduledForDeletionBranches, aliveBranches] = partition( |
| 68 | previewBranches, |
| 69 | (branch) => branch.deletion_scheduled_at !== undefined |
| 70 | ) |
| 71 | const [persistentBranches, ephemeralBranches] = partition( |
| 72 | aliveBranches, |
| 73 | (branch) => branch.persistent |
| 74 | ) |
| 75 | const { ref: projectRef } = useParams() |
| 76 | const { data: selectedOrg } = useSelectedOrganizationQuery() |
| 77 | |
| 78 | const { hasAccess: hasAccessToPersistentBranching, isLoading: isLoadingEntitlement } = |
| 79 | useCheckEntitlements('branching_persistent') |
| 80 | |
| 81 | return ( |
| 82 | <> |
| 83 | <BranchManagementSection header="Production branch"> |
| 84 | {isLoading && <BranchRowLoader />} |
| 85 | {isSuccess && mainBranch !== undefined && ( |
| 86 | <BranchRow |
| 87 | branch={mainBranch} |
| 88 | isGithubConnected={isGithubConnected} |
| 89 | label={ |
| 90 | <div className="flex items-center gap-x-2"> |
| 91 | <Shield size={14} strokeWidth={1.5} className="text-warning" /> |
| 92 | {mainBranch.name} |
| 93 | </div> |
| 94 | } |
| 95 | repo={repo} |
| 96 | rowActions={<MainBranchActions branch={mainBranch} repo={repo} />} |
| 97 | /> |
| 98 | )} |
| 99 | {isSuccess && mainBranch === undefined && ( |
| 100 | <div className="w-full flex items-center justify-between px-4 py-2.5 hover:bg-surface-100"> |
| 101 | <Link href={`/project/${projectRef}`} className="text-foreground block w-full"> |
| 102 | <div className="flex items-center gap-x-3"> |
| 103 | <Shield size={14} strokeWidth={1.5} className="text-warning" /> |
| 104 | main |
| 105 | </div> |
| 106 | </Link> |
| 107 | </div> |
| 108 | )} |
| 109 | </BranchManagementSection> |
| 110 | |
| 111 | {/* Persistent Branches Section */} |
| 112 | <BranchManagementSection header="Persistent branches"> |
| 113 | {(isLoading || isLoadingEntitlement) && <BranchLoader />} |
| 114 | {isSuccess && |
| 115 | !isLoadingEntitlement && |
| 116 | !hasAccessToPersistentBranching && |
| 117 | IS_PLATFORM && |
| 118 | persistentBranches.length === 0 && ( |
| 119 | <div className="px-6 py-10 flex items-center justify-between"> |
| 120 | <div className="flex flex-col gap-0.5"> |
| 121 | <p className="text-sm">Upgrade to unlock persistent branches</p> |
| 122 | <p className="text-sm text-foreground-lighter text-balance"> |
| 123 | Persistent branches are long-lived, cannot be reset, and are ideal for staging |
| 124 | environments. |
| 125 | </p> |
| 126 | </div> |
| 127 | <Button type="primary" asChild> |
| 128 | <Link href={`/org/${selectedOrg?.slug}/billing?panel=subscriptionPlan`}> |
| 129 | Upgrade |
| 130 | </Link> |
| 131 | </Button> |
| 132 | </div> |
| 133 | )} |
| 134 | {isSuccess && |
| 135 | !isLoadingEntitlement && |
| 136 | hasAccessToPersistentBranching && |
| 137 | persistentBranches.length === 0 && ( |
| 138 | <div className="flex items-center flex-col gap-0.5 justify-center w-full py-10"> |
| 139 | <p>No persistent branches</p> |
| 140 | <p className="text-foreground-lighter text-center text-balance"> |
| 141 | Persistent branches are long-lived, cannot be reset, and are ideal for staging |
| 142 | environments. |
| 143 | </p> |
| 144 | </div> |
| 145 | )} |
| 146 | {isSuccess && |
| 147 | !isLoadingEntitlement && |
| 148 | persistentBranches.map((branch) => { |
| 149 | return ( |
| 150 | <BranchRow |
| 151 | isGithubConnected={isGithubConnected} |
| 152 | key={branch.id} |
| 153 | repo={repo} |
| 154 | branch={branch} |
| 155 | rowActions={ |
| 156 | <PreviewBranchActions |
| 157 | branch={branch} |
| 158 | repo={repo} |
| 159 | onSelectDeleteBranch={() => onSelectDeleteBranch(branch)} |
| 160 | generateCreatePullRequestURL={generateCreatePullRequestURL} |
| 161 | /> |
| 162 | } |
| 163 | /> |
| 164 | ) |
| 165 | })} |
| 166 | </BranchManagementSection> |
| 167 | |
| 168 | {/* Ephemeral/Preview Branches Section */} |
| 169 | <BranchManagementSection header="Preview branches"> |
| 170 | {isLoading && <BranchLoader />} |
| 171 | {isSuccess && ephemeralBranches.length === 0 && ( |
| 172 | <PreviewBranchesEmptyState onSelectCreateBranch={onSelectCreateBranch} /> |
| 173 | )} |
| 174 | {isSuccess && |
| 175 | ephemeralBranches.map((branch) => { |
| 176 | return ( |
| 177 | <BranchRow |
| 178 | isGithubConnected={isGithubConnected} |
| 179 | key={branch.id} |
| 180 | repo={repo} |
| 181 | branch={branch} |
| 182 | rowActions={ |
| 183 | <PreviewBranchActions |
| 184 | branch={branch} |
| 185 | repo={repo} |
| 186 | onSelectDeleteBranch={() => onSelectDeleteBranch(branch)} |
| 187 | generateCreatePullRequestURL={generateCreatePullRequestURL} |
| 188 | /> |
| 189 | } |
| 190 | /> |
| 191 | ) |
| 192 | })} |
| 193 | </BranchManagementSection> |
| 194 | {/* Scheduled for deletion branches section */} |
| 195 | <BranchManagementSection header="Scheduled for deletion branches"> |
| 196 | {isLoading && <BranchLoader />} |
| 197 | {isSuccess && scheduledForDeletionBranches.length === 0 && ( |
| 198 | <div className="flex items-center flex-col gap-0.5 justify-center w-full py-10"> |
| 199 | <p className="text-foreground-lighter">No branches scheduled for deletion</p> |
| 200 | </div> |
| 201 | )} |
| 202 | {isSuccess && |
| 203 | scheduledForDeletionBranches.map((branch) => { |
| 204 | return ( |
| 205 | <BranchRow |
| 206 | isGithubConnected={isGithubConnected} |
| 207 | key={branch.id} |
| 208 | repo={repo} |
| 209 | branch={branch} |
| 210 | rowActions={ |
| 211 | <PreviewBranchActions |
| 212 | branch={branch} |
| 213 | repo={repo} |
| 214 | // If a scheduled for deletion branch is deleted, we force the deletion |
| 215 | onSelectDeleteBranch={() => onSelectDeleteBranch(branch)} |
| 216 | generateCreatePullRequestURL={generateCreatePullRequestURL} |
| 217 | /> |
| 218 | } |
| 219 | /> |
| 220 | ) |
| 221 | })} |
| 222 | </BranchManagementSection> |
| 223 | </> |
| 224 | ) |
| 225 | } |
| 226 | |
| 227 | // Row actions for preview branches (non-main) |
| 228 | const PreviewBranchActions = ({ |
| 229 | branch, |
| 230 | onSelectDeleteBranch, |
| 231 | generateCreatePullRequestURL, |
| 232 | }: { |
| 233 | branch: Branch |
| 234 | repo: string |
| 235 | onSelectDeleteBranch: () => void |
| 236 | generateCreatePullRequestURL: (branchName?: string) => string |
| 237 | }) => { |
| 238 | const queryClient = useQueryClient() |
| 239 | const { project_ref: branchRef, parent_project_ref: projectRef } = branch |
| 240 | |
| 241 | const { can: canDeleteBranches } = useAsyncCheckPermissions( |
| 242 | PermissionAction.DELETE, |
| 243 | 'preview_branches' |
| 244 | ) |
| 245 | const { can: canUpdateBranches } = useAsyncCheckPermissions( |
| 246 | PermissionAction.UPDATE, |
| 247 | 'preview_branches' |
| 248 | ) |
| 249 | // If user can update branches, they can restore branches |
| 250 | const canRestoreBranches = canUpdateBranches |
| 251 | |
| 252 | const { data } = useBranchQuery({ projectRef, branchRef }) |
| 253 | const isBranchActiveHealthy = data?.status === 'ACTIVE_HEALTHY' |
| 254 | const isPersistentBranch = branch.persistent |
| 255 | |
| 256 | const { hasAccess: hasAccessToPersistentBranching } = useCheckEntitlements('branching_persistent') |
| 257 | |
| 258 | const [showConfirmResetModal, setShowConfirmResetModal] = useState(false) |
| 259 | const [showBranchModeSwitch, setShowBranchModeSwitch] = useState(false) |
| 260 | const [ |
| 261 | showPersistentBranchDeleteConfirmationModal, |
| 262 | setShowPersistentBranchDeleteConfirmationModal, |
| 263 | ] = useState(false) |
| 264 | const [showEditBranchModal, setShowEditBranchModal] = useState(false) |
| 265 | |
| 266 | const { mutate: resetBranch, isPending: isResetting } = useBranchResetMutation({ |
| 267 | onSuccess() { |
| 268 | toast.success('Success! Please allow a few seconds for the branch to reset.') |
| 269 | setShowConfirmResetModal(false) |
| 270 | }, |
| 271 | }) |
| 272 | |
| 273 | const { mutate: updateBranch, isPending: isUpdatingBranch } = useBranchUpdateMutation({ |
| 274 | onSuccess() { |
| 275 | toast.success('Successfully updated branch') |
| 276 | setShowBranchModeSwitch(false) |
| 277 | if (projectRef) { |
| 278 | queryClient.invalidateQueries({ queryKey: branchKeys.list(projectRef) }) |
| 279 | } |
| 280 | }, |
| 281 | }) |
| 282 | const { mutate: restoreBranch } = useBranchRestoreMutation({ |
| 283 | onSuccess() { |
| 284 | toast.success('Success! Please allow a few minutes for the branch to restore.') |
| 285 | setShowBranchModeSwitch(false) |
| 286 | }, |
| 287 | }) |
| 288 | |
| 289 | const onRestoreBranch = () => { |
| 290 | restoreBranch({ branchRef, projectRef }) |
| 291 | } |
| 292 | |
| 293 | const onConfirmReset = () => { |
| 294 | resetBranch({ branchRef, projectRef }) |
| 295 | } |
| 296 | |
| 297 | const onTogglePersistent = () => { |
| 298 | updateBranch({ branchRef, projectRef, persistent: !branch.persistent }) |
| 299 | } |
| 300 | |
| 301 | const onDeleteBranch = (e: Event | React.MouseEvent<HTMLDivElement>) => { |
| 302 | if (isPersistentBranch) { |
| 303 | setShowPersistentBranchDeleteConfirmationModal(true) |
| 304 | } else { |
| 305 | e.stopPropagation() |
| 306 | onSelectDeleteBranch() |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | return ( |
| 311 | <> |
| 312 | <DropdownMenu> |
| 313 | <DropdownMenuTrigger asChild> |
| 314 | <Button |
| 315 | type="text" |
| 316 | icon={<MoreVertical />} |
| 317 | className="px-1" |
| 318 | onClick={(e) => e.stopPropagation()} |
| 319 | /> |
| 320 | </DropdownMenuTrigger> |
| 321 | <DropdownMenuContent className="w-56" side="bottom" align="end"> |
| 322 | <DropdownMenuItemTooltip |
| 323 | className="gap-x-2" |
| 324 | disabled={!canUpdateBranches || !isBranchActiveHealthy || isUpdatingBranch} |
| 325 | onSelect={(e) => { |
| 326 | e.stopPropagation() |
| 327 | setShowEditBranchModal(true) |
| 328 | }} |
| 329 | onClick={(e) => { |
| 330 | e.stopPropagation() |
| 331 | setShowEditBranchModal(true) |
| 332 | }} |
| 333 | tooltip={{ |
| 334 | content: { |
| 335 | side: 'left', |
| 336 | text: !canUpdateBranches |
| 337 | ? 'You need additional permissions to edit branches' |
| 338 | : !isBranchActiveHealthy |
| 339 | ? 'Branch is still initializing. Please wait for it to become healthy before editing.' |
| 340 | : undefined, |
| 341 | }, |
| 342 | }} |
| 343 | > |
| 344 | <Pencil size={14} /> Edit branch |
| 345 | </DropdownMenuItemTooltip> |
| 346 | |
| 347 | {!branch.deletion_scheduled_at && ( |
| 348 | <DropdownMenuItemTooltip |
| 349 | className="gap-x-2" |
| 350 | disabled={isResetting || !isBranchActiveHealthy} |
| 351 | onSelect={(e) => { |
| 352 | e.stopPropagation() |
| 353 | setShowConfirmResetModal(true) |
| 354 | }} |
| 355 | onClick={(e) => { |
| 356 | e.stopPropagation() |
| 357 | setShowConfirmResetModal(true) |
| 358 | }} |
| 359 | tooltip={{ |
| 360 | content: { |
| 361 | side: 'left', |
| 362 | text: !isBranchActiveHealthy |
| 363 | ? 'Branch is still initializing. Please wait for it to become healthy before resetting.' |
| 364 | : undefined, |
| 365 | }, |
| 366 | }} |
| 367 | > |
| 368 | <RefreshCw size={14} /> Reset branch |
| 369 | </DropdownMenuItemTooltip> |
| 370 | )} |
| 371 | |
| 372 | {!branch.deletion_scheduled_at && ( |
| 373 | <DropdownMenuItemTooltip |
| 374 | className="gap-x-2" |
| 375 | disabled={ |
| 376 | !isBranchActiveHealthy || (!branch.persistent && !hasAccessToPersistentBranching) |
| 377 | } |
| 378 | onSelect={(e) => { |
| 379 | e.stopPropagation() |
| 380 | setShowBranchModeSwitch(true) |
| 381 | }} |
| 382 | onClick={(e) => { |
| 383 | e.stopPropagation() |
| 384 | setShowBranchModeSwitch(true) |
| 385 | }} |
| 386 | tooltip={{ |
| 387 | content: { |
| 388 | side: 'left', |
| 389 | text: !isBranchActiveHealthy |
| 390 | ? 'Branch is still initializing. Please wait for it to become healthy before switching.' |
| 391 | : !branch.persistent && !hasAccessToPersistentBranching |
| 392 | ? 'Upgrade your plan to access persistent branches' |
| 393 | : undefined, |
| 394 | }, |
| 395 | }} |
| 396 | > |
| 397 | {branch.persistent ? ( |
| 398 | <> |
| 399 | <Clock size={14} /> Switch to preview |
| 400 | </> |
| 401 | ) : ( |
| 402 | <> |
| 403 | <Infinity size={14} className="scale-110" /> Switch to persistent |
| 404 | </> |
| 405 | )} |
| 406 | </DropdownMenuItemTooltip> |
| 407 | )} |
| 408 | |
| 409 | {/* Create PR if applicable */} |
| 410 | {branch.git_branch && branch.pr_number === undefined && ( |
| 411 | <DropdownMenuItem asChild className="gap-x-2"> |
| 412 | <a |
| 413 | target="_blank" |
| 414 | rel="noreferrer" |
| 415 | href={generateCreatePullRequestURL(branch.git_branch)} |
| 416 | onClick={(e) => e.stopPropagation()} |
| 417 | > |
| 418 | <ExternalLink size={14} /> Create pull request |
| 419 | </a> |
| 420 | </DropdownMenuItem> |
| 421 | )} |
| 422 | {branch.deletion_scheduled_at && ( |
| 423 | <DropdownMenuItemTooltip |
| 424 | className="gap-x-2" |
| 425 | disabled={!canRestoreBranches || branch.preview_project_status !== 'INACTIVE'} |
| 426 | onSelect={(e) => { |
| 427 | e.stopPropagation() |
| 428 | onRestoreBranch() |
| 429 | }} |
| 430 | onClick={(e) => { |
| 431 | e.stopPropagation() |
| 432 | onRestoreBranch() |
| 433 | }} |
| 434 | tooltip={{ |
| 435 | content: { |
| 436 | side: 'left', |
| 437 | text: !canRestoreBranches |
| 438 | ? 'You need additional permissions to restore branches' |
| 439 | : branch.preview_project_status !== 'INACTIVE' |
| 440 | ? 'Preview project is not fully paused or already coming up. Please wait for it to become fully paused before restoring.' |
| 441 | : undefined, |
| 442 | }, |
| 443 | }} |
| 444 | > |
| 445 | <Clock size={14} /> Restore branch |
| 446 | </DropdownMenuItemTooltip> |
| 447 | )} |
| 448 | |
| 449 | <DropdownMenuSeparator /> |
| 450 | |
| 451 | <DropdownMenuItemTooltip |
| 452 | className="gap-x-2" |
| 453 | disabled={!canDeleteBranches} |
| 454 | onSelect={onDeleteBranch} |
| 455 | onClick={onDeleteBranch} |
| 456 | tooltip={{ |
| 457 | content: { |
| 458 | side: 'left', |
| 459 | text: !canDeleteBranches |
| 460 | ? 'You need additional permissions to delete branches' |
| 461 | : undefined, |
| 462 | }, |
| 463 | }} |
| 464 | > |
| 465 | <Trash2 size={14} /> Delete branch |
| 466 | </DropdownMenuItemTooltip> |
| 467 | </DropdownMenuContent> |
| 468 | </DropdownMenu> |
| 469 | |
| 470 | <TextConfirmModal |
| 471 | variant="warning" |
| 472 | visible={showConfirmResetModal} |
| 473 | onCancel={() => setShowConfirmResetModal(false)} |
| 474 | onConfirm={onConfirmReset} |
| 475 | loading={isResetting} |
| 476 | title="Reset branch" |
| 477 | confirmLabel="Reset branch" |
| 478 | confirmPlaceholder="Type in name of branch" |
| 479 | confirmString={branch?.name ?? ''} |
| 480 | alert={{ |
| 481 | title: `Are you sure you want to reset the "${branch.name}" branch? All data will be deleted.`, |
| 482 | }} |
| 483 | /> |
| 484 | |
| 485 | <ConfirmationModal |
| 486 | variant="default" |
| 487 | visible={showBranchModeSwitch} |
| 488 | confirmLabel={branch.persistent ? 'Switch to preview' : 'Switch to persistent'} |
| 489 | title="Confirm branch mode switch" |
| 490 | loading={isUpdatingBranch} |
| 491 | onCancel={() => setShowBranchModeSwitch(false)} |
| 492 | onConfirm={onTogglePersistent} |
| 493 | > |
| 494 | <p className="text-sm text-foreground-light"> |
| 495 | Are you sure you want to switch the branch "{branch.name}" to{' '} |
| 496 | {branch.persistent ? 'preview' : 'persistent'}? |
| 497 | </p> |
| 498 | </ConfirmationModal> |
| 499 | |
| 500 | <ConfirmationModal |
| 501 | variant="warning" |
| 502 | visible={showPersistentBranchDeleteConfirmationModal} |
| 503 | confirmLabel={'Switch to preview'} |
| 504 | title="Branch must be switched to preview before deletion" |
| 505 | loading={isUpdatingBranch} |
| 506 | onCancel={() => setShowPersistentBranchDeleteConfirmationModal(false)} |
| 507 | onConfirm={onTogglePersistent} |
| 508 | > |
| 509 | <p className="text-sm text-foreground-light"> |
| 510 | You must switch the branch "{branch.name}" to preview before deleting it. |
| 511 | </p> |
| 512 | </ConfirmationModal> |
| 513 | |
| 514 | <EditBranchModal |
| 515 | branch={branch} |
| 516 | visible={showEditBranchModal} |
| 517 | onClose={() => setShowEditBranchModal(false)} |
| 518 | /> |
| 519 | </> |
| 520 | ) |
| 521 | } |
| 522 | |
| 523 | // Actions for main (production) branch |
| 524 | const MainBranchActions = ({ branch, repo }: { branch: Branch; repo: string }) => { |
| 525 | const { ref: projectRef } = useParams() |
| 526 | const { can: canUpdateBranches } = useAsyncCheckPermissions( |
| 527 | PermissionAction.UPDATE, |
| 528 | 'preview_branches' |
| 529 | ) |
| 530 | const [showEditBranchModal, setShowEditBranchModal] = useState(false) |
| 531 | |
| 532 | return ( |
| 533 | <> |
| 534 | <DropdownMenu> |
| 535 | <DropdownMenuTrigger asChild> |
| 536 | <Button type="text" icon={<MoreVertical />} className="px-1" /> |
| 537 | </DropdownMenuTrigger> |
| 538 | <DropdownMenuContent className="w-56" side="bottom" align="end"> |
| 539 | {repo ? ( |
| 540 | <Link passHref href={`/project/${projectRef}/settings/integrations`}> |
| 541 | <DropdownMenuItem asChild className="gap-x-2"> |
| 542 | <a>Change production branch</a> |
| 543 | </DropdownMenuItem> |
| 544 | </Link> |
| 545 | ) : ( |
| 546 | <DropdownMenuItem |
| 547 | className="gap-x-2" |
| 548 | disabled={!canUpdateBranches} |
| 549 | onSelect={() => setShowEditBranchModal(true)} |
| 550 | onClick={() => setShowEditBranchModal(true)} |
| 551 | > |
| 552 | <Pencil size={14} /> Edit Branch |
| 553 | </DropdownMenuItem> |
| 554 | )} |
| 555 | </DropdownMenuContent> |
| 556 | </DropdownMenu> |
| 557 | |
| 558 | <EditBranchModal |
| 559 | branch={branch} |
| 560 | visible={showEditBranchModal} |
| 561 | onClose={() => setShowEditBranchModal(false)} |
| 562 | /> |
| 563 | </> |
| 564 | ) |
| 565 | } |