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

     1  import React, {useEffect, useRef, useState} from "react";
     2  import { useOutletContext } from "react-router-dom";
     3  import {GitCommitIcon, HistoryIcon,} from "@primer/octicons-react";
     4  
     5  import Modal from "react-bootstrap/Modal";
     6  import Form from "react-bootstrap/Form";
     7  
     8  import Alert from "react-bootstrap/Alert";
     9  import Button from "react-bootstrap/Button";
    10  
    11  import {branches, commits, refs} from "../../../lib/api";
    12  import {useAPIWithPagination} from "../../../lib/hooks/api";
    13  import {useRefs} from "../../../lib/hooks/repo";
    14  import {ConfirmationModal} from "../../../lib/components/modals";
    15  import {ActionGroup, ActionsBar, AlertError, Loading, RefreshButton} from "../../../lib/components/controls";
    16  import RefDropdown from "../../../lib/components/repository/refDropdown";
    17  import {formatAlertText} from "../../../lib/components/repository/errors";
    18  import {ChangesTreeContainer, MetadataFields} from "../../../lib/components/repository/changes";
    19  import {useRouter} from "../../../lib/hooks/router";
    20  import {URINavigator} from "../../../lib/components/repository/tree";
    21  import {RepoError} from "./error";
    22  
    23  
    24  const CommitButton = ({repo, onCommit, enabled = false}) => {
    25  
    26      const textRef = useRef(null);
    27  
    28      const [committing, setCommitting] = useState(false)
    29      const [show, setShow] = useState(false)
    30      const [metadataFields, setMetadataFields] = useState([])
    31      const hide = () => {
    32          if (committing) return;
    33          setShow(false)
    34      }
    35  
    36      const onSubmit = () => {
    37          const message = textRef.current.value;
    38          const metadata = {};
    39          metadataFields.forEach(pair => metadata[pair.key] = pair.value)
    40          setCommitting(true)
    41          onCommit({message, metadata}, () => {
    42              setCommitting(false)
    43              setShow(false);
    44          })
    45      };
    46  
    47      const alertText = formatAlertText(repo.id, null);
    48      return (
    49          <>
    50              <Modal show={show} onHide={hide} size="lg">
    51                  <Modal.Header closeButton>
    52                      <Modal.Title>Commit Changes</Modal.Title>
    53                  </Modal.Header>
    54                  <Modal.Body>
    55                      <Form className="mb-2" onSubmit={(e) => {
    56                          onSubmit();
    57                          e.preventDefault();
    58                      }}>
    59                          <Form.Group controlId="message" className="mb-3">
    60                              <Form.Control type="text" placeholder="Commit Message" ref={textRef}/>
    61                          </Form.Group>
    62  
    63                          <MetadataFields metadataFields={metadataFields} setMetadataFields={setMetadataFields}/>
    64                      </Form>
    65                      {(alertText) ? (<Alert variant="danger">{alertText}</Alert>) : (<span/>)}
    66                  </Modal.Body>
    67                  <Modal.Footer>
    68                      <Button variant="secondary" disabled={committing} onClick={hide}>
    69                          Cancel
    70                      </Button>
    71                      <Button variant="success" disabled={committing} onClick={onSubmit}>
    72                          Commit Changes
    73                      </Button>
    74                  </Modal.Footer>
    75              </Modal>
    76              <Button variant="success" disabled={!enabled} onClick={() => setShow(true)}>
    77                  <GitCommitIcon/> Commit Changes{' '}
    78              </Button>
    79          </>
    80      );
    81  }
    82  
    83  
    84  const RevertButton = ({onRevert, enabled = false}) => {
    85      const [show, setShow] = useState(false)
    86      const hide = () => setShow(false)
    87  
    88      return (
    89          <>
    90              <ConfirmationModal
    91                  show={show}
    92                  onHide={hide}
    93                  msg="Are you sure you want to revert all uncommitted changes?"
    94                  onConfirm={() => {
    95                      onRevert()
    96                      hide()
    97                  }}/>
    98              <Button variant="light" disabled={!enabled} onClick={() => setShow(true)}>
    99                  <HistoryIcon/> Revert
   100              </Button>
   101          </>
   102      );
   103  }
   104  
   105  export async function appendMoreResults(resultsState, prefix, afterUpdated, setAfterUpdated, setResultsState, getMore) {
   106      let resultsFiltered = resultsState.results
   107      if (resultsState.prefix !== prefix) {
   108          // prefix changed, need to delete previous results
   109          setAfterUpdated("")
   110          resultsFiltered = []
   111      }
   112  
   113      if (resultsFiltered.length > 0 && resultsFiltered.at(-1).path > afterUpdated) {
   114          // results already cached
   115          return {prefix: prefix, results: resultsFiltered, pagination: resultsState.pagination}
   116      }
   117  
   118      const {results, pagination} = await getMore()
   119      setResultsState({prefix: prefix, results: resultsFiltered.concat(results), pagination: pagination})
   120      return {results: resultsState.results, pagination: pagination}
   121  }
   122  
   123  const ChangesBrowser = ({repo, reference, prefix, onSelectRef, }) => {
   124      const [actionError, setActionError] = useState(null);
   125      const [internalRefresh, setInternalRefresh] = useState(true);
   126      const [afterUpdated, setAfterUpdated] = useState(""); // state of pagination of the item's children
   127      const [resultsState, setResultsState] = useState({prefix: prefix, results:[], pagination:{}}); // current retrieved children of the item
   128  
   129      const delimiter = '/'
   130  
   131      const getMoreUncommittedChanges = (afterUpdated, path, useDelimiter= true, amount = -1) => {
   132          return refs.changes(repo.id, reference.id, afterUpdated, path, useDelimiter ? delimiter : "", amount > 0 ? amount : undefined)
   133      }
   134  
   135      const { error, loading, nextPage } = useAPIWithPagination(async () => {
   136          if (!repo) return
   137          return await appendMoreResults(resultsState, prefix, afterUpdated, setAfterUpdated, setResultsState,
   138              () => refs.changes(repo.id, reference.id, afterUpdated, prefix, delimiter));
   139      }, [repo.id, reference.id, internalRefresh, afterUpdated, delimiter, prefix])
   140  
   141      const results = resultsState.results
   142  
   143      const refresh = () => {
   144          setResultsState({prefix: prefix, results:[], pagination:{}})
   145          setInternalRefresh(!internalRefresh)
   146      }
   147  
   148  
   149      if (error) return <AlertError error={error}/>
   150      if (loading) return <Loading/>
   151  
   152      let onReset = async (entry) => {
   153          branches
   154              .reset(repo.id, reference.id, {type: entry.path_type, path: entry.path})
   155              .then(refresh)
   156              .catch(error => {
   157                  setActionError(error)
   158              })
   159      }
   160  
   161      let onNavigate = (entry) => {
   162          return {
   163              pathname: `/repositories/:repoId/changes`,
   164              params: {repoId: repo.id},
   165              query: {
   166                  ref: reference.id,
   167                  prefix: entry.path,
   168              }
   169          }
   170      }
   171  
   172      const uriNavigator =  <URINavigator path={prefix} reference={reference} repo={repo}
   173                                        pathURLBuilder={(params, query) => {
   174                                            return {
   175                                                pathname: '/repositories/:repoId/changes',
   176                                                params: params,
   177                                                query: {ref: reference.id, prefix: query.path ?? ""},
   178                                            }}}/>
   179      const changesTreeMessage = <p>Showing changes for branch <strong>{reference.id}</strong></p>
   180      const committedRef = reference.id + "@"
   181      const uncommittedRef = reference.id
   182  
   183     const actionErrorDisplay = (actionError) ?
   184          <AlertError error={actionError} onDismiss={() => setActionError(null)}/> : <></>
   185  
   186      return (
   187          <>
   188              <ActionsBar>
   189                  <ActionGroup orientation="left">
   190                      <RefDropdown
   191                          emptyText={'Select Branch'}
   192                          repo={repo}
   193                          selected={(reference) ? reference : null}
   194                          withCommits={false}
   195                          withWorkspace={false}
   196                          withTags={false}
   197                          selectRef={onSelectRef}
   198                      />
   199                  </ActionGroup>
   200  
   201                  <ActionGroup orientation="right">
   202  
   203                      <RefreshButton onClick={refresh}/>
   204  
   205                      <RevertButton enabled={results.length > 0 && !repo?.read_only} onRevert={() => {
   206                          branches.reset(repo.id, reference.id, {type: 'reset'})
   207                              .then(refresh)
   208                              .catch(error => setActionError(error))
   209                      }}/>
   210                      <CommitButton repo={repo} enabled={results.length > 0 && !repo?.read_only} onCommit={async (commitDetails, done) => {
   211                          try {
   212                              await commits.commit(repo.id, reference.id, commitDetails.message, commitDetails.metadata);
   213                              setActionError(null);
   214                              refresh();
   215                          } catch (err) {
   216                              setActionError(err);
   217                          }
   218                          done();
   219                      }}/>
   220                  </ActionGroup>
   221              </ActionsBar>
   222  
   223              {actionErrorDisplay}
   224              <ChangesTreeContainer results={results} delimiter={delimiter}
   225                                    uriNavigator={uriNavigator} leftDiffRefID={committedRef} rightDiffRefID={uncommittedRef}
   226                                    repo={repo} reference={reference} internalReferesh={internalRefresh} prefix={prefix}
   227                                    getMore={getMoreUncommittedChanges}
   228                                    loading={loading} nextPage={nextPage} setAfterUpdated={setAfterUpdated}
   229                                    onNavigate={onNavigate} onRevert={onReset} changesTreeMessage={changesTreeMessage}/>
   230          </>
   231      )
   232  }
   233  
   234  const ChangesContainer = () => {
   235      const router = useRouter();
   236      const {repo, reference, loading, error} = useRefs()
   237      const {prefix} = router.query
   238  
   239      if (loading) return <Loading/>
   240      if (error) return <RepoError error={error}/>
   241  
   242      return (
   243          <ChangesBrowser
   244              prefix={(prefix) ? prefix : ""}
   245              repo={repo}
   246              reference={reference}
   247              onSelectRef={ref => router.push({
   248                  pathname: `/repositories/:repoId/changes`,
   249                  params: {repoId: repo.id},
   250                  query: {
   251                      ref: ref.id,
   252                  }
   253              })}
   254          />
   255      )
   256  }
   257  
   258  const RepositoryChangesPage = () => {
   259    const [setActivePage] = useOutletContext();
   260    useEffect(() => setActivePage('changes'), [setActivePage]);
   261    return <ChangesContainer />;
   262  }
   263  
   264  export default RepositoryChangesPage;