From 7a06109156806250d21b78ae2bf25a696bad5a21 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 7 Feb 2025 19:38:26 +0000 Subject: [PATCH 1/5] Checkpoint --- .../LoadProjectWindow/LoadProjectPanel.tsx | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx index 263be88c..21949beb 100644 --- a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx +++ b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx @@ -1,4 +1,10 @@ -import { FunctionComponent, useCallback, use, useState } from "react"; +import { + FunctionComponent, + useCallback, + use, + useState, + useEffect, +} from "react"; import { Delete } from "@mui/icons-material"; import Button from "@mui/material/Button"; @@ -23,6 +29,10 @@ import { parseFile, } from "@SpCore/Project/ProjectSerialization"; import UploadFiles from "./UploadFiles"; +import FormControl from "@mui/material/FormControl"; +import TextField from "@mui/material/TextField"; +import FormHelperText from "@mui/material/FormHelperText"; +import { useNavigate } from "react-router-dom"; type File = { name: string; content: ArrayBuffer }; @@ -119,25 +129,63 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { [importUploadedZip], ); + const [urlToLoad, setUrlToLoad] = useState(""); + const navigate = useNavigate(); + + useEffect(() => { + if (urlToLoad === "") return; + if (urlToLoad.startsWith("https://gist.github.com/")) { + // TODO test if gist exists first? + navigate(`/?project=${urlToLoad}`); + onClose(); + } else if ( + urlToLoad.startsWith("https://stan-playground.flatironinstitute.org/") + ) { + // TODO test if valid first? + navigate( + `/{urlToLoad.replace("https://stan-playground.flatironinstitute.org/", "")}`, + ); + onClose(); + } else { + setErrorText("Unsupported URL: " + urlToLoad); + } + }, [navigate, onClose, urlToLoad]); + 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)} + > + + 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} )} From 1297fb155e01b95735df850058bf40003810055d Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Wed, 12 Feb 2025 19:36:40 +0000 Subject: [PATCH 2/5] Validation --- gui/src/app/util/gists/doesGistExist.ts | 23 ++++++++ .../LoadProjectWindow/LoadProjectPanel.tsx | 59 +++++++++++++------ 2 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 gui/src/app/util/gists/doesGistExist.ts 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 21949beb..f5dc210a 100644 --- a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx +++ b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx @@ -5,6 +5,7 @@ import { useState, useEffect, } from "react"; +import { useNavigate } from "react-router-dom"; import { Delete } from "@mui/icons-material"; import Button from "@mui/material/Button"; @@ -16,6 +17,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 { @@ -28,11 +32,13 @@ import { deserializeZipToFiles, parseFile, } from "@SpCore/Project/ProjectSerialization"; +import doesGistExist from "@SpUtil/gists/doesGistExist"; + import UploadFiles from "./UploadFiles"; -import FormControl from "@mui/material/FormControl"; -import TextField from "@mui/material/TextField"; -import FormHelperText from "@mui/material/FormHelperText"; -import { useNavigate } from "react-router-dom"; +import { + fromQueryParams, + queryStringHasParameters, +} from "@SpCore/Project/ProjectQueryLoading"; type File = { name: string; content: ArrayBuffer }; @@ -134,22 +140,41 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { useEffect(() => { if (urlToLoad === "") return; - if (urlToLoad.startsWith("https://gist.github.com/")) { - // TODO test if gist exists first? - navigate(`/?project=${urlToLoad}`); - onClose(); - } else if ( + if ( urlToLoad.startsWith("https://stan-playground.flatironinstitute.org/") ) { - // TODO test if valid first? - navigate( - `/{urlToLoad.replace("https://stan-playground.flatironinstitute.org/", "")}`, - ); - onClose(); + const queriesOnly = new URLSearchParams(urlToLoad.split("?", 2)[1]); + if (queryStringHasParameters(fromQueryParams(queriesOnly))) { + navigate(`?${queriesOnly.toString()}`); + setUrlToLoad(""); + onClose(); + } + } else if (urlToLoad.startsWith("https://gist.github.com/")) { + let cancelled = false; + + doesGistExist(urlToLoad).then((exists) => { + if (exists) { + if (cancelled) return; + navigate(`?project=${urlToLoad}`); + setUrlToLoad(""); + onClose(); + } else { + if (cancelled) return; + setErrorText("Gist not found: " + urlToLoad); + } + }); + + return () => { + cancelled = true; + }; } else { - setErrorText("Unsupported URL: " + urlToLoad); + setErrorText( + "Unsupported URL: " + + urlToLoad + + " (must be a Stan-Playground URL or a GitHub Gist URL)", + ); } - }, [navigate, onClose, urlToLoad]); + }, [navigate, onClose, urlToLoad, setErrorText]); return (
@@ -159,7 +184,7 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { variant="standard" label="Project URL" value={urlToLoad} - onChange={(e) => setUrlToLoad(e.target.value)} + onChange={(e) => setUrlToLoad(e.target.value.trim())} > You can supply a URL to load a project from: From 5b47f40ae1eb1259b1f59baf49b422f5d14dd67d Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Wed, 12 Feb 2025 19:53:06 +0000 Subject: [PATCH 3/5] Pull out into hook --- .../LoadProjectWindow/LoadProjectPanel.tsx | 90 ++++++++++--------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx index f5dc210a..6b320629 100644 --- a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx +++ b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx @@ -135,46 +135,7 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { [importUploadedZip], ); - const [urlToLoad, setUrlToLoad] = useState(""); - const navigate = useNavigate(); - - useEffect(() => { - if (urlToLoad === "") return; - if ( - urlToLoad.startsWith("https://stan-playground.flatironinstitute.org/") - ) { - const queriesOnly = new URLSearchParams(urlToLoad.split("?", 2)[1]); - if (queryStringHasParameters(fromQueryParams(queriesOnly))) { - navigate(`?${queriesOnly.toString()}`); - setUrlToLoad(""); - onClose(); - } - } else if (urlToLoad.startsWith("https://gist.github.com/")) { - let cancelled = false; - - doesGistExist(urlToLoad).then((exists) => { - if (exists) { - if (cancelled) return; - navigate(`?project=${urlToLoad}`); - setUrlToLoad(""); - onClose(); - } else { - if (cancelled) return; - setErrorText("Gist not found: " + urlToLoad); - } - }); - - return () => { - cancelled = true; - }; - } else { - setErrorText( - "Unsupported URL: " + - urlToLoad + - " (must be a Stan-Playground URL or a GitHub Gist URL)", - ); - } - }, [navigate, onClose, urlToLoad, setErrorText]); + const { urlToLoad, setUrlToLoad } = useUrlLoader({ onClose, setErrorText }); return (
@@ -266,4 +227,53 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { ); }; +const useUrlLoader = (params: { + onClose: () => void; + setErrorText: (text: string) => void; +}) => { + const { onClose, setErrorText } = params; + const [urlToLoad, setUrlToLoad] = useState(""); + const navigate = useNavigate(); + + useEffect(() => { + if (urlToLoad === "") return; + if ( + urlToLoad.startsWith("https://stan-playground.flatironinstitute.org/") + ) { + const queriesOnly = new URLSearchParams(urlToLoad.split("?", 2)[1]); + if (queryStringHasParameters(fromQueryParams(queriesOnly))) { + navigate(`?${queriesOnly.toString()}`); + setUrlToLoad(""); + onClose(); + } + } else if (urlToLoad.startsWith("https://gist.github.com/")) { + let cancelled = false; + + doesGistExist(urlToLoad).then((exists) => { + if (exists) { + if (cancelled) return; + navigate(`?project=${urlToLoad}`); + setUrlToLoad(""); + onClose(); + } else { + if (cancelled) return; + setErrorText("Gist not found: " + urlToLoad); + } + }); + + return () => { + cancelled = true; + }; + } else { + setErrorText( + "Unsupported URL: " + + urlToLoad + + " (must be a Stan-Playground URL or a GitHub Gist URL)", + ); + } + }, [navigate, onClose, setErrorText, urlToLoad]); + + return { urlToLoad, setUrlToLoad }; +}; + export default LoadProjectPanel; From ca06f0799494ca019671032a708f8b0dee90892c Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Thu, 13 Feb 2025 17:04:45 +0000 Subject: [PATCH 4/5] Only perform load on Enter/blur --- .../LoadProjectWindow/LoadProjectPanel.tsx | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx index 6b320629..72bfb809 100644 --- a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx +++ b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx @@ -1,10 +1,4 @@ -import { - FunctionComponent, - useCallback, - use, - useState, - useEffect, -} from "react"; +import { FunctionComponent, useCallback, use, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Delete } from "@mui/icons-material"; @@ -135,7 +129,10 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { [importUploadedZip], ); - const { urlToLoad, setUrlToLoad } = useUrlLoader({ onClose, setErrorText }); + const { urlToLoad, setUrlToLoad, tryLoad } = useUrlLoader({ + onClose, + setErrorText, + }); return (
@@ -146,6 +143,12 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { label="Project URL" value={urlToLoad} onChange={(e) => setUrlToLoad(e.target.value.trim())} + onBlur={tryLoad} + onKeyUp={(e) => { + if (e.key === "Enter") { + tryLoad(); + } + }} > You can supply a URL to load a project from: @@ -235,7 +238,7 @@ const useUrlLoader = (params: { const [urlToLoad, setUrlToLoad] = useState(""); const navigate = useNavigate(); - useEffect(() => { + const tryLoad = useCallback(() => { if (urlToLoad === "") return; if ( urlToLoad.startsWith("https://stan-playground.flatironinstitute.org/") @@ -247,23 +250,15 @@ const useUrlLoader = (params: { onClose(); } } else if (urlToLoad.startsWith("https://gist.github.com/")) { - let cancelled = false; - doesGistExist(urlToLoad).then((exists) => { if (exists) { - if (cancelled) return; navigate(`?project=${urlToLoad}`); setUrlToLoad(""); onClose(); } else { - if (cancelled) return; setErrorText("Gist not found: " + urlToLoad); } }); - - return () => { - cancelled = true; - }; } else { setErrorText( "Unsupported URL: " + @@ -273,7 +268,7 @@ const useUrlLoader = (params: { } }, [navigate, onClose, setErrorText, urlToLoad]); - return { urlToLoad, setUrlToLoad }; + return { urlToLoad, setUrlToLoad, tryLoad }; }; export default LoadProjectPanel; From e4fa653f69f0479dfa2e3d6ae103a0552469a98c Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Thu, 13 Feb 2025 21:09:25 +0000 Subject: [PATCH 5/5] Improve feedback to user --- .../LoadProjectWindow/LoadProjectPanel.tsx | 99 +++++++++++-------- 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx index 72bfb809..8e1291bd 100644 --- a/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx +++ b/gui/src/app/windows/LoadProjectWindow/LoadProjectPanel.tsx @@ -31,6 +31,7 @@ import doesGistExist from "@SpUtil/gists/doesGistExist"; import UploadFiles from "./UploadFiles"; import { fromQueryParams, + QueryParamKeys, queryStringHasParameters, } from "@SpCore/Project/ProjectQueryLoading"; @@ -129,10 +130,7 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { [importUploadedZip], ); - const { urlToLoad, setUrlToLoad, tryLoad } = useUrlLoader({ - onClose, - setErrorText, - }); + const { urlToLoad, setUrlToLoad, tryLoad } = useUrlLoader(setErrorText); return (
@@ -143,10 +141,14 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { label="Project URL" value={urlToLoad} onChange={(e) => setUrlToLoad(e.target.value.trim())} - onBlur={tryLoad} + onBlur={() => { + if (tryLoad()) { + onClose(); + } + }} onKeyUp={(e) => { - if (e.key === "Enter") { - tryLoad(); + if (e.key === "Enter" && tryLoad()) { + onClose(); } }} > @@ -230,43 +232,58 @@ const LoadProjectPanel: FunctionComponent = ({ onClose }) => { ); }; -const useUrlLoader = (params: { - onClose: () => void; - setErrorText: (text: string) => void; -}) => { - const { onClose, setErrorText } = params; - const [urlToLoad, setUrlToLoad] = useState(""); - const navigate = useNavigate(); +const useUrlLoader = (setErrorText: (text: string) => void) => { + const [urlToLoad, setURLRaw] = useState(""); + const [query, setQuery] = useState(""); - const tryLoad = useCallback(() => { - if (urlToLoad === "") return; - if ( - urlToLoad.startsWith("https://stan-playground.flatironinstitute.org/") - ) { - const queriesOnly = new URLSearchParams(urlToLoad.split("?", 2)[1]); - if (queryStringHasParameters(fromQueryParams(queriesOnly))) { - navigate(`?${queriesOnly.toString()}`); - setUrlToLoad(""); - onClose(); - } - } else if (urlToLoad.startsWith("https://gist.github.com/")) { - doesGistExist(urlToLoad).then((exists) => { - if (exists) { - navigate(`?project=${urlToLoad}`); - setUrlToLoad(""); - onClose(); + 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("Gist not found: " + urlToLoad); + setErrorText( + "Stan-Playground URL does not contain any relevant data: " + + url + + " (should contain at least one of " + + Object.values(QueryParamKeys).join(", ") + + ")", + ); } - }); - } else { - setErrorText( - "Unsupported URL: " + - urlToLoad + - " (must be a Stan-Playground URL or a GitHub Gist URL)", - ); - } - }, [navigate, onClose, setErrorText, urlToLoad]); + } 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 }; };