diff --git a/gui/src/app/App.tsx b/gui/src/app/App.tsx index 6bea45a..98854d4 100644 --- a/gui/src/app/App.tsx +++ b/gui/src/app/App.tsx @@ -1,9 +1,9 @@ +import { CompileContextProvider } from "@SpCompilation/CompileContextProvider"; import ToggleableThemeProvider from "./ToggleableThemeProvider"; import ProjectContextProvider from "@SpCore/ProjectContextProvider"; import HomePage from "@SpPages/HomePage"; import { Analytics } from "@vercel/analytics/react"; import { BrowserRouter } from "react-router-dom"; -import { CompileContextProvider } from "./CompileContext/CompileContextProvider"; const App = () => { return ( diff --git a/gui/src/app/CompileContext/CompileContext.ts b/gui/src/app/Compilation/Context/CompileContext.ts similarity index 86% rename from gui/src/app/CompileContext/CompileContext.ts rename to gui/src/app/Compilation/Context/CompileContext.ts index cfd9d01..489ded2 100644 --- a/gui/src/app/CompileContext/CompileContext.ts +++ b/gui/src/app/Compilation/Context/CompileContext.ts @@ -16,6 +16,8 @@ type CompileContextType = { setValidSyntax: (valid: boolean) => void; stanWasmServerUrl: string; setStanWasmServerUrl: (url: string) => void; + isConnected: boolean; + retryConnection: () => void; }; export const CompileContext = createContext({ @@ -26,4 +28,6 @@ export const CompileContext = createContext({ setValidSyntax: () => {}, stanWasmServerUrl: "", setStanWasmServerUrl: () => {}, + isConnected: false, + retryConnection: () => {}, }); diff --git a/gui/src/app/CompileContext/CompileContextProvider.tsx b/gui/src/app/Compilation/Context/CompileContextProvider.tsx similarity index 68% rename from gui/src/app/CompileContext/CompileContextProvider.tsx rename to gui/src/app/Compilation/Context/CompileContextProvider.tsx index 9ac5271..15a5ee8 100644 --- a/gui/src/app/CompileContext/CompileContextProvider.tsx +++ b/gui/src/app/Compilation/Context/CompileContextProvider.tsx @@ -1,5 +1,4 @@ import { ProjectContext } from "@SpCore/ProjectContextProvider"; -import compileStanProgram from "@SpCompileContext/compileStanProgram"; import { FunctionComponent, PropsWithChildren, @@ -8,13 +7,42 @@ import { useEffect, useState, } from "react"; -import { CompileContext, CompileStatus } from "./CompileContext"; -import { publicCompilationServerUrl } from "@SpStanc/CompilationServerConnectionControl"; +import { CompileContext, CompileStatus } from "@SpCompilation/CompileContext"; +import { publicCompilationServerUrl } from "@SpCompilation/Constants"; +import compileStanProgram from "./compileStanProgram"; type CompileContextProviderProps = { // none }; +const useIsConnected = (stanWasmServerUrl: string) => { + const probeUrl = `${stanWasmServerUrl}/probe`; + const [isConnected, setIsConnected] = useState(false); + const [retryCode, setRetryCode] = useState(0); + const retryConnection = useCallback(() => { + setRetryCode((r) => r + 1); + }, []); + useEffect(() => { + setIsConnected(false); + if (!probeUrl.startsWith("http://") && !probeUrl.startsWith("https://")) { + // important to do this check because otherwise fetch may succeed because + // the server of this web app may respond with success + return; + } + (async () => { + try { + const response = await fetch(probeUrl); + if (response.status === 200) { + setIsConnected(true); + } + } catch (err) { + setIsConnected(false); + } + })(); + }, [probeUrl, retryCode]); + return { isConnected, retryConnection }; +}; + const initialStanWasmServerUrl = localStorage.getItem("stanWasmServerUrl") || publicCompilationServerUrl; @@ -83,6 +111,8 @@ export const CompileContextProvider: FunctionComponent< stanWasmServerUrl, ]); + const { isConnected, retryConnection } = useIsConnected(stanWasmServerUrl); + return ( {children} diff --git a/gui/src/app/CompileContext/compileStanProgram.ts b/gui/src/app/Compilation/Context/compileStanProgram.ts similarity index 100% rename from gui/src/app/CompileContext/compileStanProgram.ts rename to gui/src/app/Compilation/Context/compileStanProgram.ts diff --git a/gui/src/app/CompileContext/mainJsUrlCache.ts b/gui/src/app/Compilation/Context/mainJsUrlCache.ts similarity index 100% rename from gui/src/app/CompileContext/mainJsUrlCache.ts rename to gui/src/app/Compilation/Context/mainJsUrlCache.ts diff --git a/gui/src/app/CompilationServerConnectionControl/ConfigureCompilationServerDialog.tsx b/gui/src/app/Compilation/Control/CompilationServerDialog.tsx similarity index 96% rename from gui/src/app/CompilationServerConnectionControl/ConfigureCompilationServerDialog.tsx rename to gui/src/app/Compilation/Control/CompilationServerDialog.tsx index d6a179e..a955f1b 100644 --- a/gui/src/app/CompilationServerConnectionControl/ConfigureCompilationServerDialog.tsx +++ b/gui/src/app/Compilation/Control/CompilationServerDialog.tsx @@ -1,4 +1,4 @@ -import { CompileContext } from "@SpCompileContext/CompileContext"; +import { CompileContext } from "@SpCompilation/CompileContext"; import { Refresh } from "@mui/icons-material"; import Divider from "@mui/material/Divider"; import FormControl from "@mui/material/FormControl"; @@ -10,11 +10,11 @@ import RadioGroup from "@mui/material/RadioGroup"; import TextField from "@mui/material/TextField"; import Typography from "@mui/material/Typography"; import { FunctionComponent, useCallback, useContext } from "react"; +import { serverTypeForUrl } from "./CompilationServerToolbar"; import { localCompilationServerUrl, publicCompilationServerUrl, - serverTypeForUrl, -} from "./CompilationServerConnectionControl"; +} from "./Constants"; type ConfigureCompilationServerDialogProps = { isConnected: boolean; diff --git a/gui/src/app/CompilationServerConnectionControl/CompilationServerConnectionControl.tsx b/gui/src/app/Compilation/Control/CompilationServerToolbar.tsx similarity index 53% rename from gui/src/app/CompilationServerConnectionControl/CompilationServerConnectionControl.tsx rename to gui/src/app/Compilation/Control/CompilationServerToolbar.tsx index bdc50b5..6d27113 100644 --- a/gui/src/app/CompilationServerConnectionControl/CompilationServerConnectionControl.tsx +++ b/gui/src/app/Compilation/Control/CompilationServerToolbar.tsx @@ -3,23 +3,16 @@ import { Cancel, Check } from "@mui/icons-material"; import CloseableDialog, { useDialogControls, } from "@SpComponents/CloseableDialog"; -import { - FunctionComponent, - useCallback, - useContext, - useEffect, - useState, -} from "react"; -import ConfigureCompilationServerDialog from "./ConfigureCompilationServerDialog"; +import { FunctionComponent, useCallback, useContext } from "react"; +import ConfigureCompilationServerDialog from "./CompilationServerDialog"; import IconButton from "@mui/material/IconButton"; import Typography from "@mui/material/Typography"; -import { CompileContext } from "@SpCompileContext/CompileContext"; - -export const publicCompilationServerUrl = - "https://stan-wasm.flatironinstitute.org"; -export const localCompilationServerUrl = "http://localhost:8083"; - -type ServerType = "public" | "local" | "custom"; +import { CompileContext } from "@SpCompilation/CompileContext"; +import { + ServerType, + publicCompilationServerUrl, + localCompilationServerUrl, +} from "./Constants"; type CompilationServerConnectionControlProps = { // none @@ -28,8 +21,8 @@ type CompilationServerConnectionControlProps = { const CompilationServerConnectionControl: FunctionComponent< CompilationServerConnectionControlProps > = () => { - const { stanWasmServerUrl } = useContext(CompileContext); - const { isConnected, retryConnection } = useIsConnected(stanWasmServerUrl); + const { stanWasmServerUrl, isConnected, retryConnection } = + useContext(CompileContext); const { handleOpen: openDialog, @@ -80,32 +73,4 @@ export const serverTypeForUrl = (url: string): ServerType => { : "custom"; }; -const useIsConnected = (stanWasmServerUrl: string) => { - const probeUrl = `${stanWasmServerUrl}/probe`; - const [isConnected, setIsConnected] = useState(false); - const [retryCode, setRetryCode] = useState(0); - const retryConnection = useCallback(() => { - setRetryCode((r) => r + 1); - }, []); - useEffect(() => { - setIsConnected(false); - if (!probeUrl.startsWith("http://") && !probeUrl.startsWith("https://")) { - // important to do this check because otherwise fetch may succeed because - // the server of this web app may respond with success - return; - } - (async () => { - try { - const response = await fetch(probeUrl); - if (response.status === 200) { - setIsConnected(true); - } - } catch (err) { - setIsConnected(false); - } - })(); - }, [probeUrl, retryCode]); - return { isConnected, retryConnection }; -}; - export default CompilationServerConnectionControl; diff --git a/gui/src/app/Compilation/Control/Constants.ts b/gui/src/app/Compilation/Control/Constants.ts new file mode 100644 index 0000000..678cb20 --- /dev/null +++ b/gui/src/app/Compilation/Control/Constants.ts @@ -0,0 +1,4 @@ +export const publicCompilationServerUrl = + "https://stan-wasm.flatironinstitute.org"; +export const localCompilationServerUrl = "http://localhost:8083"; +export type ServerType = "public" | "local" | "custom"; diff --git a/gui/src/app/FileEditor/StanFileEditor.tsx b/gui/src/app/FileEditor/StanFileEditor.tsx index d8eeb30..1182f2f 100644 --- a/gui/src/app/FileEditor/StanFileEditor.tsx +++ b/gui/src/app/FileEditor/StanFileEditor.tsx @@ -5,7 +5,7 @@ import TextEditor from "@SpComponents/TextEditor"; import { ToolbarItem } from "@SpComponents/ToolBar"; import { stancErrorsToCodeMarkers } from "@SpStanc/Linting"; import useStanc from "@SpStanc/useStanc"; -import { CompileContext } from "@SpCompileContext/CompileContext"; +import { CompileContext } from "@SpCompilation/CompileContext"; import { FunctionComponent, useContext, useMemo, useState } from "react"; type Props = { @@ -32,7 +32,7 @@ const StanFileEditor: FunctionComponent = ({ setEditedFileContent, ); - const { compileStatus, compileMessage, compile, validSyntax } = + const { compileStatus, compileMessage, compile, validSyntax, isConnected } = useContext(CompileContext); const hasWarnings = useMemo(() => { @@ -89,17 +89,15 @@ const StanFileEditor: FunctionComponent = ({ }); } if (editedFileContent && editedFileContent === fileContent) { - if (compileStatus !== "compiling") { - if (validSyntax) { - ret.push({ - type: "button", - tooltip: "Compile Stan model", - label: "Compile", - icon: , - onClick: compile, - color: "info.dark", - }); - } + if (compileStatus !== "compiling" && validSyntax && isConnected) { + ret.push({ + type: "button", + tooltip: "Compile Stan model", + label: "Compile", + icon: , + onClick: compile, + color: "info.dark", + }); } if (compileStatus !== "") { ret.push({ @@ -118,15 +116,16 @@ const StanFileEditor: FunctionComponent = ({ return ret; }, [ + validSyntax, editedFileContent, + hasWarnings, + readOnly, fileContent, - compile, requestFormat, - validSyntax, compileStatus, + isConnected, + compile, compileMessage, - readOnly, - hasWarnings, ]); const isCompiling = compileStatus === "compiling"; diff --git a/gui/src/app/RunPanel/RunPanel.tsx b/gui/src/app/RunPanel/RunPanel.tsx index ce1ee76..442ae77 100644 --- a/gui/src/app/RunPanel/RunPanel.tsx +++ b/gui/src/app/RunPanel/RunPanel.tsx @@ -1,18 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { FunctionComponent, useCallback, useContext } from "react"; +import { FunctionComponent, useCallback, useContext, useMemo } from "react"; import Button from "@mui/material/Button"; -import { CompileContext } from "@SpCompileContext/CompileContext"; +import { CompileContext } from "@SpCompilation/CompileContext"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; -import { - ProjectDataModel, - SamplingOpts, - modelHasUnsavedChanges, -} from "@SpCore/ProjectDataModel"; +import { SamplingOpts, modelHasUnsavedChanges } from "@SpCore/ProjectDataModel"; import StanSampler from "@SpStanSampler/StanSampler"; import { StanRun } from "@SpStanSampler/useStanSampler"; import CompiledRunPanel from "./CompiledRunPanel"; import CircularProgress from "@mui/material/CircularProgress"; +import Tooltip from "@mui/material/Tooltip"; type RunPanelProps = { sampler?: StanSampler; @@ -41,23 +38,36 @@ const RunPanel: FunctionComponent = ({ sampler.cancel(); }, [sampler]); - const { compile, compileStatus, validSyntax } = useContext(CompileContext); + const { compile, compileStatus, validSyntax, isConnected } = + useContext(CompileContext); const { data: projectData } = useContext(ProjectContext); + const tooltip = useMemo(() => { + if (!validSyntax) return "Syntax error"; + if (!isConnected) return "Not connected to compilation server"; + if (!projectData.stanFileContent.trim()) return "No model to compile"; + if (modelHasUnsavedChanges(projectData)) return "Model has unsaved changes"; + return ""; + }, [isConnected, projectData, validSyntax]); + if (!dataIsSaved) { return
Data not saved
; } const compileDiv = (
- + + + + +
); @@ -89,14 +99,4 @@ const RunPanel: FunctionComponent = ({ ); }; -const isCompileModelDisabled = ( - projectData: ProjectDataModel, - validSyntax: boolean, -) => { - if (!validSyntax) return true; - if (!projectData.stanFileContent.trim()) return true; - if (modelHasUnsavedChanges(projectData)) return true; - return false; -}; - export default RunPanel; diff --git a/gui/src/app/Stanc/useStanc.ts b/gui/src/app/Stanc/useStanc.ts index cd24191..12162cb 100644 --- a/gui/src/app/Stanc/useStanc.ts +++ b/gui/src/app/Stanc/useStanc.ts @@ -7,7 +7,7 @@ import { useCallback, useContext, useEffect, useMemo, useState } from "react"; // https://vitejs.dev/guide/assets#importing-script-as-a-worker // https://vitejs.dev/guide/assets#importing-asset-as-url import stancWorkerURL from "@SpStanc/stancWorker?worker&url"; -import { CompileContext } from "@SpCompileContext/CompileContext"; +import { CompileContext } from "@SpCompilation/CompileContext"; const useStanc = ( modelName: string, diff --git a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx index 78ed28b..b5aaa52 100644 --- a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx +++ b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx @@ -15,7 +15,7 @@ import { import AnalysisPyWindow from "@SpScripting/Analysis/AnalysisPyWindow"; import useStanSampler, { StanRun } from "@SpStanSampler/useStanSampler"; import AnalysisRWindow from "@SpScripting/Analysis/AnalysisRWindow"; -import { CompileContext } from "@SpCompileContext/CompileContext"; +import { CompileContext } from "@SpCompilation/CompileContext"; type SamplingWindowProps = { // none diff --git a/gui/src/app/pages/HomePage/TopBar.tsx b/gui/src/app/pages/HomePage/TopBar.tsx index 6099667..821f666 100644 --- a/gui/src/app/pages/HomePage/TopBar.tsx +++ b/gui/src/app/pages/HomePage/TopBar.tsx @@ -1,4 +1,4 @@ -import CompilationServerConnectionControl from "@SpStanc/CompilationServerConnectionControl"; +import CompilationServerConnectionControl from "@SpCompilation/CompilationServerToolbar"; import { Brightness7, DarkMode, Menu, QuestionMark } from "@mui/icons-material"; import AppBar from "@mui/material/AppBar"; import IconButton from "@mui/material/IconButton"; diff --git a/gui/tsconfig.json b/gui/tsconfig.json index a5db7f4..9ab7b8d 100644 --- a/gui/tsconfig.json +++ b/gui/tsconfig.json @@ -5,7 +5,6 @@ "lib": ["ES2020", "DOM", "DOM.Iterable", "webworker"], "module": "ESNext", "skipLibCheck": true, - "types": ["vite/client"], /* Bundler mode */ "moduleResolution": "bundler", @@ -14,13 +13,11 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - /* Imports */ "baseUrl": "./src", "paths": { @@ -37,14 +34,21 @@ "@SpCore/gists/*": ["app/gists/*"], /* We are playing a bit fast and loose with the distinction */ "@SpPages/*": ["app/pages/HomePage/*"], - "@SpStanc/*": ["app/Stanc/*", "app/CompilationServerConnectionControl/*"], + "@SpStanc/*": ["app/Stanc/*"], "@SpStanSampler/*": ["app/StanSampler/*"], "@SpUtil/*": ["app/util/*"], "@SpStanStats/*": ["app/util/stan_stats/*"], "@SpScripting/*": ["app/Scripting/*"], - "@SpCompileContext/*": ["app/CompileContext/*"] + "@SpCompilation/*": [ + "app/Compilation/Context/*", + "app/Compilation/Control/*" + ] } }, "include": ["src", "test"], - "references": [{ "path": "./tsconfig.node.json" }] + "references": [ + { + "path": "./tsconfig.node.json" + } + ] }