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

     1  import React, { useCallback, useEffect, useRef, useState } from "react";
     2  import dayjs from "dayjs";
     3  import { useOutletContext } from "react-router-dom";
     4  import {CheckboxIcon, UploadIcon, XIcon} from "@primer/octicons-react";
     5  import RefDropdown from "../../../lib/components/repository/refDropdown";
     6  import {
     7      ActionGroup,
     8      ActionsBar,
     9      AlertError,
    10      Loading,
    11      PrefixSearchWidget,
    12      RefreshButton,
    13      Warnings
    14  } from "../../../lib/components/controls";
    15  import Button from "react-bootstrap/Button";
    16  import Modal from "react-bootstrap/Modal";
    17  import Form from "react-bootstrap/Form";
    18  import Alert from "react-bootstrap/Alert";
    19  import { BsCloudArrowUp } from "react-icons/bs";
    20  
    21  import {humanSize, Tree} from "../../../lib/components/repository/tree";
    22  import {objects, staging, retention, repositories, imports, NotFoundError, uploadWithProgress} from "../../../lib/api";
    23  import {useAPI, useAPIWithPagination} from "../../../lib/hooks/api";
    24  import {useRefs} from "../../../lib/hooks/repo";
    25  import {useRouter} from "../../../lib/hooks/router";
    26  import {RefTypeBranch} from "../../../constants";
    27  import {
    28      ExecuteImportButton,
    29      ImportDone,
    30      ImportForm,
    31      ImportPhase,
    32      ImportProgress,
    33      startImport
    34  } from "../services/import_data";
    35  import { Box } from "@mui/material";
    36  import { RepoError } from "./error";
    37  import { getContentType, getFileExtension, FileContents } from "./objectViewer";
    38  import {OverlayTrigger, ProgressBar} from "react-bootstrap";
    39  import Tooltip from "react-bootstrap/Tooltip";
    40  import { useSearchParams } from "react-router-dom";
    41  import { useStorageConfig } from "../../../lib/hooks/storageConfig";
    42  import {useDropzone} from "react-dropzone";
    43  import Container from "react-bootstrap/Container";
    44  import Row from "react-bootstrap/Row";
    45  import Col from "react-bootstrap/Col";
    46  import pMap from "p-map";
    47  
    48  const README_FILE_NAME = "README.md";
    49  const REPOSITORY_AGE_BEFORE_GC = 14;
    50  const MAX_PARALLEL_UPLOADS = 5;
    51  
    52  const ImportButton = ({ variant = "success", onClick, config }) => {
    53    const tip = config.import_support
    54      ? "Import data from a remote source"
    55      : config.blockstore_type === "local"
    56      ? "Import is not enabled for local blockstore"
    57      : "Unsupported for " + config.blockstore_type + " blockstore";
    58  
    59    return (
    60      <OverlayTrigger placement="bottom" overlay={<Tooltip>{tip}</Tooltip>}>
    61        <span>
    62          <Button
    63            variant={variant}
    64            disabled={!config.import_support}
    65            onClick={onClick}
    66          >
    67            <BsCloudArrowUp /> Import
    68          </Button>
    69        </span>
    70      </OverlayTrigger>
    71    );
    72  };
    73  
    74  export const useInterval = (callback, delay) => {
    75      const savedCallback = useRef();
    76  
    77      useEffect(() => {
    78          savedCallback.current = callback;
    79      }, [callback]);
    80  
    81      useEffect(() => {
    82          function tick() {
    83              savedCallback.current();
    84          }
    85          if (delay !== null) {
    86              const id = setInterval(tick, delay);
    87              return () => clearInterval(id);
    88          }
    89      }, [delay]);
    90  }
    91  
    92  const ImportModal = ({config, repoId, referenceId, referenceType, path = '', onDone, onHide, show = false}) => {
    93      const [importPhase, setImportPhase] = useState(ImportPhase.NotStarted);
    94      const [numberOfImportedObjects, setNumberOfImportedObjects] = useState(0);
    95      const [isImportEnabled, setIsImportEnabled] = useState(false);
    96      const [importError, setImportError] = useState(null);
    97      const [metadataFields, setMetadataFields] = useState([])
    98      const [importID, setImportID] = useState("")
    99  
   100      const sourceRef = useRef(null);
   101      const destRef = useRef(null);
   102      const commitMsgRef = useRef(null);
   103  
   104      useInterval(() => {
   105          if (importID !== "" && importPhase === ImportPhase.InProgress) {
   106              const getState = async () => {
   107                  try {
   108                      const importState = await imports.get(repoId, referenceId, importID);
   109                      setNumberOfImportedObjects(importState.ingested_objects);
   110                      if (importState.error) {
   111                          throw importState.error;
   112                      }
   113                      if (importState.completed) {
   114                          setImportPhase(ImportPhase.Completed);
   115                          onDone();
   116                      }
   117                  } catch (error) {
   118                      setImportPhase(ImportPhase.Failed);
   119                      setImportError(error);
   120                      setIsImportEnabled(false);
   121                  }
   122              };
   123              getState()
   124          }
   125      }, 3000);
   126      
   127      if (!referenceId || referenceType !== RefTypeBranch) return <></>
   128  
   129      let branchId = referenceId;
   130      
   131      const resetState = () => {
   132          setImportError(null);
   133          setImportPhase(ImportPhase.NotStarted);
   134          setIsImportEnabled(false);
   135          setNumberOfImportedObjects(0);
   136          setMetadataFields([]);
   137          setImportID("");
   138      }
   139  
   140    const hide = () => {
   141      if (
   142        ImportPhase.InProgress === importPhase ||
   143        ImportPhase.Merging === importPhase
   144      )
   145        return;
   146      resetState();
   147      onHide();
   148    };
   149  
   150      const doImport = async () => {
   151          setImportPhase(ImportPhase.InProgress);
   152          try {
   153              const metadata = {};
   154              metadataFields.forEach(pair => metadata[pair.key] = pair.value)
   155              setImportPhase(ImportPhase.InProgress)
   156              await startImport(
   157                  setImportID,
   158                  destRef.current.value,
   159                  commitMsgRef.current.value,
   160                  sourceRef.current.value,
   161                  repoId,
   162                  branchId,
   163                  metadata
   164              );
   165          } catch (error) {
   166              setImportPhase(ImportPhase.Failed);
   167              setImportError(error);
   168              setIsImportEnabled(false);
   169          }
   170      }
   171      const pathStyle = {'minWidth': '25%'};
   172  
   173      return (
   174          <>
   175              <Modal show={show} onHide={hide} size="lg">
   176                  <Modal.Header closeButton>
   177                      <Modal.Title>Import data from {config.blockstore_type}</Modal.Title>
   178                  </Modal.Header>
   179                  <Modal.Body>
   180                      {
   181  
   182                          <ImportForm
   183                              config={config}
   184                              repo={repoId}
   185                              branch={branchId}
   186                              pathStyle={pathStyle}
   187                              sourceRef={sourceRef}
   188                              destRef={destRef}
   189                              updateSrcValidity={(isValid) => setIsImportEnabled(isValid)}
   190                              path={path}
   191                              commitMsgRef={commitMsgRef}
   192                              metadataFields={metadataFields}
   193                              setMetadataFields={setMetadataFields}
   194                              err={importError}
   195                              className={importPhase === ImportPhase.NotStarted || importPhase === ImportPhase.Failed ? '' : 'd-none'}
   196                          />
   197                      }
   198                      {
   199                          importPhase === ImportPhase.InProgress &&
   200                          <ImportProgress numObjects={numberOfImportedObjects}/>
   201                      }
   202                      {
   203                          importPhase === ImportPhase.Completed &&
   204                          <ImportDone branch={branchId}
   205                                      numObjects={numberOfImportedObjects}/>
   206                      }
   207                  </Modal.Body>
   208                  <Modal.Footer>
   209                      <Button variant="secondary" onClick={ async () => {
   210                          if (importPhase === ImportPhase.InProgress && importID.length > 0) {
   211                              await imports.delete(repoId, branchId, importID);
   212                          }
   213                          hide();
   214                      }} hidden={importPhase === ImportPhase.Completed}>
   215                          Cancel
   216                      </Button>
   217  
   218                      <ExecuteImportButton
   219                          importPhase={importPhase}
   220                          importFunc={doImport}
   221                          doneFunc={hide}
   222                          isEnabled={isImportEnabled}/>
   223          </Modal.Footer>
   224        </Modal>
   225      </>
   226    );
   227  };
   228  
   229  function extractChecksumFromResponse(response) {
   230    if (response.contentMD5) {
   231      // convert base64 to hex
   232      const raw = atob(response.contentMD5)
   233      let result = '';
   234      for (let i = 0; i < raw.length; i++) {
   235        const hex = raw.charCodeAt(i).toString(16);
   236        result += (hex.length === 2 ? hex : '0' + hex);
   237      }
   238      return result;
   239    }
   240  
   241    if (response.etag) {
   242      // drop any quote and space
   243      return response.etag.replace(/[" ]+/g, "");
   244    }
   245    return ""
   246  }
   247  
   248  const uploadFile = async (config, repo, reference, path, file, onProgress) => {
   249    const fpath = destinationPath(path, file);
   250    if (config.pre_sign_support_ui) {
   251        let additionalHeaders;
   252        if (config.blockstore_type === "azure") {
   253            additionalHeaders = { "x-ms-blob-type": "BlockBlob" }
   254        }
   255      const getResp = await staging.get(repo.id, reference.id, fpath, config.pre_sign_support_ui);
   256      const uploadResponse = await uploadWithProgress(getResp.presigned_url, file, 'PUT', onProgress, additionalHeaders)
   257      if (uploadResponse.status >= 400) {
   258        throw new Error(`Error uploading file: HTTP ${status}`)
   259      }
   260      const checksum = extractChecksumFromResponse(uploadResponse)
   261      await staging.link(repo.id, reference.id, fpath, getResp, checksum, file.size, file.type);
   262    } else {
   263      await objects.upload(repo.id, reference.id, fpath, file, onProgress);
   264    }
   265  };
   266  
   267  const destinationPath = (path, file) => {
   268    return `${path ? path : ""}${file.path.replace(/\\/g, '/').replace(/^\//, '')}`;
   269  };
   270  
   271  const UploadCandidate = ({ repo, reference, path, file, state, onRemove = null }) => {
   272    const fpath = destinationPath(path, file)
   273    let uploadIndicator = null;
   274    if (state && state.status === "uploading") {
   275      uploadIndicator = <ProgressBar variant="success" now={state.percent}/>
   276    } else if (state && state.status === "done") {
   277      uploadIndicator = <strong><CheckboxIcon/></strong>
   278    } else if (!state && onRemove !== null) {
   279      uploadIndicator = (
   280        <a  href="#" onClick={ e => {
   281          e.preventDefault()
   282          onRemove()
   283        }}>
   284          <XIcon />
   285        </a>
   286      );
   287    }
   288    return (
   289      <Container>
   290        <Row className={`upload-item upload-item-${state ? state.status : "none"}`}>
   291          <Col>
   292            <span className="path">
   293              lakefs://{repo.id}/{reference.id}/{fpath}
   294            </span>
   295          </Col>
   296          <Col xs md="2">
   297            <span className="size">
   298              {humanSize(file.size)}
   299            </span>
   300          </Col>
   301          <Col xs md="1">
   302            <span className="upload-state">
   303              {uploadIndicator ? uploadIndicator : <></>}
   304            </span>
   305          </Col>
   306        </Row>
   307      </Container>
   308    )
   309  };
   310  
   311  const UploadButton = ({config, repo, reference, path, onDone, onClick, onHide, show = false, disabled = false}) => {
   312    const initialState = {
   313      inProgress: false,
   314      error: null,
   315      done: false,
   316    };
   317    const [currentPath, setCurrentPath] = useState(path);
   318    const [uploadState, setUploadState] = useState(initialState);
   319    const [files, setFiles] = useState([]);
   320    const [fileStates, setFileStates] = useState({});
   321    const [abortController, setAbortController] = useState(null)
   322    const onDrop = useCallback(acceptedFiles => {
   323      setFiles([...acceptedFiles])
   324    }, [files])
   325  
   326    const { getRootProps, getInputProps, isDragAccept } = useDropzone({onDrop})
   327  
   328    if (!reference || reference.type !== RefTypeBranch) return <></>;
   329  
   330    const hide = () => {
   331      if (uploadState.inProgress) {
   332        if (abortController !== null) {
   333            abortController.abort()
   334        } else {
   335          return
   336        }
   337      }
   338      setUploadState(initialState);
   339      setFileStates({});
   340      setFiles([]);
   341      setCurrentPath(path);
   342      setAbortController(null)
   343      onHide();
   344    };
   345  
   346    useEffect(() => {
   347      setCurrentPath(path)
   348    }, [path])
   349  
   350    const upload = async () => {
   351      if (files.length < 1) {
   352        return
   353      }
   354  
   355      const abortController = new AbortController()
   356      setAbortController(abortController)
   357  
   358      const mapper = async (file) => {
   359        try {
   360          setFileStates(next => ( {...next, [file.path]: {status: 'uploading', percent: 0}}))
   361          await uploadFile(config, repo, reference, currentPath, file, progress => {
   362            setFileStates(next => ( {...next, [file.path]: {status: 'uploading', percent: progress}}))
   363          })
   364        } catch (error) {
   365          setFileStates(next => ( {...next, [file.path]: {status: 'error'}}))
   366          setUploadState({ ...initialState, error });
   367          throw error;
   368        }
   369        setFileStates(next => ( {...next, [file.path]: {status: 'done'}}))
   370      }
   371  
   372      setUploadState({...initialState,  inProgress: true });
   373      try {
   374        await pMap(files, mapper, {
   375          concurrency: MAX_PARALLEL_UPLOADS,
   376          signal: abortController.signal
   377        });
   378        onDone();
   379        hide();
   380      } catch (error) {
   381        if (error instanceof DOMException) {
   382          // abort!
   383          onDone();
   384          hide();
   385        } else {
   386          setUploadState({ ...initialState, error });
   387        }
   388      }
   389  
   390  
   391    };
   392  
   393    const changeCurrentPath = useCallback(e => {
   394      setCurrentPath(e.target.value)
   395    }, [setCurrentPath])
   396  
   397    const onRemoveCandidate = useCallback(file => {
   398      return () => setFiles(current => current.filter(f => f !== file))
   399    }, [setFiles])
   400  
   401    return (
   402      <>
   403        <Modal size="xl" show={show} onHide={hide}>
   404          <Modal.Header closeButton>
   405            <Modal.Title>Upload Object</Modal.Title>
   406          </Modal.Header>
   407          <Modal.Body>
   408            <Form
   409              onSubmit={(e) => {
   410                if (uploadState.inProgress) return;
   411                e.preventDefault();
   412                upload();
   413              }}
   414            >
   415              {config?.warnings && (
   416                <Form.Group controlId="warnings" className="mb-3">
   417                  <Warnings warnings={config.warnings} />
   418                </Form.Group>
   419              )}
   420  
   421              <Form.Group controlId="path" className="mb-3">
   422                <Form.Text>Path</Form.Text>
   423                <Form.Control disabled={uploadState.inProgress} defaultValue={currentPath} onChange={changeCurrentPath}/>
   424              </Form.Group>
   425  
   426              <Form.Group controlId="content" className="mb-3">
   427                <div {...getRootProps({className: 'dropzone'})}>
   428                    <input {...getInputProps()} />
   429                    <div className={isDragAccept ? "file-drop-zone file-drop-zone-focus" : "file-drop-zone"}>
   430                      Drag &apos;n&apos; drop files or folders here (or click to select)
   431                    </div>
   432                </div>
   433                <aside className="mt-3">
   434                  {(files && files.length > 0) &&
   435                    <h5>{files.length} File{files.length > 1 ? "s":""} to upload ({humanSize(files.reduce((a,f) => a + f.size ,0))})</h5>
   436                  }
   437                  {files && files.map(file =>
   438                      <UploadCandidate
   439                        key={file.path}
   440                        config={config}
   441                        repo={repo}
   442                        reference={reference}
   443                        file={file}
   444                        path={currentPath}
   445                        state={fileStates[file.path]}
   446                        onRemove={!uploadState.inProgress ? onRemoveCandidate(file) : null}
   447                      />
   448                  )}
   449                </aside>
   450              </Form.Group>
   451            </Form>
   452          {(uploadState.error) ? (<AlertError error={uploadState.error}/>) : (<></>)}
   453        </Modal.Body>
   454      <Modal.Footer>
   455          <Button variant="secondary" onClick={hide}>
   456              Cancel
   457          </Button>
   458          <Button variant="success" disabled={uploadState.inProgress || files.length < 1} onClick={() => {
   459              if (uploadState.inProgress) return;
   460              upload()
   461          }}>
   462              {(uploadState.inProgress) ? 'Uploading...' : 'Upload'}
   463          </Button>
   464      </Modal.Footer>
   465    </Modal>
   466  
   467      <Button
   468        variant={!config.import_support ? "success" : "light"}
   469        disabled={disabled}
   470        onClick={onClick}
   471        >
   472        <UploadIcon /> Upload Object
   473      </Button>
   474    </>
   475    );
   476  };
   477  
   478  const TreeContainer = ({
   479    config,
   480    repo,
   481    reference,
   482    path,
   483    after,
   484    onPaginate,
   485    onRefresh,
   486    onUpload,
   487    onImport,
   488    refreshToken,
   489  }) => {
   490    const { results, error, loading, nextPage } = useAPIWithPagination(() => {
   491      return objects.list(
   492        repo.id,
   493        reference.id,
   494        path,
   495        after,
   496        config.pre_sign_support_ui
   497      );
   498    }, [repo.id, reference.id, path, after, refreshToken]);
   499    const initialState = {
   500      inProgress: false,
   501      error: null,
   502      done: false,
   503    };
   504    const [deleteState, setDeleteState] = useState(initialState);
   505  
   506      if (loading) return <Loading/>;
   507      if (error) return <AlertError error={error}/>;
   508  
   509      return (
   510          <>
   511              {deleteState.error && <AlertError error={deleteState.error} onDismiss={() => setDeleteState(initialState)}/>}
   512              <Tree
   513                  config={{config}}
   514                  repo={repo}
   515                  reference={reference}
   516                  path={(path) ? path : ""}
   517                  showActions={true}
   518                  results={results}
   519                  after={after}
   520                  nextPage={nextPage}
   521                  onPaginate={onPaginate}
   522                  onUpload={onUpload}
   523                  onImport={onImport}
   524                  onDelete={entry => {
   525                      objects
   526                          .delete(repo.id, reference.id, entry.path)
   527                          .catch(error => {
   528                              setDeleteState({...initialState, error: error})
   529                              throw error
   530                          })
   531                          .then(onRefresh)
   532                  }}
   533              /></>
   534      );
   535  }
   536  
   537  const ReadmeContainer = ({
   538    config,
   539    repo,
   540    reference,
   541    path = "",
   542    refreshDep = "",
   543  }) => {
   544    let readmePath = "";
   545  
   546    if (path) {
   547      readmePath = path.endsWith("/")
   548        ? `${path}${README_FILE_NAME}`
   549        : `${path}/${README_FILE_NAME}`;
   550    } else {
   551      readmePath = README_FILE_NAME;
   552    }
   553    const { response, error, loading } = useAPI(
   554      () => objects.head(repo.id, reference.id, readmePath),
   555      [path, refreshDep]
   556    );
   557  
   558    if (loading || error) {
   559      return <></>; // no file found.
   560    }
   561  
   562    const fileExtension = getFileExtension(readmePath);
   563    const contentType = getContentType(response?.headers);
   564  
   565      return (
   566          <FileContents 
   567              repoId={repo.id} 
   568              reference={reference}
   569              path={readmePath}
   570              fileExtension={fileExtension}
   571              contentType={contentType}
   572              error={error}
   573              loading={loading}
   574              showFullNavigator={false}
   575              presign={config.pre_sign_support_ui}
   576          />
   577      );
   578  }
   579  
   580  const NoGCRulesWarning = ({ repoId }) => {
   581    const storageKey = `show_gc_warning_${repoId}`;
   582    const [show, setShow] = useState(
   583      window.localStorage.getItem(storageKey) !== "false"
   584    );
   585    const closeAndRemember = useCallback(() => {
   586      window.localStorage.setItem(storageKey, "false");
   587      setShow(false);
   588    }, [repoId]);
   589  
   590    const { response } = useAPI(async () => {
   591      const repo = await repositories.get(repoId);
   592      if (
   593        !repo.storage_namespace.startsWith("s3:") &&
   594        !repo.storage_namespace.startsWith("http")
   595      ) {
   596        return false;
   597      }
   598      const createdAgo = dayjs().diff(dayjs.unix(repo.creation_date), "days");
   599      if (createdAgo > REPOSITORY_AGE_BEFORE_GC) {
   600        try {
   601          await retention.getGCPolicy(repoId);
   602        } catch (e) {
   603          if (e instanceof NotFoundError) {
   604            return true;
   605          }
   606        }
   607      }
   608      return false;
   609    }, [repoId]);
   610  
   611    if (show && response) {
   612      return (
   613        <Alert variant="warning" onClose={closeAndRemember} dismissible>
   614          <strong>Warning</strong>: No garbage collection rules configured for
   615          this repository.{" "}
   616          <a
   617            href="https://docs.lakefs.io/howto/garbage-collection/"
   618            target="_blank"
   619            rel="noreferrer"
   620          >
   621            Learn More
   622          </a>
   623          .
   624        </Alert>
   625      );
   626    }
   627    return <></>;
   628  };
   629  
   630  const ObjectsBrowser = ({ config, configError }) => {
   631    const router = useRouter();
   632    const { path, after, importDialog } = router.query;
   633    const [searchParams, setSearchParams] = useSearchParams();
   634    const { repo, reference, loading, error } = useRefs();
   635    const [showUpload, setShowUpload] = useState(false);
   636    const [showImport, setShowImport] = useState(false);
   637    const [refreshToken, setRefreshToken] = useState(false);
   638  
   639    const refresh = () => setRefreshToken(!refreshToken);
   640    const parts = (path && path.split("/")) || [];
   641    const searchSuffix = parts.pop();
   642    let searchPrefix = parts.join("/");
   643    searchPrefix = searchPrefix && searchPrefix + "/";
   644  
   645    useEffect(() => {
   646      if (importDialog) {
   647        setShowImport(true);
   648        searchParams.delete("importDialog");
   649        setSearchParams(searchParams);
   650      }
   651    }, [router.route, importDialog, searchParams, setSearchParams]);
   652  
   653    if (loading || !config) return <Loading />;
   654    if (error || configError) return <RepoError error={error || configError} />;
   655  
   656    return (
   657      <>
   658        <ActionsBar>
   659          <ActionGroup orientation="left">
   660            <RefDropdown
   661              emptyText={"Select Branch"}
   662              repo={repo}
   663              selected={reference}
   664              withCommits={true}
   665              withWorkspace={true}
   666              selectRef={(ref) =>
   667                router.push({
   668                  pathname: `/repositories/:repoId/objects`,
   669                  params: {
   670                    repoId: repo.id,
   671                    path: path === undefined ? "" : path,
   672                  },
   673                  query: { ref: ref.id, path: path === undefined ? "" : path },
   674                })
   675              }
   676            />
   677          </ActionGroup>
   678  
   679          <ActionGroup orientation="right">
   680            <PrefixSearchWidget
   681              text="Search by Prefix"
   682              key={path}
   683              defaultValue={searchSuffix}
   684              onFilter={(prefix) => {
   685                const query = { path: "" };
   686                if (searchPrefix !== undefined) query.path = searchPrefix;
   687                if (prefix) query.path += prefix;
   688                if (reference) query.ref = reference.id;
   689                const url = {
   690                  pathname: `/repositories/:repoId/objects`,
   691                  query,
   692                  params: { repoId: repo.id },
   693                };
   694                router.push(url);
   695              }}
   696            />
   697            <RefreshButton onClick={refresh} />
   698            <UploadButton
   699              config={config}
   700              path={path}
   701              repo={repo}
   702              reference={reference}
   703              onDone={refresh}
   704              onClick={() => {
   705                setShowUpload(true);
   706              }}
   707              onHide={() => {
   708                setShowUpload(false);
   709              }}
   710              show={showUpload}
   711              disabled={repo?.read_only}
   712            />
   713            <ImportButton onClick={() => setShowImport(true)} config={config} />
   714            <ImportModal
   715              config={config}
   716              path={path}
   717              repoId={repo.id}
   718              referenceId={reference.id}
   719              referenceType={reference.type}
   720              onDone={refresh}
   721              onHide={() => {
   722                setShowImport(false);
   723              }}
   724              show={showImport}
   725            />
   726          </ActionGroup>
   727        </ActionsBar>
   728  
   729        <NoGCRulesWarning repoId={repo.id} />
   730  
   731        <Box
   732          sx={{
   733            display: "flex",
   734            flexDirection: "column",
   735            gap: "10px",
   736            mb: "30px",
   737          }}
   738        >
   739          <TreeContainer
   740            config={config}
   741            reference={reference}
   742            repo={repo}
   743            path={path ? path : ""}
   744            after={after ? after : ""}
   745            onPaginate={(after) => {
   746              const query = { after };
   747              if (path) query.path = path;
   748              if (reference) query.ref = reference.id;
   749              const url = {
   750                pathname: `/repositories/:repoId/objects`,
   751                query,
   752                params: { repoId: repo.id },
   753              };
   754              router.push(url);
   755            }}
   756            refreshToken={refreshToken}
   757            onUpload={() => {
   758              setShowUpload(true);
   759            }}
   760            onImport={() => {
   761              setShowImport(true);
   762            }}
   763            onRefresh={refresh}
   764          />
   765  
   766          <ReadmeContainer
   767            config={config}
   768            reference={reference}
   769            repo={repo}
   770            path={path}
   771            refreshDep={refreshToken}
   772          />
   773        </Box>
   774      </>
   775    );
   776  };
   777  
   778  const RepositoryObjectsPage = () => {
   779    const config = useStorageConfig();
   780    const [setActivePage] = useOutletContext();
   781    useEffect(() => setActivePage("objects"), [setActivePage]);
   782  
   783    return <ObjectsBrowser config={config} configError={config.error} />;
   784  };
   785  
   786  export default RepositoryObjectsPage;