diff --git a/src/components/Settings/ProjectSettings/About.tsx b/src/components/Settings/ProjectSettings/About.tsx index 46a294ef..88708a9b 100644 --- a/src/components/Settings/ProjectSettings/About.tsx +++ b/src/components/Settings/ProjectSettings/About.tsx @@ -129,7 +129,11 @@ export default function About() { {name} v{version} - {latestUpdate === null ? ( + {checkState === "LOADING" ? ( + + Checking for updates… + + ) : latestUpdate === null ? ( Up to date diff --git a/src/components/Settings/ProjectSettings/CloudRun.tsx b/src/components/Settings/ProjectSettings/CloudRun.tsx deleted file mode 100644 index 787bbfaf..00000000 --- a/src/components/Settings/ProjectSettings/CloudRun.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Typography, Link, Grid, TextField } from "@mui/material"; -import LoadingButton from "@mui/lab/LoadingButton"; -import InlineOpenInNewIcon from "components/InlineOpenInNewIcon"; - -import { IProjectSettingsChildProps } from "pages/Settings/ProjectSettings"; -import WIKI_LINKS from "constants/wikiLinks"; -import { name } from "@root/package.json"; -import { runRepoUrl } from "constants/runRoutes"; - -export default function rowyRun({ - settings, - updateSettings, -}: IProjectSettingsChildProps) { - return ( - <> - - {name} Run is a Cloud Run instance that provides back-end functionality, - such as table action scripts, user management, and easy Cloud Function - deployment.{" "} - - Learn more - - - - -
- - - - If you have not yet deployed {name} Run, click this button and - follow the prompts on Cloud Shell. - - - - - - Deploy to Cloud Run - - - -
- - updateSettings({ rowyRunUrl: e.target.value })} - fullWidth - placeholder="https://.run.app" - type="url" - autoComplete="url" - /> - - ); -} diff --git a/src/components/Settings/ProjectSettings/RowyRun.tsx b/src/components/Settings/ProjectSettings/RowyRun.tsx new file mode 100644 index 00000000..3cec71c0 --- /dev/null +++ b/src/components/Settings/ProjectSettings/RowyRun.tsx @@ -0,0 +1,263 @@ +import { useState, useCallback, useEffect } from "react"; +import createPersistedState from "use-persisted-state"; +import { differenceInDays } from "date-fns"; + +import { + Typography, + Link, + Divider, + Button, + Grid, + TextField, +} from "@mui/material"; +import LoadingButton from "@mui/lab/LoadingButton"; +import InlineOpenInNewIcon from "components/InlineOpenInNewIcon"; + +import { IProjectSettingsChildProps } from "pages/Settings/ProjectSettings"; +import WIKI_LINKS from "constants/wikiLinks"; +import { name } from "@root/package.json"; +import { RunRoutes, runRepoUrl } from "constants/runRoutes"; + +const useLastCheckedUpdateState = createPersistedState( + "__ROWY__RUN_LAST_CHECKED_UPDATE" +); +export const useLatestUpdateState = createPersistedState( + "__ROWY__RUN_LATEST_UPDATE" +); + +export default function RowyRun({ + settings, + updateSettings, +}: IProjectSettingsChildProps) { + const [inputRowyRunUrl, setInputRowyRunUrl] = useState(settings.rowyRunUrl); + const [verified, setVerified] = useState(); + const handleVerify = async () => { + setVerified("LOADING"); + try { + const versionReq = await fetch(inputRowyRunUrl + RunRoutes.version.path, { + method: RunRoutes.version.method, + }).then((res) => res.json()); + + if (!versionReq.version) throw new Error("No version found"); + else { + setVerified(true); + setVersion(versionReq.version); + updateSettings({ rowyRunUrl: inputRowyRunUrl }); + } + } catch (e) { + console.error(e); + setVerified(false); + } + }; + + const [lastCheckedUpdate, setLastCheckedUpdate] = + useLastCheckedUpdateState(); + const [latestUpdate, setLatestUpdate] = useLatestUpdateState>(null); + + const [checkState, setCheckState] = useState( + null + ); + const [version, setVersion] = useState(""); + useEffect(() => { + fetch(settings.rowyRunUrl + RunRoutes.version.path, { + method: RunRoutes.version.method, + }) + .then((res) => res.json()) + .then((data) => setVersion(data.version)); + }, [settings.rowyRunUrl]); + + const checkForUpdate = useCallback(async () => { + setCheckState("LOADING"); + + // https://docs.github.com/en/rest/reference/repos#get-the-latest-release + const endpoint = + runRepoUrl.replace("github.com", "api.github.com/repos") + + "/releases/latest"; + try { + const versionReq = await fetch( + settings.rowyRunUrl + RunRoutes.version.path, + { method: RunRoutes.version.method } + ).then((res) => res.json()); + const version = versionReq.version; + setVersion(version); + + const req = await fetch(endpoint, { + headers: { + Accept: "application/vnd.github.v3+json", + }, + }); + const res = await req.json(); + + if (res.tag_name > "v" + version) { + setLatestUpdate(res); + setCheckState(null); + } else { + setCheckState("NO_UPDATE"); + } + + setLastCheckedUpdate(new Date().toISOString()); + } catch (e) { + console.error(e); + setLatestUpdate(null); + setCheckState("NO_UPDATE"); + } + }, [setLastCheckedUpdate, setLatestUpdate, settings.rowyRunUrl]); + + // Check for new updates on page load, if last check was more than 7 days ago + useEffect(() => { + if (!lastCheckedUpdate) checkForUpdate(); + else if (differenceInDays(new Date(), new Date(lastCheckedUpdate)) > 7) + checkForUpdate(); + }, [lastCheckedUpdate, checkForUpdate]); + + // Verify latest update is not installed yet + useEffect(() => { + if (version && latestUpdate?.tag_name <= "v" + version) + setLatestUpdate(null); + }, [latestUpdate, setLatestUpdate, version]); + + const deployButton = window.location.hostname.includes("rowy.app") ? ( + + One-Click Deploy + + ) : ( + + ); + + return ( + <> + + {name} Run is a Cloud Run instance that provides back-end functionality, + such as table action scripts, user management, and easy Cloud Function + deployment.{" "} + + Learn more + + + + + + + {settings.rowyRunUrl && ( +
+ + + + {name} Run v{version} + + {checkState === "LOADING" ? ( + + Checking for updates… + + ) : latestUpdate === null ? ( + + Up to date + + ) : ( + + Update available: {latestUpdate.tag_name} + + + )} + + + + {latestUpdate === null ? ( + + Check for Updates + + ) : ( + deployButton + )} + + +
+ )} + + {settings.rowyRunUrl && } + + {!settings.rowyRunUrl && ( +
+ + + + If you have not yet deployed {name} Run, click this button and + follow the prompts on Cloud Shell. + + + + {deployButton} + +
+ )} + +
+ + + setInputRowyRunUrl(e.target.value)} + fullWidth + placeholder="https://.run.app" + type="url" + autoComplete="url" + error={verified === false} + helperText={ + verified === true + ? `${name} Run is set up correctly` + : verified === false + ? `${name} Run is not set up correctly` + : " " + } + /> + + + + + Verify + + + +
+ + ); +} diff --git a/src/pages/Settings/ProjectSettings.tsx b/src/pages/Settings/ProjectSettings.tsx index 0ed2224b..a5c1047c 100644 --- a/src/pages/Settings/ProjectSettings.tsx +++ b/src/pages/Settings/ProjectSettings.tsx @@ -7,7 +7,7 @@ import { Container, Stack, Fade } from "@mui/material"; import SettingsSkeleton from "components/Settings/SettingsSkeleton"; import SettingsSection from "components/Settings/SettingsSection"; import About from "components/Settings/ProjectSettings/About"; -import CloudRun from "@src/components/Settings/ProjectSettings/CloudRun"; +import RowyRun from "@src/components/Settings/ProjectSettings/RowyRun"; import Authentication from "components/Settings/ProjectSettings/Authentication"; import Customization from "components/Settings/ProjectSettings/Customization"; @@ -68,7 +68,7 @@ export default function ProjectSettingsPage() { const sections = [ { title: "About", Component: About }, - { title: `${name} Run`, Component: CloudRun, props: childProps }, + { title: `${name} Run`, Component: RowyRun, props: childProps }, { title: "Authentication", Component: Authentication, props: childProps }, { title: "Customization", Component: Customization, props: childProps }, ];