github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/pages/repositories/repository/branches.jsx (about) 1 import React, {useEffect, useMemo, useRef, useState} from "react"; 2 import { useOutletContext } from "react-router-dom"; 3 import { 4 GitBranchIcon, 5 LinkIcon, 6 PackageIcon, 7 TrashIcon 8 } from "@primer/octicons-react"; 9 import ButtonGroup from "react-bootstrap/ButtonGroup"; 10 import Button from "react-bootstrap/Button"; 11 import Card from "react-bootstrap/Card"; 12 import ListGroup from "react-bootstrap/ListGroup"; 13 14 import {branches} from "../../../lib/api"; 15 16 import { 17 ActionGroup, 18 ActionsBar, ClipboardButton, 19 AlertError, LinkButton, 20 Loading, PrefixSearchWidget, RefreshButton 21 } from "../../../lib/components/controls"; 22 import {useRefs} from "../../../lib/hooks/repo"; 23 import {useAPIWithPagination} from "../../../lib/hooks/api"; 24 import {Paginator} from "../../../lib/components/pagination"; 25 import Modal from "react-bootstrap/Modal"; 26 import Form from "react-bootstrap/Form"; 27 import RefDropdown from "../../../lib/components/repository/refDropdown"; 28 import Badge from "react-bootstrap/Badge"; 29 import {ConfirmationButton} from "../../../lib/components/modals"; 30 import Alert from "react-bootstrap/Alert"; 31 import {Link} from "../../../lib/components/nav"; 32 import {useRouter} from "../../../lib/hooks/router"; 33 import {RepoError} from "./error"; 34 35 const ImportBranchName = 'import-from-inventory'; 36 37 38 const BranchWidget = ({ repo, branch, onDelete }) => { 39 40 const buttonVariant = "outline-dark"; 41 const isDefault = repo.default_branch === branch.id; 42 let deleteMsg = ( 43 <> 44 Are you sure you wish to delete branch <strong>{branch.id}</strong> ? 45 </> 46 ); 47 if (branch.id === ImportBranchName) { 48 deleteMsg = ( 49 <> 50 <p>{deleteMsg}</p> 51 <Alert variant="warning"><strong>Warning</strong> this is a system branch used for importing data to lakeFS</Alert> 52 </> 53 ); 54 } 55 56 return ( 57 <ListGroup.Item> 58 <div className="clearfix"> 59 <div className="float-start"> 60 <h6> 61 <Link href={{ 62 pathname: '/repositories/:repoId/objects', 63 params: {repoId: repo.id}, 64 query: {ref: branch.id} 65 }}> 66 {branch.id} 67 </Link> 68 69 {isDefault && 70 <> 71 {' '} 72 <Badge variant="info">Default</Badge> 73 </>} 74 </h6> 75 </div> 76 77 78 <div className="float-end"> 79 {!isDefault && 80 <ButtonGroup className="commit-actions"> 81 <ConfirmationButton 82 variant="outline-danger" 83 disabled={isDefault} 84 msg={deleteMsg} 85 tooltip="delete branch" 86 onConfirm={() => { 87 branches.delete(repo.id, branch.id) 88 .catch(err => alert(err)) 89 .then(() => onDelete(branch.id)) 90 }} 91 > 92 <TrashIcon/> 93 </ConfirmationButton> 94 </ButtonGroup> 95 } 96 97 <ButtonGroup className="branch-actions ms-2"> 98 <LinkButton 99 href={{ 100 pathname: '/repositories/:repoId/commits/:commitId', 101 params:{repoId: repo.id, commitId: branch.commit_id}, 102 }} 103 buttonVariant="outline-dark" 104 tooltip="View referenced commit"> 105 {branch.commit_id.substr(0, 12)} 106 </LinkButton> 107 <ClipboardButton variant={buttonVariant} text={branch.id} tooltip="Copy ID to clipboard"/> 108 <ClipboardButton variant={buttonVariant} text={`lakefs://${repo.id}/${branch.id}`} tooltip="Copy URI to clipboard" icon={<LinkIcon/>}/> 109 <ClipboardButton variant={buttonVariant} text={`s3://${repo.id}/${branch.id}`} tooltip="Copy S3 URI to clipboard" icon={<PackageIcon/>}/> 110 </ButtonGroup> 111 </div> 112 </div> 113 </ListGroup.Item> 114 ); 115 }; 116 117 118 const CreateBranchButton = ({ repo, variant = "success", onCreate = null, readOnly = false, children }) => { 119 const [show, setShow] = useState(false); 120 const [disabled, setDisabled] = useState(false); 121 const [error, setError] = useState(null); 122 const textRef = useRef(null); 123 const defaultBranch = useMemo( 124 () => ({ id: repo.default_branch, type: "branch"}), 125 [repo.default_branch]); 126 const [selectedBranch, setSelectedBranch] = useState(defaultBranch); 127 128 129 const hide = () => { 130 if (disabled) return; 131 setShow(false); 132 }; 133 134 const display = () => { 135 setShow(true); 136 }; 137 138 const onSubmit = async () => { 139 setDisabled(true); 140 const branchId = textRef.current.value; 141 const sourceRef = selectedBranch.id; 142 143 try { 144 await branches.create(repo.id, branchId, sourceRef); 145 setError(false); 146 setDisabled(false); 147 setShow(false); 148 onCreate(); 149 } catch (err) { 150 setError(err); 151 setDisabled(false); 152 } 153 }; 154 155 return ( 156 <> 157 <Modal show={show} onHide={hide} enforceFocus={false}> 158 <Modal.Header closeButton> 159 Create Branch 160 </Modal.Header> 161 <Modal.Body> 162 163 <Form onSubmit={(e) => { 164 onSubmit(); 165 e.preventDefault(); 166 }}> 167 <Form.Group controlId="name" className="mb-3"> 168 <Form.Control type="text" placeholder="Branch Name" name="text" ref={textRef}/> 169 </Form.Group> 170 <Form.Group controlId="source" className="mb-3"> 171 <RefDropdown 172 repo={repo} 173 emptyText={'Select Source Branch'} 174 prefix={'From '} 175 selected={selectedBranch} 176 selectRef={(refId) => { 177 setSelectedBranch(refId); 178 }} 179 withCommits={true} 180 withWorkspace={false}/> 181 </Form.Group> 182 </Form> 183 184 {!!error && <AlertError error={error}/>} 185 </Modal.Body> 186 <Modal.Footer> 187 <Button variant="secondary" disabled={disabled} onClick={hide}> 188 Cancel 189 </Button> 190 <Button variant="success" onClick={onSubmit} disabled={disabled}> 191 Create 192 </Button> 193 </Modal.Footer> 194 </Modal> 195 <Button variant={variant} disabled={readOnly} onClick={display}>{children}</Button> 196 </> 197 ); 198 }; 199 200 201 const BranchList = ({ repo, prefix, after, onPaginate }) => { 202 const router = useRouter() 203 const [refresh, setRefresh] = useState(true); 204 const { results, error, loading, nextPage } = useAPIWithPagination(async () => { 205 return branches.list(repo.id, prefix, after); 206 }, [repo.id, refresh, prefix, after]); 207 208 const doRefresh = () => setRefresh(!refresh); 209 210 let content; 211 212 if (loading) content = <Loading/>; 213 else if (error) content = <AlertError error={error}/>; 214 else content = ( 215 <> 216 <Card> 217 <ListGroup variant="flush"> 218 {results.map(branch => ( 219 <BranchWidget key={branch.id} repo={repo} branch={branch} onDelete={doRefresh}/> 220 ))} 221 </ListGroup> 222 </Card> 223 <Paginator onPaginate={onPaginate} nextPage={nextPage} after={after}/> 224 </> 225 ); 226 227 return ( 228 <div className="mb-5"> 229 <ActionsBar> 230 <ActionGroup orientation="right"> 231 232 <PrefixSearchWidget 233 defaultValue={router.query.prefix} 234 text="Find branch" 235 onFilter={prefix => { 236 const query = {prefix}; 237 router.push({pathname: '/repositories/:repoId/branches', params: {repoId: repo.id}, query}); 238 }}/> 239 240 <RefreshButton onClick={doRefresh}/> 241 242 <CreateBranchButton repo={repo} readOnly={repo?.read_only} variant="success" onCreate={doRefresh}> 243 <GitBranchIcon/> Create Branch 244 </CreateBranchButton> 245 246 </ActionGroup> 247 </ActionsBar> 248 {content} 249 <div className={"mt-2"}> 250 lakeFS uses a Git-like branching model. <a href="https://docs.lakefs.io/understand/branching-model.html" target="_blank" rel="noopener noreferrer">Learn more.</a> 251 </div> 252 </div> 253 ); 254 }; 255 256 const BranchesContainer = () => { 257 const router = useRouter() 258 const { repo, loading, error } = useRefs(); 259 const { after } = router.query; 260 const routerPfx = (router.query.prefix) ? router.query.prefix : ""; 261 262 if (loading) return <Loading/>; 263 if (error) return <RepoError error={error}/>; 264 265 return ( 266 <BranchList 267 repo={repo} 268 after={(after) ? after : ""} 269 prefix={routerPfx} 270 onPaginate={after => { 271 const query = {after}; 272 if (router.query.prefix) query.prefix = router.query.prefix; 273 router.push({pathname: '/repositories/:repoId/branches', params: {repoId: repo.id}, query}); 274 }}/> 275 ); 276 }; 277 278 279 const RepositoryBranchesPage = () => { 280 const [setActivePage] = useOutletContext(); 281 useEffect(() => setActivePage("branches"), [setActivePage]); 282 return <BranchesContainer />; 283 } 284 285 export default RepositoryBranchesPage;