github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/lib/components/repository/ChangeSummary.jsx (about) 1 import React, {useCallback, useEffect, useState} from "react"; 2 import { 3 ClockIcon, 4 DiffAddedIcon, 5 DiffIgnoredIcon, 6 DiffModifiedIcon, 7 DiffRemovedIcon, 8 } from "@primer/octicons-react"; 9 import {OverlayTrigger, Tooltip} from "react-bootstrap"; 10 import {humanSize} from "./tree"; 11 12 const MAX_NUM_OBJECTS = 10_000; 13 const PAGE_SIZE = 1_000; 14 15 class SummaryEntry { 16 constructor() { 17 this.count = 0 18 this.sizeBytes = 0 19 } 20 add(count, sizeBytes) { 21 this.count += count 22 this.sizeBytes += sizeBytes 23 } 24 } 25 26 class SummaryData { 27 constructor() { 28 this.added = new SummaryEntry() 29 this.changed = new SummaryEntry() 30 this.removed = new SummaryEntry() 31 this.conflict = new SummaryEntry() 32 } 33 } 34 35 /** 36 * Widget to display a summary of a change: the number of added/changed/deleted/conflicting objects. 37 * Shows an error if the change has more than {@link MAX_NUM_OBJECTS} entries. 38 39 * @param {string} prefix - prefix to display summary for. 40 * @param {(after : string, path : string, useDelimiter :? boolean, amount :? number) => Promise<any> } getMore - function to use to get the change entries. 41 */ 42 export default ({prefix, getMore}) => { 43 const [pullMore, setPullMore] = useState(false); 44 const [resultsState, setResultsState] = useState({results: [], pagination: {}}); 45 const [loading, setLoading] = useState(true); 46 useEffect(() => { 47 const calculateChanges = async () => { 48 // get pages until reaching the max change size 49 if (resultsState.results && resultsState.results.length >= MAX_NUM_OBJECTS && !pullMore) { 50 setLoading(false) 51 return 52 } 53 if (!loading) { 54 return 55 } 56 const {results, pagination} = await getMore(resultsState.pagination.next_offset || "", prefix, false, PAGE_SIZE) 57 if (!pagination.has_more) { 58 setLoading(false) 59 } 60 setResultsState({results: resultsState.results.concat(results), pagination: pagination}) 61 } 62 63 calculateChanges() 64 .catch(e => { 65 alert(e.toString()); 66 setResultsState({results: [], pagination: {}}) 67 setLoading(false) 68 }) 69 }, [resultsState.results, loading, pullMore]) 70 71 const onLoadAll = useCallback((e) => { 72 e.preventDefault() 73 setLoading(true) 74 setPullMore(true) 75 }, [setLoading, setPullMore]) 76 77 if (loading || !resultsState || !resultsState.results) return <ClockIcon/> 78 if (resultsState.results && resultsState.results.length >= MAX_NUM_OBJECTS && !pullMore) { 79 return ( 80 <OverlayTrigger placement="bottom" 81 overlay={ 82 <Tooltip> 83 <span className={"small font-weight-bold"}> 84 Can't show summary for a change with more than {MAX_NUM_OBJECTS} objects 85 </span> 86 </Tooltip> 87 }> 88 <small> 89 >= {MAX_NUM_OBJECTS.toLocaleString()} results, <a href="#" onClick={onLoadAll}>load more?</a> 90 </small> 91 </OverlayTrigger> 92 ) 93 } 94 const summaryData = resultsState.results.reduce((prev, current) => { 95 prev[current.type].add(1, current.size_bytes) 96 return prev 97 }, new SummaryData()) 98 const detailsTooltip = <Tooltip> 99 <div className="m-1 small text-start"> 100 {summaryData.added.count > 0 && 101 <><span className={"color-fg-added"}>{summaryData.added.count.toLocaleString()}</span> objects added (total {humanSize(summaryData.added.sizeBytes)})<br/></>} 102 {summaryData.removed.count > 0 && 103 <><span className={"color-fg-removed"}>{summaryData.removed.count.toLocaleString()}</span> objects removed (total {humanSize(summaryData.removed.sizeBytes)})<br/></>} 104 {summaryData.changed.count > 0 && 105 <><span className={"color-fg-changed"}>{summaryData.changed.count.toLocaleString()}</span> objects changed<br/></>} 106 {summaryData.conflict.count > 0 && 107 <><span className={"color-fg-conflict"}>{summaryData.conflict.count.toLocaleString()}</span> conflicts<br/></>} 108 </div> 109 </Tooltip> 110 return ( 111 <OverlayTrigger placement="left" overlay={detailsTooltip}> 112 <div className={"m-1 small float-end"}> 113 {summaryData.added.count > 0 && 114 <span className={"color-fg-added"}><DiffAddedIcon className={"change-summary-icon"}/>{summaryData.added.count.toLocaleString()}</span>} 115 {summaryData.removed.count > 0 && 116 <span className={"color-fg-removed"}><DiffRemovedIcon className={"change-summary-icon"}/>{summaryData.removed.count.toLocaleString()}</span>} 117 {summaryData.changed.count > 0 && 118 <span className={"font-weight-bold"}><DiffModifiedIcon className={"change-summary-icon"}/>{summaryData.changed.count.toLocaleString()}</span>} 119 {summaryData.conflict.count > 0 && 120 <span className={"color-fg-conflict"}><DiffIgnoredIcon className={"change-summary-icon"}/>{summaryData.conflict.count.toLocaleString()}</span>} 121 </div> 122 </OverlayTrigger> 123 ) 124 }