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

     1  import React, {useEffect, useMemo, useRef, useState} from "react";
     2  import { useOutletContext } from "react-router-dom";
     3  import {
     4      GitBranchIcon,
     5      LinkIcon,
     6      PackageIcon,
     7      TrashIcon
     8  } from "@primer/octicons-react";
     9  import ButtonGroup from "react-bootstrap/ButtonGroup";
    10  import Button from "react-bootstrap/Button";
    11  import Card from "react-bootstrap/Card";
    12  import ListGroup from "react-bootstrap/ListGroup";
    13  
    14  import {branches} from "../../../lib/api";
    15  
    16  import {
    17      ActionGroup,
    18      ActionsBar, ClipboardButton,
    19      AlertError, LinkButton,
    20      Loading, PrefixSearchWidget, RefreshButton
    21  } from "../../../lib/components/controls";
    22  import {useRefs} from "../../../lib/hooks/repo";
    23  import {useAPIWithPagination} from "../../../lib/hooks/api";
    24  import {Paginator} from "../../../lib/components/pagination";
    25  import Modal from "react-bootstrap/Modal";
    26  import Form from "react-bootstrap/Form";
    27  import RefDropdown from "../../../lib/components/repository/refDropdown";
    28  import Badge from "react-bootstrap/Badge";
    29  import {ConfirmationButton} from "../../../lib/components/modals";
    30  import Alert from "react-bootstrap/Alert";
    31  import {Link} from "../../../lib/components/nav";
    32  import {useRouter} from "../../../lib/hooks/router";
    33  import {RepoError} from "./error";
    34  
    35  const ImportBranchName = 'import-from-inventory';
    36  
    37  
    38  const BranchWidget = ({ repo, branch, onDelete }) => {
    39  
    40      const buttonVariant = "outline-dark";
    41      const isDefault = repo.default_branch === branch.id;
    42      let deleteMsg = (
    43          <>
    44              Are you sure you wish to delete branch <strong>{branch.id}</strong> ?
    45          </>
    46      );
    47      if (branch.id === ImportBranchName) {
    48          deleteMsg = (
    49              <>
    50                  <p>{deleteMsg}</p>
    51                  <Alert variant="warning"><strong>Warning</strong> this is a system branch used for importing data to lakeFS</Alert>
    52              </>
    53          );
    54      }
    55  
    56      return (
    57          <ListGroup.Item>
    58              <div className="clearfix">
    59                  <div className="float-start">
    60                      <h6>
    61                          <Link href={{
    62                              pathname: '/repositories/:repoId/objects',
    63                              params: {repoId: repo.id},
    64                              query: {ref: branch.id}
    65                          }}>
    66                              {branch.id}
    67                          </Link>
    68  
    69                          {isDefault &&
    70                          <>
    71                              {' '}
    72                              <Badge variant="info">Default</Badge>
    73                          </>}
    74                      </h6>
    75                  </div>
    76  
    77  
    78                  <div className="float-end">
    79                      {!isDefault &&
    80                      <ButtonGroup className="commit-actions">
    81                          <ConfirmationButton
    82                              variant="outline-danger"
    83                              disabled={isDefault}
    84                              msg={deleteMsg}
    85                              tooltip="delete branch"
    86                              onConfirm={() => {
    87                                  branches.delete(repo.id, branch.id)
    88                                      .catch(err => alert(err))
    89                                      .then(() => onDelete(branch.id))
    90                              }}
    91                          >
    92                              <TrashIcon/>
    93                          </ConfirmationButton>
    94                      </ButtonGroup>
    95                      }
    96  
    97                      <ButtonGroup className="branch-actions ms-2">
    98                          <LinkButton
    99                              href={{
   100                                  pathname: '/repositories/:repoId/commits/:commitId',
   101                                  params:{repoId: repo.id, commitId: branch.commit_id},
   102                              }}
   103                              buttonVariant="outline-dark"
   104                              tooltip="View referenced commit">
   105                              {branch.commit_id.substr(0, 12)}
   106                          </LinkButton>
   107                          <ClipboardButton variant={buttonVariant} text={branch.id} tooltip="Copy ID to clipboard"/>
   108                          <ClipboardButton variant={buttonVariant} text={`lakefs://${repo.id}/${branch.id}`} tooltip="Copy URI to clipboard" icon={<LinkIcon/>}/>
   109                          <ClipboardButton variant={buttonVariant} text={`s3://${repo.id}/${branch.id}`} tooltip="Copy S3 URI to clipboard" icon={<PackageIcon/>}/>
   110                      </ButtonGroup>
   111                  </div>
   112              </div>
   113          </ListGroup.Item>
   114      );
   115  };
   116  
   117  
   118  const CreateBranchButton = ({ repo, variant = "success", onCreate = null, readOnly = false, children }) => {
   119      const [show, setShow] = useState(false);
   120      const [disabled, setDisabled] = useState(false);
   121      const [error, setError] = useState(null);
   122      const textRef = useRef(null);
   123      const defaultBranch = useMemo(
   124          () => ({ id: repo.default_branch, type: "branch"}),
   125          [repo.default_branch]);
   126      const [selectedBranch, setSelectedBranch] = useState(defaultBranch);
   127  
   128  
   129      const hide = () => {
   130          if (disabled) return;
   131          setShow(false);
   132      };
   133  
   134      const display = () => {
   135          setShow(true);
   136      };
   137  
   138      const onSubmit = async () => {
   139          setDisabled(true);
   140          const branchId = textRef.current.value;
   141          const sourceRef = selectedBranch.id;
   142  
   143          try {
   144              await branches.create(repo.id, branchId, sourceRef);
   145              setError(false);
   146              setDisabled(false);
   147              setShow(false);
   148              onCreate();
   149          } catch (err) {
   150              setError(err);
   151              setDisabled(false);
   152          }
   153      };
   154  
   155      return (
   156          <>
   157              <Modal show={show} onHide={hide} enforceFocus={false}>
   158                  <Modal.Header closeButton>
   159                      Create Branch
   160                  </Modal.Header>
   161                  <Modal.Body>
   162  
   163                      <Form onSubmit={(e) => {
   164                          onSubmit();
   165                          e.preventDefault();
   166                      }}>
   167                          <Form.Group controlId="name" className="mb-3">
   168                              <Form.Control type="text" placeholder="Branch Name" name="text" ref={textRef}/>
   169                          </Form.Group>
   170                          <Form.Group controlId="source" className="mb-3">
   171                              <RefDropdown
   172                                  repo={repo}
   173                                  emptyText={'Select Source Branch'}
   174                                  prefix={'From '}
   175                                  selected={selectedBranch}
   176                                  selectRef={(refId) => {
   177                                      setSelectedBranch(refId);
   178                                  }}
   179                                  withCommits={true}
   180                                  withWorkspace={false}/>
   181                          </Form.Group>
   182                      </Form>
   183  
   184                      {!!error && <AlertError error={error}/>}
   185                  </Modal.Body>
   186                  <Modal.Footer>
   187                      <Button variant="secondary" disabled={disabled} onClick={hide}>
   188                          Cancel
   189                      </Button>
   190                      <Button variant="success" onClick={onSubmit} disabled={disabled}>
   191                          Create
   192                      </Button>
   193                  </Modal.Footer>
   194              </Modal>
   195              <Button variant={variant} disabled={readOnly} onClick={display}>{children}</Button>
   196          </>
   197      );
   198  };
   199  
   200  
   201  const BranchList = ({ repo, prefix, after, onPaginate }) => {
   202      const router = useRouter()
   203      const [refresh, setRefresh] = useState(true);
   204      const { results, error, loading, nextPage } = useAPIWithPagination(async () => {
   205          return branches.list(repo.id, prefix, after);
   206      }, [repo.id, refresh, prefix, after]);
   207  
   208      const doRefresh = () =>  setRefresh(!refresh);
   209  
   210      let content;
   211  
   212      if (loading) content = <Loading/>;
   213      else if (error) content = <AlertError error={error}/>;
   214      else content = (
   215          <>
   216              <Card>
   217                  <ListGroup variant="flush">
   218                      {results.map(branch => (
   219                          <BranchWidget key={branch.id} repo={repo} branch={branch} onDelete={doRefresh}/>
   220                      ))}
   221                  </ListGroup>
   222              </Card>
   223              <Paginator onPaginate={onPaginate} nextPage={nextPage} after={after}/>
   224          </>
   225      );
   226  
   227      return (
   228          <div className="mb-5">
   229              <ActionsBar>
   230                  <ActionGroup orientation="right">
   231  
   232                      <PrefixSearchWidget
   233                          defaultValue={router.query.prefix}
   234                          text="Find branch"
   235                          onFilter={prefix => {
   236                              const query = {prefix};
   237                              router.push({pathname: '/repositories/:repoId/branches', params: {repoId: repo.id}, query});
   238                          }}/>
   239  
   240                      <RefreshButton onClick={doRefresh}/>
   241  
   242                      <CreateBranchButton repo={repo} readOnly={repo?.read_only} variant="success" onCreate={doRefresh}>
   243                          <GitBranchIcon/> Create Branch
   244                      </CreateBranchButton>
   245  
   246                  </ActionGroup>
   247              </ActionsBar>
   248              {content}
   249              <div className={"mt-2"}>
   250                  lakeFS uses a Git-like branching model. <a href="https://docs.lakefs.io/understand/branching-model.html" target="_blank" rel="noopener noreferrer">Learn more.</a>
   251              </div>
   252          </div>
   253      );
   254  };
   255  
   256  const BranchesContainer = () => {
   257      const router = useRouter()
   258      const { repo, loading, error } = useRefs();
   259      const { after } = router.query;
   260      const routerPfx = (router.query.prefix) ? router.query.prefix : "";
   261  
   262      if (loading) return <Loading/>;
   263      if (error) return <RepoError error={error}/>;
   264  
   265      return (
   266          <BranchList
   267              repo={repo}
   268              after={(after) ? after : ""}
   269              prefix={routerPfx}
   270              onPaginate={after => {
   271                  const query = {after};
   272                  if (router.query.prefix) query.prefix = router.query.prefix;
   273                  router.push({pathname: '/repositories/:repoId/branches', params: {repoId: repo.id}, query});
   274              }}/>
   275      );
   276  };
   277  
   278  
   279  const RepositoryBranchesPage = () => {
   280    const [setActivePage] = useOutletContext();
   281    useEffect(() => setActivePage("branches"), [setActivePage]);
   282    return <BranchesContainer />;
   283  }
   284  
   285  export default RepositoryBranchesPage;