github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/pages/repositories/repository/compare.jsx (about)

     1  import React, {useCallback, useEffect, useState, useRef} from "react";
     2  import { useOutletContext } from "react-router-dom";
     3  import {ActionGroup, ActionsBar, AlertError, Loading, RefreshButton} from "../../../lib/components/controls";
     4  import {useRefs} from "../../../lib/hooks/repo";
     5  import RefDropdown from "../../../lib/components/repository/refDropdown";
     6  import {ArrowLeftIcon, ArrowSwitchIcon, GitMergeIcon} from "@primer/octicons-react";
     7  import {useAPIWithPagination} from "../../../lib/hooks/api";
     8  import {refs} from "../../../lib/api";
     9  import Alert from "react-bootstrap/Alert";
    10  import {ChangesTreeContainer, defaultGetMoreChanges, MetadataFields} from "../../../lib/components/repository/changes";
    11  import {useRouter} from "../../../lib/hooks/router";
    12  import {URINavigator} from "../../../lib/components/repository/tree";
    13  import {appendMoreResults} from "./changes";
    14  import {RefTypeBranch, RefTypeCommit} from "../../../constants";
    15  import Button from "react-bootstrap/Button";
    16  import {FormControl, FormHelperText, InputLabel, MenuItem, Select} from "@mui/material";
    17  import Modal from "react-bootstrap/Modal";
    18  import {RepoError} from "./error";
    19  import OverlayTrigger from "react-bootstrap/OverlayTrigger";
    20  import Tooltip from "react-bootstrap/Tooltip";
    21  import Form from "react-bootstrap/Form";
    22  
    23  const CompareList = ({ repo, reference, compareReference, prefix, onSelectRef, onSelectCompare, onNavigate }) => {
    24      const [internalRefresh, setInternalRefresh] = useState(true);
    25      const [afterUpdated, setAfterUpdated] = useState(""); // state of pagination of the item's children
    26      const [resultsState, setResultsState] = useState({prefix: prefix, results:[], pagination:{}}); // current retrieved children of the item
    27  
    28      const router = useRouter();
    29      const handleSwitchRefs = useCallback(
    30          (e) => {
    31              e.preventDefault();
    32              router.push({pathname: `/repositories/:repoId/compare`, params: {repoId: repo.id},
    33                  query: {ref: compareReference.id, compare: reference.id}});
    34          },[]
    35      );
    36  
    37      const refresh = () => {
    38          setResultsState({prefix: prefix, results:[], pagination:{}})
    39          setInternalRefresh(!internalRefresh)
    40      }
    41  
    42      const delimiter = "/"
    43  
    44      const { error, loading, nextPage } = useAPIWithPagination(async () => {
    45          if (!repo) return
    46          if (compareReference.id === reference.id)
    47              return {pagination: {has_more: false}, results: []}; // nothing to compare here.
    48  
    49          return await appendMoreResults(resultsState, prefix, afterUpdated, setAfterUpdated, setResultsState,
    50              () => refs.diff(repo.id, reference.id, compareReference.id, afterUpdated, prefix, delimiter));
    51      }, [repo.id, reference.id, internalRefresh, afterUpdated, delimiter, prefix])
    52  
    53      let results = resultsState.results
    54      let content;
    55  
    56      const relativeTitle = (from, to) => {
    57          let fromId = from.id;
    58          let toId = to.id;
    59          if (from.type === RefTypeCommit) {
    60              fromId = fromId.substr(0, 12);
    61          }
    62          if (to.type === RefTypeCommit) {
    63              toId = toId.substr(0, 12);
    64          }
    65  
    66          return `${fromId}...${toId}`
    67      }
    68      const uriNavigator = <URINavigator
    69              path={prefix}
    70              reference={reference}
    71              relativeTo={relativeTitle(reference, compareReference)}
    72              repo={repo}
    73              pathURLBuilder={(params, query) => {
    74                  const q = {
    75                      delimiter: "/",
    76                      prefix: query.path,
    77                  };
    78                  if (compareReference)
    79                      q.compare = compareReference.id;
    80                  if (reference)
    81                      q.ref = reference.id;
    82                  return {
    83                      pathname: '/repositories/:repoId/compare',
    84                      params: {repoId: repo.id},
    85                      query: q
    86                  };
    87              }}/>
    88  
    89      const changesTreeMessage = <p>Showing changes between <strong>{reference.id}</strong> and <strong>{compareReference.id}</strong></p>
    90      let leftCommittedRef = reference.id;
    91      let rightCommittedRef = compareReference.id;
    92      if (reference.type === RefTypeBranch) {
    93          leftCommittedRef += "@";
    94      }
    95      if (compareReference.type === RefTypeBranch) {
    96          rightCommittedRef += "@";
    97      }
    98  
    99      if (loading) {
   100          content = <Loading/>
   101      }
   102      else if (error) content = <AlertError error={error}/>
   103      else if (compareReference.id === reference.id) {
   104          content = (
   105              <Alert variant="warning">
   106                  <Alert.Heading>There isn’t anything to compare.</Alert.Heading>
   107                  You’ll need to use two different sources to get a valid comparison.
   108              </Alert>
   109          )
   110      }
   111      else {
   112          content = <ChangesTreeContainer results={results} delimiter={delimiter}
   113                                          uriNavigator={uriNavigator} leftDiffRefID={leftCommittedRef} rightDiffRefID={rightCommittedRef}
   114                                          repo={repo} reference={reference} internalReferesh={internalRefresh} prefix={prefix}
   115                                          getMore={defaultGetMoreChanges(repo, reference.id, compareReference.id, delimiter)}
   116                                          loading={loading} nextPage={nextPage} setAfterUpdated={setAfterUpdated} onNavigate={onNavigate}
   117                                          changesTreeMessage={changesTreeMessage}/>
   118      }
   119  
   120      const emptyDiff = (!loading && !error && !!results && results.length === 0);
   121  
   122      return (
   123          <>
   124              <ActionsBar>
   125                  <ActionGroup orientation="left">
   126                      <RefDropdown
   127                          prefix={'Base '}
   128                          repo={repo}
   129                          selected={(reference) ? reference : null}
   130                          withCommits={true}
   131                          withWorkspace={false}
   132                          selectRef={onSelectRef}/>
   133  
   134                      <ArrowLeftIcon className="me-2 mt-2" size="small" verticalAlign="middle"/>
   135  
   136                      <RefDropdown
   137                          prefix={'Compared to '}
   138                          emptyText={'Compare with...'}
   139                          repo={repo}
   140                          selected={(compareReference) ? compareReference : null}
   141                          withCommits={true}
   142                          withWorkspace={false}
   143                          selectRef={onSelectCompare}/>
   144  
   145                      <OverlayTrigger placement="bottom" overlay={
   146                          <Tooltip>Switch directions</Tooltip>
   147                      }>
   148                      <span>
   149                          <Button variant={"link"}
   150                                onClick={handleSwitchRefs}>
   151                              <ArrowSwitchIcon className="me-2 mt-2" size="small" verticalAlign="middle"/>
   152                          </Button>
   153                      </span>
   154                      </OverlayTrigger>&#160;&#160;
   155                  </ActionGroup>
   156  
   157                  <ActionGroup orientation="right">
   158  
   159                      <RefreshButton onClick={refresh}/>
   160  
   161                      {(compareReference.type === RefTypeBranch && reference.type === RefTypeBranch) &&
   162                          <MergeButton
   163                              repo={repo}
   164                              disabled={((compareReference.id === reference.id) || emptyDiff || repo?.read_only)}
   165                              source={compareReference.id}
   166                              dest={reference.id}
   167                              onDone={refresh}
   168                          />
   169                      }
   170                  </ActionGroup>
   171              </ActionsBar>
   172              {content}
   173          </>
   174      );
   175  };
   176  
   177  const MergeButton = ({repo, onDone, source, dest, disabled = false}) => {
   178      const textRef = useRef(null);
   179      const [metadataFields, setMetadataFields] = useState([])
   180      const initialMerge = {
   181          merging: false,
   182          show: false,
   183          err: null,
   184          strategy: "none",
   185      }
   186      const [mergeState, setMergeState] = useState(initialMerge);
   187  
   188      const onClickMerge = useCallback(() => {
   189          setMergeState({merging: mergeState.merging, err: mergeState.err, show: true, strategy: mergeState.strategy})}
   190      );
   191  
   192      const onStrategyChange = (event) => {
   193          setMergeState({merging: mergeState.merging, err: mergeState.err, show: mergeState.show, strategy: event.target.value});
   194      }
   195      const hide = () => {
   196          if (mergeState.merging) return;
   197          setMergeState(initialMerge);
   198          setMetadataFields([])
   199      }
   200  
   201      const onSubmit = async () => {
   202          const message = textRef.current.value;
   203          const metadata = {};
   204          metadataFields.forEach(pair => metadata[pair.key] = pair.value)
   205  
   206          let strategy = mergeState.strategy;
   207          if (strategy === "none") {
   208              strategy = "";
   209          }
   210          setMergeState({merging: true, show: mergeState.show, err: mergeState.err, strategy: mergeState.strategy})
   211          try {
   212              await refs.merge(repo.id, source, dest, strategy, message, metadata);
   213              setMergeState({merging: mergeState.merging, show: mergeState.show, err: null, strategy: mergeState.strategy})
   214              onDone();
   215              hide();
   216          } catch (err) {
   217              setMergeState({merging: mergeState.merging, show: mergeState.show, err: err, strategy: mergeState.strategy})
   218          }
   219      }
   220  
   221      return (
   222          <>
   223              <Modal show={mergeState.show} onHide={hide} size="lg">
   224                  <Modal.Header closeButton>
   225                      <Modal.Title>Merge branch {source} into {dest}</Modal.Title>
   226                  </Modal.Header>
   227                  <Modal.Body>
   228                      <Form className="mb-2">
   229                          <Form.Group controlId="message" className="mb-3">
   230                              <Form.Control type="text" placeholder="Commit Message (Optional)" ref={textRef}/>
   231                          </Form.Group>
   232  
   233                          <MetadataFields metadataFields={metadataFields} setMetadataFields={setMetadataFields}/>
   234                      </Form>
   235                      <FormControl sx={{ m: 1, minWidth: 120 }}>
   236                          <InputLabel id="demo-select-small">Strategy</InputLabel>
   237                          <Select
   238                              labelId="demo-select-small"
   239                              id="demo-simple-select-helper"
   240                              value={mergeState.strategy}
   241                              label="Strategy"
   242                              onChange={onStrategyChange}
   243                          >
   244                              <MenuItem value={"none"}>Default</MenuItem>
   245                              <MenuItem value={"source-wins"}>source-wins</MenuItem>
   246                              <MenuItem value={"dest-wins"}>dest-wins</MenuItem>
   247                          </Select>
   248                      </FormControl>
   249                      <FormHelperText>In case of a merge conflict, this option will force the merge process
   250                          to automatically favor changes from <b>{dest}</b> (&rdquo;dest-wins&rdquo;) or
   251                          from <b>{source}</b> (&rdquo;source-wins&rdquo;). In case no selection is made,
   252                          the merge process will fail in case of a conflict.</FormHelperText>
   253                      {(mergeState.err) ? (<AlertError error={mergeState.err}/>) : (<></>)}
   254                  </Modal.Body>
   255                  <Modal.Footer>
   256                      <Button variant="secondary" disabled={mergeState.merging} onClick={hide}>
   257                          Cancel
   258                      </Button>
   259                      <Button variant="success" disabled={mergeState.merging} onClick={onSubmit}>
   260                          {(mergeState.merging) ? 'Merging...' : 'Merge'}
   261                      </Button>
   262                  </Modal.Footer>
   263              </Modal>
   264              <Button variant="success" disabled={disabled} onClick={() => onClickMerge()}>
   265                  <GitMergeIcon/> {"Merge"}
   266              </Button>
   267          </>
   268      );
   269  }
   270  
   271  const CompareContainer = () => {
   272      const router = useRouter();
   273      const { loading, error, repo, reference, compare } = useRefs();
   274  
   275      const { prefix } = router.query;
   276  
   277      if (loading) return <Loading/>;
   278      if (error) return <RepoError error={error}/>;
   279  
   280      const route = query => router.push({pathname: `/repositories/:repoId/compare`, params: {repoId: repo.id}, query: {
   281          ...query,
   282      }});
   283  
   284      return (
   285          <CompareList
   286              repo={repo}
   287              prefix={(prefix) ? prefix : ""}
   288              reference={reference}
   289              onSelectRef={reference => route(compare ? {ref: reference.id, compare: compare.id} : {ref: reference.id})}
   290              compareReference={compare}
   291              onSelectCompare={compare => route(reference ? {ref: reference.id, compare: compare.id} : {compare: compare.id})}
   292              onNavigate={entry => {
   293                  return {
   294                      pathname: `/repositories/:repoId/compare`,
   295                      params: {repoId: repo.id},
   296                      query: {
   297                          ref: reference.id,
   298                          compare: compare.id,
   299                          prefix: entry.path,
   300                      }
   301                  }
   302              }}
   303          />
   304      );
   305  };
   306  
   307  const RepositoryComparePage = () => {
   308    const [setActivePage] = useOutletContext();
   309    useEffect(() => setActivePage("compare"), [setActivePage]);
   310    return <CompareContainer />;
   311  };
   312  
   313  export default RepositoryComparePage;