github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/lib/components/repository/changes.jsx (about) 1 import React, {useCallback, useState} from "react"; 2 3 import {ClockIcon, PlusIcon, XIcon} from "@primer/octicons-react"; 4 5 import {useAPIWithPagination} from "../../hooks/api"; 6 import {AlertError} from "../controls"; 7 import {ObjectsDiff} from "./ObjectsDiff"; 8 import {ObjectTreeEntryRow, PrefixTreeEntryRow} from "./treeRows"; 9 import Alert from "react-bootstrap/Alert"; 10 import Button from "react-bootstrap/Button"; 11 import Card from "react-bootstrap/Card"; 12 import Table from "react-bootstrap/Table"; 13 import {refs} from "../../api"; 14 import Form from "react-bootstrap/Form"; 15 import Row from "react-bootstrap/Row"; 16 import Col from "react-bootstrap/Col"; 17 18 /** 19 * Tree item is a node in the tree view. It can be expanded to multiple TreeEntryRow: 20 * 1. A single TreeEntryRow for the current prefix (or entry for leaves). 21 * 2. Multiple TreeItem as children, each representing another tree node. 22 * @param entry The entry the TreeItem is representing, could be either an object or a prefix. 23 * @param repo Repository 24 * @param reference commitID / branch 25 * @param leftDiffRefID commitID / branch 26 * @param rightDiffRefID commitID / branch 27 * @param internalRefresh to be called when the page refreshes manually 28 * @param onRevert to be called when an object/prefix is requested to be reverted 29 * @param delimiter objects delimiter ('' or '/') 30 * @param after all entries must be greater than after 31 * @param relativeTo prefix of the parent item ('' for root elements) 32 * @param {(after : string, path : string, useDelimiter :? boolean, amount :? number) => Promise<any> } getMore callback to be called when more items need to be rendered 33 */ 34 export const TreeItemRow = ({ entry, repo, reference, leftDiffRefID, rightDiffRefID, internalRefresh, onRevert, onNavigate, delimiter, relativeTo, getMore, 35 depth=0}) => { 36 const [dirExpanded, setDirExpanded] = useState(false); // state of a non-leaf item expansion 37 const [afterUpdated, setAfterUpdated] = useState(""); // state of pagination of the item's children 38 const [resultsState, setResultsState] = useState({results:[], pagination:{}}); // current retrieved children of the item 39 const [diffExpanded, setDiffExpanded] = useState(false); // state of a leaf item expansion 40 41 const { error, loading, nextPage } = useAPIWithPagination(async () => { 42 if (!dirExpanded) return 43 if (!repo) return 44 45 if (resultsState.results.length > 0 && resultsState.results.at(-1).path > afterUpdated) { 46 // results already cached 47 return {results:resultsState.results, pagination: resultsState.pagination} 48 } 49 50 const { results, pagination } = await getMore(afterUpdated, entry.path) 51 setResultsState({results: resultsState.results.concat(results), pagination: pagination}) 52 return {results:resultsState.results, pagination: pagination} 53 }, [repo.id, reference.id, internalRefresh, afterUpdated, entry.path, delimiter, dirExpanded]) 54 55 const results = resultsState.results 56 if (error) 57 return <tr><td><AlertError error={error}/></td></tr> 58 59 if (loading && results.length === 0) { 60 return <ObjectTreeEntryRow key={entry.path + "entry-row"} entry={entry} loading={true} relativeTo={relativeTo} 61 depth={depth} onRevert={onRevert} repo={repo} reference={reference} 62 getMore={getMore}/> 63 } 64 if (entry.path_type === "object") { 65 return <> 66 <ObjectTreeEntryRow key={entry.path + "entry-row"} entry={entry} relativeTo={relativeTo} 67 depth={depth === 0 ? 0 : depth + 1} onRevert={onRevert} repo={repo} 68 diffExpanded={diffExpanded} onClickExpandDiff={() => setDiffExpanded(!diffExpanded)}/> 69 {diffExpanded && <tr key={"row-" + entry.path} className={"leaf-entry-row"}> 70 <td className="objects-diff" colSpan={4}> 71 <ObjectsDiff 72 diffType={entry.type} 73 repoId={repo.id} 74 leftRef={leftDiffRefID} 75 rightRef={rightDiffRefID} 76 path={entry.path} 77 /> 78 {loading && <ClockIcon/>} 79 </td> 80 </tr> 81 } 82 </> 83 } 84 // entry is a common prefix 85 return <> 86 <PrefixTreeEntryRow key={entry.path + "entry-row"} entry={entry} dirExpanded={dirExpanded} relativeTo={relativeTo} depth={depth} onClick={() => setDirExpanded(!dirExpanded)} onRevert={onRevert} onNavigate={onNavigate} getMore={getMore} repo={repo} reference={reference}/> 87 {dirExpanded && results && 88 results.map(child => 89 (<TreeItemRow key={child.path + "-item"} entry={child} repo={repo} reference={reference} leftDiffRefID={leftDiffRefID} rightDiffRefID={rightDiffRefID} onRevert={onRevert} onNavigate={onNavigate} 90 internalReferesh={internalRefresh} delimiter={delimiter} depth={depth + 1} 91 relativeTo={entry.path} getMore={getMore} 92 />))} 93 {(!!nextPage || loading) && 94 <TreeEntryPaginator path={entry.path} depth={depth} loading={loading} nextPage={nextPage} 95 setAfterUpdated={setAfterUpdated}/> 96 } 97 </> 98 } 99 100 export const TreeEntryPaginator = ({ path, setAfterUpdated, nextPage, depth=0, loading=false }) => { 101 let pathSectionText = "Load more results ..."; 102 if (path !== ""){ 103 pathSectionText = `Load more results for prefix ${path} ....` 104 } 105 return ( 106 <tr key={"row-" + path} 107 className={"tree-entry-row diff-more"} 108 onClick={() => setAfterUpdated(nextPage)} 109 > 110 <td className="diff-indicator"/> 111 <td className="tree-path"> 112 <span style={{marginLeft: depth * 20 + "px",color:"#007bff"}}> 113 {loading && <ClockIcon/>} 114 {pathSectionText} 115 </span> 116 </td> 117 <td/> 118 </tr> 119 ); 120 }; 121 122 /** 123 * A container component for entries that represent a diff between refs. This container is used by the compare, commit changes, 124 * and uncommitted changes views. 125 * 126 * @param results to be displayed in the changes tree container 127 * @param delimiter objects delimiter ('' or '/') 128 * @param uriNavigator to navigate in the page using the changes container 129 * @param leftDiffRefID commitID / branch 130 * @param rightDiffRefID commitID / branch 131 * @param repo Repository 132 * @param reference commitID / branch 133 * @param internalRefresh to be called when the page refreshes manually 134 * @param prefix for which changes are displayed 135 * @param getMore to be called when requesting more diff results for a prefix 136 * @param loading of API response state to get changes 137 * @param nextPage of API response state to get changes 138 * @param setAfterUpdated state of pagination of the item's children 139 * @param onNavigate to be called when navigating to a prefix 140 * @param onRevert to be called when an object/prefix is requested to be reverted 141 * @param changesTreeMessage 142 */ 143 export const ChangesTreeContainer = ({results, delimiter, uriNavigator, 144 leftDiffRefID, rightDiffRefID, repo, reference, internalRefresh, prefix, 145 getMore, loading, nextPage, setAfterUpdated, onNavigate, onRevert, 146 changesTreeMessage= ""}) => { 147 if (results.length === 0) { 148 return <div className="tree-container"> 149 <Alert variant="info">No changes</Alert> 150 </div> 151 } else { 152 return <div className="tree-container"> 153 <div>{changesTreeMessage}</div> 154 <Card> 155 <Card.Header> 156 <span className="float-start"> 157 {(delimiter !== "") && uriNavigator} 158 </span> 159 </Card.Header> 160 <Card.Body> 161 <Table borderless size="sm"> 162 <tbody> 163 {results.map(entry => { 164 return ( 165 <TreeItemRow key={entry.path + "-item"} entry={entry} repo={repo} 166 reference={reference} 167 internalReferesh={internalRefresh} leftDiffRefID={leftDiffRefID} 168 rightDiffRefID={rightDiffRefID} delimiter={delimiter} 169 relativeTo={prefix} 170 onNavigate={onNavigate} 171 getMore={getMore} 172 onRevert={onRevert} 173 />); 174 })} 175 {!!nextPage && 176 <TreeEntryPaginator path={""} loading={loading} nextPage={nextPage} 177 setAfterUpdated={setAfterUpdated}/>} 178 </tbody> 179 </Table> 180 </Card.Body> 181 </Card> 182 </div> 183 } 184 } 185 186 export const defaultGetMoreChanges = (repo, leftRefId, rightRefId, delimiter) => (afterUpdated, path, useDelimiter= true, amount = -1) => { 187 return refs.diff(repo.id, leftRefId, rightRefId, afterUpdated, path, useDelimiter ? delimiter : "", amount > 0 ? amount : undefined); 188 }; 189 190 export const MetadataFields = ({ metadataFields, setMetadataFields, ...rest}) => { 191 const onChangeKey = useCallback((i) => { 192 return e => { 193 const key = e.currentTarget.value; 194 setMetadataFields(prev => [...prev.slice(0,i), {...prev[i], key}, ...prev.slice(i+1)]); 195 e.preventDefault() 196 }; 197 }, [setMetadataFields]); 198 199 const onChangeValue = useCallback((i) => { 200 return e => { 201 const value = e.currentTarget.value; 202 setMetadataFields(prev => [...prev.slice(0,i), {...prev[i], value}, ...prev.slice(i+1)]); 203 }; 204 }, [setMetadataFields]); 205 206 const onRemovePair = useCallback((i) => { 207 return () => setMetadataFields(prev => [...prev.slice(0, i), ...prev.slice(i + 1)]) 208 }, [setMetadataFields]) 209 210 const onAddPair = useCallback(() => { 211 setMetadataFields(prev => [...prev, {key: "", value: ""}]) 212 }, [setMetadataFields]) 213 214 return ( 215 <div className="mt-3 mb-3" {...rest}> 216 {metadataFields.map((f, i) => { 217 return ( 218 <Form.Group key={`commit-metadata-field-${i}`} className="mb-3"> 219 <Row> 220 <Col md={{span: 5}}> 221 <Form.Control type="text" placeholder="Key" defaultValue={f.key} onChange={onChangeKey(i)}/> 222 </Col> 223 <Col md={{span: 5}}> 224 <Form.Control type="text" placeholder="Value" defaultValue={f.value} onChange={onChangeValue(i)}/> 225 </Col> 226 <Col md={{span: 1}}> 227 <Form.Text> 228 <Button size="sm" variant="secondary" onClick={onRemovePair(i)}> 229 <XIcon/> 230 </Button> 231 </Form.Text> 232 </Col> 233 </Row> 234 </Form.Group> 235 ) 236 })} 237 <Button onClick={onAddPair} size="sm" variant="secondary"> 238 <PlusIcon/>{' '} 239 Add Metadata field 240 </Button> 241 </div> 242 ) 243 }