github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/lib/components/repository/refDropdown.jsx (about)

     1  import React, {useEffect, useRef, useState} from "react";
     2  
     3  import Form from "react-bootstrap/Form";
     4  import Alert from "react-bootstrap/Alert";
     5  import Button from "react-bootstrap/Button";
     6  import Badge from "react-bootstrap/Badge";
     7  import Overlay from "react-bootstrap/Overlay";
     8  import {ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, XIcon} from "@primer/octicons-react";
     9  import Popover from "react-bootstrap/Popover";
    10  
    11  import {tags, branches, commits} from '../../api';
    12  import {Nav} from "react-bootstrap";
    13  import {RefTypeBranch, RefTypeCommit, RefTypeTag} from "../../../constants";
    14  
    15  
    16  const RefSelector = ({ repo, selected, selectRef, withCommits, withWorkspace, withTags, amount = 300 }) => {
    17      // used for ref pagination
    18      const [pagination, setPagination] = useState({after: "", prefix: "", amount});
    19      const [refList, setRefs] = useState({loading: true, payload: null, error: null});
    20      const [refType, setRefType] = useState(selected && selected.type || RefTypeBranch)
    21      useEffect(() => {
    22          setRefs({loading: true, payload: null, error: null});
    23          const fetchRefs = async () => {
    24              try {
    25                  let response;
    26                  if (refType === RefTypeTag) {
    27                      response = await tags.list(repo.id, pagination.prefix, pagination.after, pagination.amount);
    28                  } else {
    29                      response = await branches.list(repo.id, pagination.prefix, pagination.after, pagination.amount);
    30                  }
    31                  setRefs({loading: false, payload: response, error: null});
    32              } catch (error) {
    33                  setRefs({loading: false, payload: null, error: error});
    34              }
    35          };
    36          fetchRefs();
    37      }, [refType, repo.id, pagination])
    38  
    39      // used for commit listing
    40      const initialCommitList = {branch: selected, commits: null, loading: false};
    41      const [commitList, setCommitList] = useState(initialCommitList);
    42  
    43  
    44      const form = (
    45          <div className="ref-filter-form">
    46              <Form onSubmit={e => { e.preventDefault(); }}>
    47                  <Form.Control type="text" placeholder={refType === RefTypeTag ? "Filter tags" : "Filter branches"} onChange={(e)=> {
    48                      setPagination({
    49                          amount,
    50                          after: "",
    51                          prefix: e.target.value
    52                      })
    53                  }}/>
    54              </Form>
    55          </div>
    56      );
    57      const refTypeNav = withTags && <Nav variant="tabs" onSelect={setRefType} activeKey={refType} className="mt-2">
    58          <Nav.Item>
    59              <Nav.Link eventKey={"branch"}>Branches</Nav.Link>
    60          </Nav.Item>
    61          <Nav.Item>
    62              <Nav.Link eventKey={"tag"}>Tags</Nav.Link>
    63          </Nav.Item>
    64      </Nav>
    65  
    66      if (refList.loading) {
    67          return  (
    68              <div className="ref-selector">
    69                  {form}
    70                  {refTypeNav}
    71                  <p>Loading...</p>
    72              </div>
    73          );
    74      }
    75  
    76      if (refList.error) {
    77          return  (
    78              <div className="ref-selector">
    79                  {form}
    80                  {refTypeNav}
    81                  <Alert variant="danger">{refList.error}</Alert>
    82              </div>
    83          );
    84      }
    85  
    86      if (commitList.commits !== null) {
    87          return (
    88              <CommitList
    89                  withWorkspace={withWorkspace}
    90                  commits={commitList.commits}
    91                  branch={commitList.branch}
    92                  selectRef={selectRef}
    93                  reset={() => {
    94                      setCommitList(initialCommitList);
    95                  }}/>
    96          );
    97      }
    98  
    99  
   100      const results = refList.payload.results;
   101  
   102      return (
   103          <div className="ref-selector">
   104              {form}
   105              {refTypeNav}
   106              <div className="ref-scroller">
   107                  {(results && results.length > 0) ? (
   108                      <>
   109                          <ul className="list-group ref-list">
   110                              {results.map(namedRef => (
   111                                  <RefEntry key={namedRef.id} repo={repo} refType={refType} namedRef={namedRef.id} selectRef={selectRef} selected={selected} withCommits={refType !== RefTypeTag && withCommits} logCommits={async () => {
   112                                      const data = await commits.log(repo.id, namedRef.id)
   113                                      setCommitList({...commitList, branch: namedRef.id, commits: data.results});
   114                                  }}/>
   115                              ))}
   116                          </ul>
   117                          <Paginator results={refList.payload.results} pagination={refList.payload.pagination} from={pagination.after} onPaginate={(after) => {
   118                              setPagination({after})
   119                          }}/>
   120                      </>
   121                  ) : (
   122                      <p className="text-center mt-3"><small>No references found</small></p>
   123                  )}
   124  
   125              </div>
   126          </div>
   127      );
   128  };
   129  
   130  const CommitList = ({ commits, selectRef, reset, branch, withWorkspace }) => {
   131      const getMessage = commit => {
   132          if (!commit.message) {
   133              return 'repository epoch';
   134          }
   135  
   136          if (commit.message.length > 60) {
   137              return commit.message.substr(0, 40) + '...';
   138          }
   139  
   140          return commit.message;
   141      };
   142  
   143      return (
   144          <div className="ref-selector">
   145              <h5>{branch}</h5>
   146              <div className="ref-scroller">
   147                  <ul className="list-group ref-list">
   148                      {(withWorkspace) ? (
   149                          <li className="list-group-item" key={branch}>
   150                              <Button variant="link" onClick={() => {
   151                                  selectRef({id: branch, type: RefTypeBranch});
   152                              }}><em>{branch}{'\''}s Workspace (uncommitted changes)</em></Button>
   153                          </li>
   154                      ) : (<span/>)}
   155                      {commits.map(commit => (
   156                          <li className="list-group-item" key={commit.id}>
   157                              <Button variant="link" onClick={() => {
   158                                  selectRef({id: commit.id, type: RefTypeCommit});
   159                              }}>{getMessage(commit)} </Button>
   160                              <div className="actions">
   161                                  <Badge variant="light">{commit.id.substr(0, 12)}</Badge>
   162                              </div>
   163                          </li>
   164                      ))}
   165                  </ul>
   166                  <p className="ref-paginator">
   167                      <Button variant="link" size="sm" onClick={reset}>Back</Button>
   168                  </p>
   169              </div>
   170          </div>
   171      );
   172  };
   173  
   174  const RefEntry = ({repo, namedRef, refType, selectRef, selected, logCommits, withCommits}) => {
   175      return (
   176          <li className="list-group-item" key={namedRef}>
   177              {(!!selected && namedRef === selected.id) ?
   178                  <strong>{namedRef}</strong> :
   179                  <Button variant="link" onClick={() => {
   180                      selectRef({id: namedRef, type: refType});
   181                  }}>{namedRef}</Button>
   182              }
   183              <div className="actions">
   184                  {(refType === RefTypeBranch && namedRef === repo.default_branch) ? (<Badge variant="info">Default</Badge>) : <span/>}
   185                  {(withCommits) ? (
   186                      <Button onClick={logCommits} size="sm" variant="link">
   187                          <ChevronRightIcon/>
   188                      </Button>
   189                  ) : (<span/>)}
   190              </div>
   191          </li>
   192      );
   193  };
   194  
   195  const Paginator = ({ pagination, onPaginate, results, from }) => {
   196      const next = (results.length) ? results[results.length-1].id : "";
   197  
   198      if (!pagination.has_more && from === "") return (<span/>);
   199  
   200      return (
   201          <p className="ref-paginator">
   202              {(from !== "") ?
   203                  (<Button  size={"sm"} variant="link" onClick={() => { onPaginate(""); }}>Reset</Button>) :
   204                  (<span/>)
   205              }
   206              {' '}
   207              {(pagination.has_more) ?
   208                  (<Button size={"sm"} variant="link" onClick={() => { onPaginate(next); }}>Next...</Button>) :
   209                  (<span/>)
   210              }
   211          </p>
   212      );
   213  };
   214  
   215  const RefDropdown = ({ repo, selected, selectRef, onCancel, variant="light", prefix = '', emptyText = '', withCommits = true, withWorkspace = true, withTags = true }) => {
   216  
   217      const [show, setShow] = useState(false);
   218      const target = useRef(null);
   219  
   220  
   221      const popover = (
   222          <Overlay target={target.current} show={show} placement="bottom" rootClose={true} onHide={() => setShow(false)}>
   223              <Popover className="ref-popover">
   224                  <Popover.Body>
   225                      <RefSelector
   226                          repo={repo}
   227                          withCommits={withCommits}
   228                          withWorkspace={withWorkspace}
   229                          withTags={withTags}
   230                          selected={selected}
   231                          selectRef={(ref) => {
   232                              selectRef(ref);
   233                              setShow(false);
   234                          }}/>
   235                  </Popover.Body>
   236              </Popover>
   237          </Overlay>
   238      );
   239  
   240      const cancelButton = (!!onCancel && !!selected) ? (<Button onClick={() => {
   241          setShow(false);
   242          onCancel();
   243      }} variant={variant}><XIcon/></Button>) : (<span/>);
   244  
   245      if (!selected) {
   246          return (
   247              <>
   248                  <Button ref={target} variant={variant} onClick={()=> { setShow(!show) }}>
   249                      {emptyText} {show ? <ChevronUpIcon/> : <ChevronDownIcon/>}
   250                  </Button>
   251                  {cancelButton}
   252                  {popover}
   253              </>
   254          );
   255      }
   256  
   257      const showId = (ref) => {
   258          if (!ref)
   259              return ''
   260          if (ref.type === RefTypeCommit)
   261              return ref.id.substr(0, 12)
   262          return ref.id
   263      }
   264  
   265      const title = prefix + (!!selected) ? `${prefix} ${selected.type}: ` : '';
   266      return (
   267          <>
   268              <Button ref={target} variant={variant} onClick={()=> { setShow(!show) }}>
   269                  {title} <strong>{showId(selected)}</strong> {show ? <ChevronUpIcon/> : <ChevronDownIcon/>}
   270              </Button>
   271              {cancelButton}
   272              {popover}
   273          </>
   274      );
   275  };
   276  
   277  export default RefDropdown;