github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/pages/repositories/repository/changes.jsx (about) 1 import React, {useEffect, useRef, useState} from "react"; 2 import { useOutletContext } from "react-router-dom"; 3 import {GitCommitIcon, HistoryIcon,} from "@primer/octicons-react"; 4 5 import Modal from "react-bootstrap/Modal"; 6 import Form from "react-bootstrap/Form"; 7 8 import Alert from "react-bootstrap/Alert"; 9 import Button from "react-bootstrap/Button"; 10 11 import {branches, commits, refs} from "../../../lib/api"; 12 import {useAPIWithPagination} from "../../../lib/hooks/api"; 13 import {useRefs} from "../../../lib/hooks/repo"; 14 import {ConfirmationModal} from "../../../lib/components/modals"; 15 import {ActionGroup, ActionsBar, AlertError, Loading, RefreshButton} from "../../../lib/components/controls"; 16 import RefDropdown from "../../../lib/components/repository/refDropdown"; 17 import {formatAlertText} from "../../../lib/components/repository/errors"; 18 import {ChangesTreeContainer, MetadataFields} from "../../../lib/components/repository/changes"; 19 import {useRouter} from "../../../lib/hooks/router"; 20 import {URINavigator} from "../../../lib/components/repository/tree"; 21 import {RepoError} from "./error"; 22 23 24 const CommitButton = ({repo, onCommit, enabled = false}) => { 25 26 const textRef = useRef(null); 27 28 const [committing, setCommitting] = useState(false) 29 const [show, setShow] = useState(false) 30 const [metadataFields, setMetadataFields] = useState([]) 31 const hide = () => { 32 if (committing) return; 33 setShow(false) 34 } 35 36 const onSubmit = () => { 37 const message = textRef.current.value; 38 const metadata = {}; 39 metadataFields.forEach(pair => metadata[pair.key] = pair.value) 40 setCommitting(true) 41 onCommit({message, metadata}, () => { 42 setCommitting(false) 43 setShow(false); 44 }) 45 }; 46 47 const alertText = formatAlertText(repo.id, null); 48 return ( 49 <> 50 <Modal show={show} onHide={hide} size="lg"> 51 <Modal.Header closeButton> 52 <Modal.Title>Commit Changes</Modal.Title> 53 </Modal.Header> 54 <Modal.Body> 55 <Form className="mb-2" onSubmit={(e) => { 56 onSubmit(); 57 e.preventDefault(); 58 }}> 59 <Form.Group controlId="message" className="mb-3"> 60 <Form.Control type="text" placeholder="Commit Message" ref={textRef}/> 61 </Form.Group> 62 63 <MetadataFields metadataFields={metadataFields} setMetadataFields={setMetadataFields}/> 64 </Form> 65 {(alertText) ? (<Alert variant="danger">{alertText}</Alert>) : (<span/>)} 66 </Modal.Body> 67 <Modal.Footer> 68 <Button variant="secondary" disabled={committing} onClick={hide}> 69 Cancel 70 </Button> 71 <Button variant="success" disabled={committing} onClick={onSubmit}> 72 Commit Changes 73 </Button> 74 </Modal.Footer> 75 </Modal> 76 <Button variant="success" disabled={!enabled} onClick={() => setShow(true)}> 77 <GitCommitIcon/> Commit Changes{' '} 78 </Button> 79 </> 80 ); 81 } 82 83 84 const RevertButton = ({onRevert, enabled = false}) => { 85 const [show, setShow] = useState(false) 86 const hide = () => setShow(false) 87 88 return ( 89 <> 90 <ConfirmationModal 91 show={show} 92 onHide={hide} 93 msg="Are you sure you want to revert all uncommitted changes?" 94 onConfirm={() => { 95 onRevert() 96 hide() 97 }}/> 98 <Button variant="light" disabled={!enabled} onClick={() => setShow(true)}> 99 <HistoryIcon/> Revert 100 </Button> 101 </> 102 ); 103 } 104 105 export async function appendMoreResults(resultsState, prefix, afterUpdated, setAfterUpdated, setResultsState, getMore) { 106 let resultsFiltered = resultsState.results 107 if (resultsState.prefix !== prefix) { 108 // prefix changed, need to delete previous results 109 setAfterUpdated("") 110 resultsFiltered = [] 111 } 112 113 if (resultsFiltered.length > 0 && resultsFiltered.at(-1).path > afterUpdated) { 114 // results already cached 115 return {prefix: prefix, results: resultsFiltered, pagination: resultsState.pagination} 116 } 117 118 const {results, pagination} = await getMore() 119 setResultsState({prefix: prefix, results: resultsFiltered.concat(results), pagination: pagination}) 120 return {results: resultsState.results, pagination: pagination} 121 } 122 123 const ChangesBrowser = ({repo, reference, prefix, onSelectRef, }) => { 124 const [actionError, setActionError] = useState(null); 125 const [internalRefresh, setInternalRefresh] = useState(true); 126 const [afterUpdated, setAfterUpdated] = useState(""); // state of pagination of the item's children 127 const [resultsState, setResultsState] = useState({prefix: prefix, results:[], pagination:{}}); // current retrieved children of the item 128 129 const delimiter = '/' 130 131 const getMoreUncommittedChanges = (afterUpdated, path, useDelimiter= true, amount = -1) => { 132 return refs.changes(repo.id, reference.id, afterUpdated, path, useDelimiter ? delimiter : "", amount > 0 ? amount : undefined) 133 } 134 135 const { error, loading, nextPage } = useAPIWithPagination(async () => { 136 if (!repo) return 137 return await appendMoreResults(resultsState, prefix, afterUpdated, setAfterUpdated, setResultsState, 138 () => refs.changes(repo.id, reference.id, afterUpdated, prefix, delimiter)); 139 }, [repo.id, reference.id, internalRefresh, afterUpdated, delimiter, prefix]) 140 141 const results = resultsState.results 142 143 const refresh = () => { 144 setResultsState({prefix: prefix, results:[], pagination:{}}) 145 setInternalRefresh(!internalRefresh) 146 } 147 148 149 if (error) return <AlertError error={error}/> 150 if (loading) return <Loading/> 151 152 let onReset = async (entry) => { 153 branches 154 .reset(repo.id, reference.id, {type: entry.path_type, path: entry.path}) 155 .then(refresh) 156 .catch(error => { 157 setActionError(error) 158 }) 159 } 160 161 let onNavigate = (entry) => { 162 return { 163 pathname: `/repositories/:repoId/changes`, 164 params: {repoId: repo.id}, 165 query: { 166 ref: reference.id, 167 prefix: entry.path, 168 } 169 } 170 } 171 172 const uriNavigator = <URINavigator path={prefix} reference={reference} repo={repo} 173 pathURLBuilder={(params, query) => { 174 return { 175 pathname: '/repositories/:repoId/changes', 176 params: params, 177 query: {ref: reference.id, prefix: query.path ?? ""}, 178 }}}/> 179 const changesTreeMessage = <p>Showing changes for branch <strong>{reference.id}</strong></p> 180 const committedRef = reference.id + "@" 181 const uncommittedRef = reference.id 182 183 const actionErrorDisplay = (actionError) ? 184 <AlertError error={actionError} onDismiss={() => setActionError(null)}/> : <></> 185 186 return ( 187 <> 188 <ActionsBar> 189 <ActionGroup orientation="left"> 190 <RefDropdown 191 emptyText={'Select Branch'} 192 repo={repo} 193 selected={(reference) ? reference : null} 194 withCommits={false} 195 withWorkspace={false} 196 withTags={false} 197 selectRef={onSelectRef} 198 /> 199 </ActionGroup> 200 201 <ActionGroup orientation="right"> 202 203 <RefreshButton onClick={refresh}/> 204 205 <RevertButton enabled={results.length > 0 && !repo?.read_only} onRevert={() => { 206 branches.reset(repo.id, reference.id, {type: 'reset'}) 207 .then(refresh) 208 .catch(error => setActionError(error)) 209 }}/> 210 <CommitButton repo={repo} enabled={results.length > 0 && !repo?.read_only} onCommit={async (commitDetails, done) => { 211 try { 212 await commits.commit(repo.id, reference.id, commitDetails.message, commitDetails.metadata); 213 setActionError(null); 214 refresh(); 215 } catch (err) { 216 setActionError(err); 217 } 218 done(); 219 }}/> 220 </ActionGroup> 221 </ActionsBar> 222 223 {actionErrorDisplay} 224 <ChangesTreeContainer results={results} delimiter={delimiter} 225 uriNavigator={uriNavigator} leftDiffRefID={committedRef} rightDiffRefID={uncommittedRef} 226 repo={repo} reference={reference} internalReferesh={internalRefresh} prefix={prefix} 227 getMore={getMoreUncommittedChanges} 228 loading={loading} nextPage={nextPage} setAfterUpdated={setAfterUpdated} 229 onNavigate={onNavigate} onRevert={onReset} changesTreeMessage={changesTreeMessage}/> 230 </> 231 ) 232 } 233 234 const ChangesContainer = () => { 235 const router = useRouter(); 236 const {repo, reference, loading, error} = useRefs() 237 const {prefix} = router.query 238 239 if (loading) return <Loading/> 240 if (error) return <RepoError error={error}/> 241 242 return ( 243 <ChangesBrowser 244 prefix={(prefix) ? prefix : ""} 245 repo={repo} 246 reference={reference} 247 onSelectRef={ref => router.push({ 248 pathname: `/repositories/:repoId/changes`, 249 params: {repoId: repo.id}, 250 query: { 251 ref: ref.id, 252 } 253 })} 254 /> 255 ) 256 } 257 258 const RepositoryChangesPage = () => { 259 const [setActivePage] = useOutletContext(); 260 useEffect(() => setActivePage('changes'), [setActivePage]); 261 return <ChangesContainer />; 262 } 263 264 export default RepositoryChangesPage;