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&apos;t show summary for a change with more than {MAX_NUM_OBJECTS} objects
    85                                     </span>
    86                                  </Tooltip>
    87                              }>
    88                  <small>
    89                      &gt;= {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  }