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  }