Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a text box to the Load Project window for URLs #276

Merged
merged 6 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions gui/src/app/util/gists/doesGistExist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Octokit } from "@octokit/rest";

const doesGistExist = async (gistUri: string): Promise<boolean> => {
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;
127 changes: 111 additions & 16 deletions gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 {
Expand All @@ -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 };

Expand Down Expand Up @@ -119,25 +130,53 @@ const LoadProjectPanel: FunctionComponent<LoadProjectProps> = ({ onClose }) => {
[importUploadedZip],
);

const { urlToLoad, setUrlToLoad, tryLoad } = useUrlLoader(setErrorText);

return (
<div className="dialogWrapper">
<Stack spacing={2}>
<div>
You can upload:
<ul>
<li>A .zip file that was previously exported</li>
<li>
A directory of files that were extracted from an exported .zip
file
</li>
<li>An individual *.stan file</li>
<li>
Other individual project files (data.json, meta.json, data.py,
etc.)
</li>
</ul>
</div>
<UploadFiles height={300} onUpload={onUpload} />
<FormControl margin="normal">
<TextField
variant="standard"
label="Project URL"
value={urlToLoad}
onChange={(e) => setUrlToLoad(e.target.value.trim())}
onBlur={() => {
if (tryLoad()) {
onClose();
}
}}
onKeyUp={(e) => {
if (e.key === "Enter" && tryLoad()) {
onClose();
}
}}
></TextField>
<FormHelperText component="div">
You can supply a URL to load a project from:
<ul style={{ margin: 0 }}>
<li>A Stan-Playground URL</li>
<li>A GitHub Gist URL</li>
</ul>
</FormHelperText>
<UploadFiles height={300} onUpload={onUpload} />
<FormHelperText component="div">
You can upload:
<ul style={{ margin: 0 }}>
<li>A .zip file that was previously exported</li>
<li>
A directory of files that were extracted from an exported .zip
file
</li>
<li>An individual *.stan file</li>
<li>
Other individual project files (data.json, meta.json, data.py,
etc.)
</li>
</ul>
</FormHelperText>
</FormControl>

{errorText !== "" && (
<Typography color="error.main">{errorText}</Typography>
)}
Expand Down Expand Up @@ -193,4 +232,60 @@ const LoadProjectPanel: FunctionComponent<LoadProjectProps> = ({ 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;