github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/lib/components/repository/refDropdown.jsx (about) 1 import React, {useEffect, useRef, useState} from "react"; 2 3 import Form from "react-bootstrap/Form"; 4 import Alert from "react-bootstrap/Alert"; 5 import Button from "react-bootstrap/Button"; 6 import Badge from "react-bootstrap/Badge"; 7 import Overlay from "react-bootstrap/Overlay"; 8 import {ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, XIcon} from "@primer/octicons-react"; 9 import Popover from "react-bootstrap/Popover"; 10 11 import {tags, branches, commits} from '../../api'; 12 import {Nav} from "react-bootstrap"; 13 import {RefTypeBranch, RefTypeCommit, RefTypeTag} from "../../../constants"; 14 15 16 const RefSelector = ({ repo, selected, selectRef, withCommits, withWorkspace, withTags, amount = 300 }) => { 17 // used for ref pagination 18 const [pagination, setPagination] = useState({after: "", prefix: "", amount}); 19 const [refList, setRefs] = useState({loading: true, payload: null, error: null}); 20 const [refType, setRefType] = useState(selected && selected.type || RefTypeBranch) 21 useEffect(() => { 22 setRefs({loading: true, payload: null, error: null}); 23 const fetchRefs = async () => { 24 try { 25 let response; 26 if (refType === RefTypeTag) { 27 response = await tags.list(repo.id, pagination.prefix, pagination.after, pagination.amount); 28 } else { 29 response = await branches.list(repo.id, pagination.prefix, pagination.after, pagination.amount); 30 } 31 setRefs({loading: false, payload: response, error: null}); 32 } catch (error) { 33 setRefs({loading: false, payload: null, error: error}); 34 } 35 }; 36 fetchRefs(); 37 }, [refType, repo.id, pagination]) 38 39 // used for commit listing 40 const initialCommitList = {branch: selected, commits: null, loading: false}; 41 const [commitList, setCommitList] = useState(initialCommitList); 42 43 44 const form = ( 45 <div className="ref-filter-form"> 46 <Form onSubmit={e => { e.preventDefault(); }}> 47 <Form.Control type="text" placeholder={refType === RefTypeTag ? "Filter tags" : "Filter branches"} onChange={(e)=> { 48 setPagination({ 49 amount, 50 after: "", 51 prefix: e.target.value 52 }) 53 }}/> 54 </Form> 55 </div> 56 ); 57 const refTypeNav = withTags && <Nav variant="tabs" onSelect={setRefType} activeKey={refType} className="mt-2"> 58 <Nav.Item> 59 <Nav.Link eventKey={"branch"}>Branches</Nav.Link> 60 </Nav.Item> 61 <Nav.Item> 62 <Nav.Link eventKey={"tag"}>Tags</Nav.Link> 63 </Nav.Item> 64 </Nav> 65 66 if (refList.loading) { 67 return ( 68 <div className="ref-selector"> 69 {form} 70 {refTypeNav} 71 <p>Loading...</p> 72 </div> 73 ); 74 } 75 76 if (refList.error) { 77 return ( 78 <div className="ref-selector"> 79 {form} 80 {refTypeNav} 81 <Alert variant="danger">{refList.error}</Alert> 82 </div> 83 ); 84 } 85 86 if (commitList.commits !== null) { 87 return ( 88 <CommitList 89 withWorkspace={withWorkspace} 90 commits={commitList.commits} 91 branch={commitList.branch} 92 selectRef={selectRef} 93 reset={() => { 94 setCommitList(initialCommitList); 95 }}/> 96 ); 97 } 98 99 100 const results = refList.payload.results; 101 102 return ( 103 <div className="ref-selector"> 104 {form} 105 {refTypeNav} 106 <div className="ref-scroller"> 107 {(results && results.length > 0) ? ( 108 <> 109 <ul className="list-group ref-list"> 110 {results.map(namedRef => ( 111 <RefEntry key={namedRef.id} repo={repo} refType={refType} namedRef={namedRef.id} selectRef={selectRef} selected={selected} withCommits={refType !== RefTypeTag && withCommits} logCommits={async () => { 112 const data = await commits.log(repo.id, namedRef.id) 113 setCommitList({...commitList, branch: namedRef.id, commits: data.results}); 114 }}/> 115 ))} 116 </ul> 117 <Paginator results={refList.payload.results} pagination={refList.payload.pagination} from={pagination.after} onPaginate={(after) => { 118 setPagination({after}) 119 }}/> 120 </> 121 ) : ( 122 <p className="text-center mt-3"><small>No references found</small></p> 123 )} 124 125 </div> 126 </div> 127 ); 128 }; 129 130 const CommitList = ({ commits, selectRef, reset, branch, withWorkspace }) => { 131 const getMessage = commit => { 132 if (!commit.message) { 133 return 'repository epoch'; 134 } 135 136 if (commit.message.length > 60) { 137 return commit.message.substr(0, 40) + '...'; 138 } 139 140 return commit.message; 141 }; 142 143 return ( 144 <div className="ref-selector"> 145 <h5>{branch}</h5> 146 <div className="ref-scroller"> 147 <ul className="list-group ref-list"> 148 {(withWorkspace) ? ( 149 <li className="list-group-item" key={branch}> 150 <Button variant="link" onClick={() => { 151 selectRef({id: branch, type: RefTypeBranch}); 152 }}><em>{branch}{'\''}s Workspace (uncommitted changes)</em></Button> 153 </li> 154 ) : (<span/>)} 155 {commits.map(commit => ( 156 <li className="list-group-item" key={commit.id}> 157 <Button variant="link" onClick={() => { 158 selectRef({id: commit.id, type: RefTypeCommit}); 159 }}>{getMessage(commit)} </Button> 160 <div className="actions"> 161 <Badge variant="light">{commit.id.substr(0, 12)}</Badge> 162 </div> 163 </li> 164 ))} 165 </ul> 166 <p className="ref-paginator"> 167 <Button variant="link" size="sm" onClick={reset}>Back</Button> 168 </p> 169 </div> 170 </div> 171 ); 172 }; 173 174 const RefEntry = ({repo, namedRef, refType, selectRef, selected, logCommits, withCommits}) => { 175 return ( 176 <li className="list-group-item" key={namedRef}> 177 {(!!selected && namedRef === selected.id) ? 178 <strong>{namedRef}</strong> : 179 <Button variant="link" onClick={() => { 180 selectRef({id: namedRef, type: refType}); 181 }}>{namedRef}</Button> 182 } 183 <div className="actions"> 184 {(refType === RefTypeBranch && namedRef === repo.default_branch) ? (<Badge variant="info">Default</Badge>) : <span/>} 185 {(withCommits) ? ( 186 <Button onClick={logCommits} size="sm" variant="link"> 187 <ChevronRightIcon/> 188 </Button> 189 ) : (<span/>)} 190 </div> 191 </li> 192 ); 193 }; 194 195 const Paginator = ({ pagination, onPaginate, results, from }) => { 196 const next = (results.length) ? results[results.length-1].id : ""; 197 198 if (!pagination.has_more && from === "") return (<span/>); 199 200 return ( 201 <p className="ref-paginator"> 202 {(from !== "") ? 203 (<Button size={"sm"} variant="link" onClick={() => { onPaginate(""); }}>Reset</Button>) : 204 (<span/>) 205 } 206 {' '} 207 {(pagination.has_more) ? 208 (<Button size={"sm"} variant="link" onClick={() => { onPaginate(next); }}>Next...</Button>) : 209 (<span/>) 210 } 211 </p> 212 ); 213 }; 214 215 const RefDropdown = ({ repo, selected, selectRef, onCancel, variant="light", prefix = '', emptyText = '', withCommits = true, withWorkspace = true, withTags = true }) => { 216 217 const [show, setShow] = useState(false); 218 const target = useRef(null); 219 220 221 const popover = ( 222 <Overlay target={target.current} show={show} placement="bottom" rootClose={true} onHide={() => setShow(false)}> 223 <Popover className="ref-popover"> 224 <Popover.Body> 225 <RefSelector 226 repo={repo} 227 withCommits={withCommits} 228 withWorkspace={withWorkspace} 229 withTags={withTags} 230 selected={selected} 231 selectRef={(ref) => { 232 selectRef(ref); 233 setShow(false); 234 }}/> 235 </Popover.Body> 236 </Popover> 237 </Overlay> 238 ); 239 240 const cancelButton = (!!onCancel && !!selected) ? (<Button onClick={() => { 241 setShow(false); 242 onCancel(); 243 }} variant={variant}><XIcon/></Button>) : (<span/>); 244 245 if (!selected) { 246 return ( 247 <> 248 <Button ref={target} variant={variant} onClick={()=> { setShow(!show) }}> 249 {emptyText} {show ? <ChevronUpIcon/> : <ChevronDownIcon/>} 250 </Button> 251 {cancelButton} 252 {popover} 253 </> 254 ); 255 } 256 257 const showId = (ref) => { 258 if (!ref) 259 return '' 260 if (ref.type === RefTypeCommit) 261 return ref.id.substr(0, 12) 262 return ref.id 263 } 264 265 const title = prefix + (!!selected) ? `${prefix} ${selected.type}: ` : ''; 266 return ( 267 <> 268 <Button ref={target} variant={variant} onClick={()=> { setShow(!show) }}> 269 {title} <strong>{showId(selected)}</strong> {show ? <ChevronUpIcon/> : <ChevronDownIcon/>} 270 </Button> 271 {cancelButton} 272 {popover} 273 </> 274 ); 275 }; 276 277 export default RefDropdown;