diff --git a/gui/src/app/util/gists/doesGistExist.ts b/gui/src/app/util/gists/doesGistExist.ts new file mode 100644 index 00000000..e5b54a41 --- /dev/null +++ b/gui/src/app/util/gists/doesGistExist.ts @@ -0,0 +1,23 @@ +import { Octokit } from "@octokit/rest"; + +const doesGistExist = async (gistUri: string): Promise => { + const parts = gistUri.split("/"); + const gistId = parts[parts.length - 1]; + if (!gistId) { + return false; + } + const octokit = new Octokit(); + try { + const r = await octokit.request("HEAD /gists/{gist_id}", { + gist_id: gistId, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + }); + return r.status === 200; + } catch (e) { + return false; + } +}; + +export default doesGistExist; diff --git a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx index 263be88c..8e1291bd 100644 --- a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx +++ b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx @@ -1,4 +1,5 @@ import { FunctionComponent, useCallback, use, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { Delete } from "@mui/icons-material"; import Button from "@mui/material/Button"; @@ -10,6 +11,9 @@ import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; import TableContainer from "@mui/material/TableContainer"; +import FormControl from "@mui/material/FormControl"; +import TextField from "@mui/material/TextField"; +import FormHelperText from "@mui/material/FormHelperText"; import { AlternatingTableRow } from "@SpComponents/StyledTables"; import { @@ -22,7 +26,14 @@ import { deserializeZipToFiles, parseFile, } from "@SpCore/Project/ProjectSerialization"; +import doesGistExist from "@SpUtil/gists/doesGistExist"; + import UploadFiles from "./UploadFiles"; +import { + fromQueryParams, + QueryParamKeys, + queryStringHasParameters, +} from "@SpCore/Project/ProjectQueryLoading"; type File = { name: string; content: ArrayBuffer }; @@ -119,25 +130,53 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { [importUploadedZip], ); + const { urlToLoad, setUrlToLoad, tryLoad } = useUrlLoader(setErrorText); + return (
-
- You can upload: -
    -
  • A .zip file that was previously exported
  • -
  • - A directory of files that were extracted from an exported .zip - file -
  • -
  • An individual *.stan file
  • -
  • - Other individual project files (data.json, meta.json, data.py, - etc.) -
  • -
-
- + + setUrlToLoad(e.target.value.trim())} + onBlur={() => { + if (tryLoad()) { + onClose(); + } + }} + onKeyUp={(e) => { + if (e.key === "Enter" && tryLoad()) { + onClose(); + } + }} + > + + You can supply a URL to load a project from: +
    +
  • A Stan-Playground URL
  • +
  • A GitHub Gist URL
  • +
+
+ + + You can upload: +
    +
  • A .zip file that was previously exported
  • +
  • + A directory of files that were extracted from an exported .zip + file +
  • +
  • An individual *.stan file
  • +
  • + Other individual project files (data.json, meta.json, data.py, + etc.) +
  • +
+
+
+ {errorText !== "" && ( {errorText} )} @@ -193,4 +232,60 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { ); }; +const useUrlLoader = (setErrorText: (text: string) => void) => { + const [urlToLoad, setURLRaw] = useState(""); + const [query, setQuery] = useState(""); + + const setUrlToLoad = useCallback( + (url: string) => { + setURLRaw(url); + setQuery(""); + + if (url === "") return; + if (url.startsWith("https://stan-playground.flatironinstitute.org/")) { + const queriesOnly = new URLSearchParams(url.split("?", 2)[1]); + if (queryStringHasParameters(fromQueryParams(queriesOnly))) { + setQuery(`?${queriesOnly.toString()}`); + setErrorText(""); + } else { + setErrorText( + "Stan-Playground URL does not contain any relevant data: " + + url + + " (should contain at least one of " + + Object.values(QueryParamKeys).join(", ") + + ")", + ); + } + } else if (url.startsWith("https://gist.github.com/")) { + doesGistExist(url).then((exists) => { + if (exists) { + setQuery(`?project=${url}`); + setErrorText(""); + } else { + setErrorText("Gist not found: " + url); + } + }); + } else { + setErrorText( + "Unsupported URL: " + + url + + " (must be a Stan-Playground URL or a GitHub Gist URL)", + ); + } + }, + [setErrorText], + ); + + const navigate = useNavigate(); + + const tryLoad = useCallback(() => { + if (query === "") return false; + navigate(query, { replace: true }); + setURLRaw(""); + return true; + }, [navigate, query]); + + return { urlToLoad, setUrlToLoad, tryLoad }; +}; + export default LoadProjectPanel;