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 },
];