diff --git a/src/components/Settings/ProjectSettings/RowyRun.tsx b/src/components/Settings/ProjectSettings/RowyRun.tsx index 3cec71c0..806cd65f 100644 --- a/src/components/Settings/ProjectSettings/RowyRun.tsx +++ b/src/components/Settings/ProjectSettings/RowyRun.tsx @@ -16,7 +16,7 @@ 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"; +import { runRoutes, runRepoUrl } from "constants/runRoutes"; const useLastCheckedUpdateState = createPersistedState( "__ROWY__RUN_LAST_CHECKED_UPDATE" @@ -34,8 +34,8 @@ export default function RowyRun({ const handleVerify = async () => { setVerified("LOADING"); try { - const versionReq = await fetch(inputRowyRunUrl + RunRoutes.version.path, { - method: RunRoutes.version.method, + 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"); @@ -62,8 +62,8 @@ export default function RowyRun({ ); const [version, setVersion] = useState(""); useEffect(() => { - fetch(settings.rowyRunUrl + RunRoutes.version.path, { - method: RunRoutes.version.method, + fetch(settings.rowyRunUrl + runRoutes.version.path, { + method: runRoutes.version.method, }) .then((res) => res.json()) .then((data) => setVersion(data.version)); @@ -78,8 +78,8 @@ export default function RowyRun({ "/releases/latest"; try { const versionReq = await fetch( - settings.rowyRunUrl + RunRoutes.version.path, - { method: RunRoutes.version.method } + settings.rowyRunUrl + runRoutes.version.path, + { method: runRoutes.version.method } ).then((res) => res.json()); const version = versionReq.version; setVersion(version); diff --git a/src/components/Setup/SignInWithGoogle.tsx b/src/components/Setup/SignInWithGoogle.tsx new file mode 100644 index 00000000..67e3f7d4 --- /dev/null +++ b/src/components/Setup/SignInWithGoogle.tsx @@ -0,0 +1,71 @@ +import { useState } from "react"; + +import { Typography } from "@mui/material"; +import LoadingButton, { LoadingButtonProps } from "@mui/lab/LoadingButton"; + +import { auth, googleProvider } from "@src/firebase"; + +export interface ISignInWithGoogleProps extends Partial { + matchEmail?: string; +} + +export default function SignInWithGoogle({ + matchEmail, + ...props +}: ISignInWithGoogleProps) { + const [status, setStatus] = useState<"IDLE" | "LOADING" | string>("IDLE"); + + const handleSignIn = async () => { + setStatus("LOADING"); + try { + const result = await auth.signInWithPopup(googleProvider); + if (!result.user) throw new Error("Missing user"); + if ( + matchEmail && + matchEmail.toLowerCase() !== result.user.email?.toLowerCase() + ) + throw Error(`Account is not ${matchEmail}`); + + setStatus("IDLE"); + } catch (error: any) { + if (auth.currentUser) auth.signOut(); + console.log(error); + setStatus(error.message); + } + }; + + return ( +
+ + } + onClick={handleSignIn} + loading={status === "LOADING"} + {...props} + > + Sign in with Google + + + {status !== "LOADING" && status !== "IDLE" && ( + + {status} + + )} +
+ ); +} diff --git a/src/components/Setup/Step1RowyRun.tsx b/src/components/Setup/Step1RowyRun.tsx index 0cae4567..812f4cb8 100644 --- a/src/components/Setup/Step1RowyRun.tsx +++ b/src/components/Setup/Step1RowyRun.tsx @@ -10,7 +10,8 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import SetupItem from "./SetupItem"; import { name } from "@root/package.json"; -import { runRepoUrl, RunRoutes } from "constants/runRoutes"; +import { rowyRun } from "utils/rowyRun"; +import { runRepoUrl, runRoutes } from "constants/runRoutes"; export default function Step1RowyRun({ completion, @@ -28,15 +29,15 @@ export default function Step1RowyRun({ const [rowyRunUrl, setRowyRunUrl] = useState(paramsRowyRunUrl); const [latestVersion, setLatestVersion] = useState(""); const [verificationStatus, setVerificationStatus] = useState< - "idle" | "loading" | "pass" | "fail" - >("idle"); + "IDLE" | "LOADING" | "FAIL" + >("IDLE"); const verifyRowyRun = async () => { - setVerificationStatus("loading"); + setVerificationStatus("LOADING"); try { const result = await checkRowyRun(rowyRunUrl); - setVerificationStatus("pass"); + setVerificationStatus("IDLE"); if (result.isValidRowyRunUrl) setIsValidRowyRunUrl(true); @@ -51,8 +52,8 @@ export default function Step1RowyRun({ }); } } catch (e: any) { - console.error(`Failed to verify Rowy Run URL: ${e.message}`); - setVerificationStatus("fail"); + console.error(`Failed to verify Rowy Run URL: ${e}`); + setVerificationStatus("FAIL"); } }; @@ -120,15 +121,15 @@ export default function Step1RowyRun({ type="url" autoComplete="url" fullWidth - error={verificationStatus === "fail"} + error={verificationStatus === "FAIL"} helperText={ - verificationStatus === "fail" ? "Invalid URL" : " " + verificationStatus === "FAIL" ? "Invalid URL" : " " } /> Verify @@ -155,7 +156,7 @@ export default function Step1RowyRun({ Verify @@ -168,39 +169,43 @@ export default function Step1RowyRun({ ); } -export const checkRowyRun = async (rowyRunUrl: string) => { +export const checkRowyRun = async ( + rowyRunUrl: string, + signal?: AbortSignal +) => { const result = { isValidRowyRunUrl: false, isLatestVersion: false, latestVersion: "", }; - const req = await fetch(rowyRunUrl + RunRoutes.version.path, { - method: RunRoutes.version.method, - }); - if (!req.ok) return result; - const res = await req.json(); - if (!res.version) return result; + try { + const res = await rowyRun({ rowyRunUrl, route: runRoutes.version, signal }); + if (!res.version) return result; - result.isValidRowyRunUrl = true; + result.isValidRowyRunUrl = true; - // https://docs.github.com/en/rest/reference/repos#get-the-latest-release - const endpoint = - runRepoUrl.replace("github.com", "api.github.com/repos") + - "/releases/latest"; - const latestVersionReq = await fetch(endpoint, { - headers: { Accept: "application/vnd.github.v3+json" }, - }); - const latestVersion = await latestVersionReq.json(); - if (!latestVersion.tag_name) return result; + // https://docs.github.com/en/rest/reference/repos#get-the-latest-release + const endpoint = + runRepoUrl.replace("github.com", "api.github.com/repos") + + "/releases/latest"; + const latestVersionReq = await fetch(endpoint, { + headers: { Accept: "application/vnd.github.v3+json" }, + signal, + }); + const latestVersion = await latestVersionReq.json(); + if (!latestVersion.tag_name) return result; - if (latestVersion.tag_name > "v" + res.version) { - result.isLatestVersion = false; - result.latestVersion = latestVersion.tag_name; - } else { - result.isLatestVersion = true; - result.latestVersion = res.version; + if (latestVersion.tag_name > "v" + res.version) { + result.isLatestVersion = false; + result.latestVersion = latestVersion.tag_name; + } else { + result.isLatestVersion = true; + result.latestVersion = res.version; + } + } catch (e: any) { + console.error(e); + } finally { + return result; } - - return result; }; diff --git a/src/components/Setup/Step2ServiceAccount.tsx b/src/components/Setup/Step2ServiceAccount.tsx index b1709cb3..cc56f681 100644 --- a/src/components/Setup/Step2ServiceAccount.tsx +++ b/src/components/Setup/Step2ServiceAccount.tsx @@ -11,7 +11,8 @@ import SetupItem from "./SetupItem"; import { name } from "@root/package.json"; import { useAppContext } from "contexts/AppContext"; -import { RunRoutes } from "constants/runRoutes"; +import { rowyRun } from "utils/rowyRun"; +import { runRoutes } from "constants/runRoutes"; export default function Step2ServiceAccount({ rowyRunUrl, @@ -20,14 +21,14 @@ export default function Step2ServiceAccount({ }: ISetupStepBodyProps) { const [hasAllRoles, setHasAllRoles] = useState(completion.serviceAccount); const [verificationStatus, setVerificationStatus] = useState< - "idle" | "loading" | "pass" | "fail" - >("idle"); + "IDLE" | "LOADING" | "FAIL" + >("IDLE"); const { projectId } = useAppContext(); const [region, setRegion] = useState(""); useEffect(() => { - fetch(rowyRunUrl + RunRoutes.region.path, { - method: RunRoutes.region.method, + fetch(rowyRunUrl + runRoutes.region.path, { + method: runRoutes.region.method, }) .then((res) => res.json()) .then((data) => setRegion(data.region)) @@ -35,26 +36,26 @@ export default function Step2ServiceAccount({ }, []); const verifyRoles = async () => { - setVerificationStatus("loading"); + setVerificationStatus("LOADING"); try { const result = await checkServiceAccount(rowyRunUrl); if (result) { - setVerificationStatus("pass"); + setVerificationStatus("IDLE"); setHasAllRoles(true); setCompletion((c) => ({ ...c, serviceAccount: true })); } else { - setVerificationStatus("fail"); + setVerificationStatus("FAIL"); setHasAllRoles(false); } } catch (e) { console.error(e); - setVerificationStatus("fail"); + setVerificationStatus("FAIL"); } }; return ( <> - + {name} Run uses the{" "} service account - . Rowy Run operates exclusively on your GCP project and we will never - have access to your service account or any of your data. + . + + + Rowy Run operates exclusively on your GCP project and we will never have + access to your service account or any of your data. -
    +
    • Service Account User – required to deploy Cloud Functions
    • Firebase Authentication Admin
    • Firestore Service Agent
    • @@ -113,12 +117,18 @@ export default function Step2ServiceAccount({ variant="contained" color="primary" onClick={verifyRoles} - loading={verificationStatus === "loading"} + loading={verificationStatus === "LOADING"} > Verify + {verificationStatus === "FAIL" && ( + + The service account does not have the required IAM roles. + + )} + @@ -133,7 +143,6 @@ export default function Step2ServiceAccount({ href="https://cloud.google.com/iam/docs/understanding-roles" target="_blank" rel="noopener noreferrer" - variant="body2" > Learn about IAM roles @@ -143,15 +152,21 @@ export default function Step2ServiceAccount({ ); } -export const checkServiceAccount = async (rowyRunUrl: string) => { - const req = await fetch(rowyRunUrl + RunRoutes.serviceAccountAccess.path, { - method: RunRoutes.serviceAccountAccess.method, - }); - if (!req.ok) return false; - - const res = await req.json(); - return Object.values(res).reduce( - (acc, value) => acc && value, - true - ) as boolean; +export const checkServiceAccount = async ( + rowyRunUrl: string, + signal?: AbortSignal +) => { + try { + const res = await rowyRun({ + rowyRunUrl, + route: runRoutes.serviceAccountAccess, + }); + return Object.values(res).reduce( + (acc, value) => acc && value, + true + ) as boolean; + } catch (e: any) { + console.error(e); + return false; + } }; diff --git a/src/components/Setup/Step3ProjectOwner.tsx b/src/components/Setup/Step3ProjectOwner.tsx new file mode 100644 index 00000000..dbe26fd3 --- /dev/null +++ b/src/components/Setup/Step3ProjectOwner.tsx @@ -0,0 +1,177 @@ +import { useState, useEffect } from "react"; +import { ISetupStepBodyProps } from "pages/Setup"; + +import { Typography, Button } from "@mui/material"; +import LoadingButton from "@mui/lab/LoadingButton"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; + +import SetupItem from "./SetupItem"; +import SignInWithGoogle from "./SignInWithGoogle"; + +import { useAppContext } from "contexts/AppContext"; +import { rowyRun } from "utils/rowyRun"; +import { runRoutes } from "constants/runRoutes"; + +export default function Step3ProjectOwner({ + rowyRunUrl, + completion, + setCompletion, +}: ISetupStepBodyProps) { + const { projectId, currentUser, authToken } = useAppContext(); + + const [email, setEmail] = useState(""); + useEffect(() => { + rowyRun({ rowyRunUrl, route: runRoutes.projectOwner }) + .then((data) => setEmail(data.email)) + .catch((e: any) => { + console.error(e); + alert(`Failed to get project owner email: ${e.message}`); + }); + }, [rowyRunUrl]); + + const isSignedIn = currentUser?.email === email; + const [hasRoles, setHasRoles] = useState( + completion.projectOwner + ); + + const setRoles = async () => { + setHasRoles("LOADING"); + try { + const res = await rowyRun({ + route: runRoutes.setOwnerRoles, + rowyRunUrl, + authToken, + }); + + if (!res.success) + throw new Error(`${res.message}. Project owner: ${res.ownerEmail}`); + + setHasRoles(true); + setCompletion((c) => ({ ...c, projectOwner: true })); + } catch (e: any) { + console.error(e); + setHasRoles(e.message); + } + }; + + return ( + <> + + The project owner requires the admin and owner roles to have full access + to manage this project. The default project owner is the Google Cloud + account used to deploy Rowy Run:{" "} + {email} + + + + {!isSignedIn && ( + <> +
        +
      1. the Google auth provider enabled and
      2. +
      3. + this domain authorized:{" "} + {window.location.hostname} +
      4. +
      + + + + )} +
      + + + Sign in as the project owner: {email} + + ) + } + > + {!isSignedIn && ( + + )} + + + {isSignedIn && ( + + {hasRoles !== true && ( +
      + + Assign Roles + + + {typeof hasRoles === "string" && hasRoles !== "LOADING" && ( + + {hasRoles} + + )} +
      + )} +
      + )} + + ); +} + +export const checkProjectOwner = async ( + rowyRunUrl: string, + currentUser: firebase.default.User | null | undefined, + userRoles: string[] | null, + signal?: AbortSignal +) => { + if (!currentUser || !Array.isArray(userRoles)) return false; + + try { + const res = await rowyRun({ + rowyRunUrl, + route: runRoutes.projectOwner, + signal, + }); + const email = res.email; + if (currentUser.email !== email) return false; + return userRoles.includes("ADMIN") && userRoles.includes("OWNER"); + } catch (e: any) { + console.error(e); + return false; + } +}; diff --git a/src/components/Table/ColumnMenu/FieldSettings/index.tsx b/src/components/Table/ColumnMenu/FieldSettings/index.tsx index 6f369a16..b699facc 100644 --- a/src/components/Table/ColumnMenu/FieldSettings/index.tsx +++ b/src/components/Table/ColumnMenu/FieldSettings/index.tsx @@ -23,7 +23,7 @@ import Button from "@mui/material/Button"; import routes from "constants/routes"; import { SETTINGS } from "config/dbPaths"; import { name as appName } from "@root/package.json"; -import { RunRoutes } from "@src/constants/runRoutes"; +import { runRoutes } from "@src/constants/runRoutes"; export default function FieldSettings(props: IMenuModalProps) { const { name, fieldName, type, open, config, handleClose, handleSave } = @@ -126,7 +126,7 @@ export default function FieldSettings(props: IMenuModalProps) { handleConfirm: async () => { if (!rowyRun) return; rowyRun({ - route: RunRoutes.buildFunction, + route: runRoutes.buildFunction, body: { tablePath: tableState?.tablePath, pathname: window.location.pathname, diff --git a/src/components/Table/TableHeader/Extensions/index.tsx b/src/components/Table/TableHeader/Extensions/index.tsx index 080ea227..a6b5eb5f 100644 --- a/src/components/Table/TableHeader/Extensions/index.tsx +++ b/src/components/Table/TableHeader/Extensions/index.tsx @@ -19,7 +19,7 @@ import { useSnackLogContext } from "contexts/SnackLogContext"; import { emptyExtensionObject, IExtension, IExtensionType } from "./utils"; import { name } from "@root/package.json"; -import { RunRoutes } from "@src/constants/runRoutes"; +import { runRoutes } from "@src/constants/runRoutes"; import { analytics } from "@src/analytics"; export default function ExtensionsEditor() { @@ -88,7 +88,7 @@ export default function ExtensionsEditor() { snackLogContext.requestSnackLog(); if (rowyRun) rowyRun({ - route: RunRoutes.buildFunction, + route: runRoutes.buildFunction, body: { tablePath: tableState?.tablePath, pathname: window.location.pathname, diff --git a/src/components/fields/Action/ActionFab.tsx b/src/components/fields/Action/ActionFab.tsx index 0ada75fc..b1ef065e 100644 --- a/src/components/fields/Action/ActionFab.tsx +++ b/src/components/fields/Action/ActionFab.tsx @@ -12,7 +12,7 @@ import { cloudFunction } from "firebase/callables"; import { formatPath } from "utils/fns"; import { useConfirmation } from "components/ConfirmationDialog"; import { useActionParams } from "./FormDialog/Context"; -import { RunRoutes } from "@src/constants/runRoutes"; +import { runRoutes } from "@src/constants/runRoutes"; const replacer = (data: any) => (m: string, key: string) => { const objKey = key.split(":")[0]; @@ -78,7 +78,7 @@ export default function ActionFab({ }; const resp = await rowyRun({ - route: RunRoutes.actionScript, + route: runRoutes.actionScript, body: data, params: [], }); diff --git a/src/constants/runRoutes.ts b/src/constants/runRoutes.ts index 50d8f4c7..0cfb154f 100644 --- a/src/constants/runRoutes.ts +++ b/src/constants/runRoutes.ts @@ -29,9 +29,9 @@ type actionScriptRequest = { body: ActionData; }; -type RunRoutes = actionScriptRequest | impersonateUserRequest; +export type runRouteRequest = actionScriptRequest | impersonateUserRequest; -export const RunRoutes: { [key: string]: RunRoute } = { +export const runRoutes: Record = { impersonateUser: { path: "/impersonateUser", method: "GET" }, version: { path: "/version", method: "GET" }, region: { path: "/region", method: "GET" }, diff --git a/src/pages/Auth/ImpersonatorAuth.tsx b/src/pages/Auth/ImpersonatorAuth.tsx index 554cc2c3..b2ad6dcf 100644 --- a/src/pages/Auth/ImpersonatorAuth.tsx +++ b/src/pages/Auth/ImpersonatorAuth.tsx @@ -9,7 +9,7 @@ import FirebaseUi from "components/Auth/FirebaseUi"; import { signOut } from "utils/auth"; import { auth } from "../../firebase"; import { useProjectContext } from "@src/contexts/ProjectContext"; -import { RunRoutes } from "@src/constants/runRoutes"; +import { runRoutes } from "@src/constants/runRoutes"; import { name } from "@root/package.json"; export default function ImpersonatorAuthPage() { @@ -32,7 +32,7 @@ export default function ImpersonatorAuthPage() { console.log("rowyRun"); setLoading(true); const resp = await rowyRun({ - route: RunRoutes.impersonateUser, + route: runRoutes.impersonateUser, params: [email], }); console.log(resp); diff --git a/src/pages/RowyRunTest.tsx b/src/pages/RowyRunTest.tsx index a69b7c94..6ae61bc3 100644 --- a/src/pages/RowyRunTest.tsx +++ b/src/pages/RowyRunTest.tsx @@ -18,7 +18,7 @@ import { } from "@mui/material"; import { useConfirmation } from "components/ConfirmationDialog"; import { useProjectContext } from "@src/contexts/ProjectContext"; -import { RunRoutes } from "@src/constants/runRoutes"; +import { runRoutes } from "@src/constants/runRoutes"; const useBodyCacheState = createPersistedState("__ROWY__RR_TEST_REQ_BODY"); export default function TestView() { @@ -37,7 +37,7 @@ export default function TestView() { const handleMethodChange = (_, newMethod) => setMethod(newMethod); const setDefinedRoute = (newPath) => { setPath(newPath.target.value); - const _method = Object.values(RunRoutes).find( + const _method = Object.values(runRoutes).find( (r) => r.path === path )?.method; if (_method) { @@ -88,12 +88,12 @@ export default function TestView() { label="Defined Route" select value={ - Object.values(RunRoutes).find((r) => r.path === path)?.path ?? "" + Object.values(runRoutes).find((r) => r.path === path)?.path ?? "" } onChange={setDefinedRoute} style={{ width: 255 }} > - {Object.values(RunRoutes).map((route) => ( + {Object.values(runRoutes).map((route) => ( {route.path} diff --git a/src/pages/Setup.tsx b/src/pages/Setup.tsx index 823bd1fd..a9bdddcf 100644 --- a/src/pages/Setup.tsx +++ b/src/pages/Setup.tsx @@ -19,6 +19,7 @@ import { Tooltip, } from "@mui/material"; import { alpha } from "@mui/material/styles"; +import LoadingButton from "@mui/lab/LoadingButton"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; @@ -31,9 +32,12 @@ import Step0Welcome from "components/Setup/Step0Welcome"; import Step1RowyRun, { checkRowyRun } from "components/Setup/Step1RowyRun"; // prettier-ignore import Step2ServiceAccount, { checkServiceAccount } from "components/Setup/Step2ServiceAccount"; +// prettier-ignore +import Step3ProjectOwner, { checkProjectOwner } from "@src/components/Setup/Step3ProjectOwner"; import { name } from "@root/package.json"; import routes from "constants/routes"; +import { useAppContext } from "contexts/AppContext"; export interface ISetupStep { id: string; @@ -54,24 +58,35 @@ export interface ISetupStepBodyProps { const checkAllSteps = async ( rowyRunUrl: string, - setCompletion: React.Dispatch>> + currentUser: firebase.default.User | null | undefined, + userRoles: string[] | null, + signal: AbortSignal ) => { console.log("Check all steps"); const completion: Record = {}; - const rowyRunValidation = await checkRowyRun(rowyRunUrl); + const rowyRunValidation = await checkRowyRun(rowyRunUrl, signal); if (rowyRunValidation.isValidRowyRunUrl) { if (rowyRunValidation.isLatestVersion) completion.rowyRun = true; - const serviceAccount = await checkServiceAccount(rowyRunUrl); + const serviceAccount = await checkServiceAccount(rowyRunUrl, signal); if (serviceAccount) completion.serviceAccount = true; + + const projectOwner = await checkProjectOwner( + rowyRunUrl, + currentUser, + userRoles, + signal + ); + if (projectOwner) completion.projectOwner = true; } - if (Object.keys(completion).length > 0) - setCompletion((c) => ({ ...c, ...completion })); + return completion; }; export default function SetupPage() { + const { currentUser, userRoles } = useAppContext(); + const fullScreenHeight = use100vh() ?? 0; const isMobile = useMediaQuery((theme: any) => theme.breakpoints.down("sm")); @@ -84,13 +99,27 @@ export default function SetupPage() { welcome: false, rowyRun: false, serviceAccount: false, - signIn: false, + projectOwner: false, rules: false, }); + const [checkingAllSteps, setCheckingAllSteps] = useState(false); useEffect(() => { - if (rowyRunUrl) checkAllSteps(rowyRunUrl, setCompletion); - }, [rowyRunUrl]); + const controller = new AbortController(); + const signal = controller.signal; + + if (rowyRunUrl) { + setCheckingAllSteps(true); + checkAllSteps(rowyRunUrl, currentUser, userRoles, signal).then( + (result) => { + if (!signal.aborted) setCompletion((c) => ({ ...c, ...result })); + setCheckingAllSteps(false); + } + ); + } + + return () => controller.abort(); + }, [rowyRunUrl, currentUser, userRoles]); const stepProps = { completion, setCompletion, checkAllSteps, rowyRunUrl }; @@ -102,15 +131,25 @@ export default function SetupPage() { title: `Welcome to ${name}`, body: , actions: completion.welcome ? ( - + ) : (
      - +
      ), @@ -128,25 +167,22 @@ export default function SetupPage() { body: , }, { - id: "signIn", - shortTitle: `Sign In`, - title: `Sign In as the Project Owner`, - description: `${name} Run is a Google Cloud Run instance that provides back-end functionality, such as table action scripts, user management, and easy Cloud Functions deployment. Learn more`, - body: `x`, + id: "projectOwner", + shortTitle: `Project Owner`, + title: `Set Up Project Owner`, + body: , }, { id: "rules", shortTitle: `Rules`, title: `Set Up Firestore Rules`, - description: `${name} Run is a Google Cloud Run instance that provides back-end functionality, such as table action scripts, user management, and easy Cloud Functions deployment. Learn more`, body: `x`, }, completion.migrate !== undefined ? { id: "migrate", shortTitle: `Migrate`, - title: `Migrate to ${name}`, - description: `${name} Run is a Google Cloud Run instance that provides back-end functionality, such as table action scripts, user management, and easy Cloud Functions deployment. Learn more`, + title: `Migrate to ${name} (optional)`, body: `x`, } : ({} as ISetupStep), @@ -358,14 +394,15 @@ export default function SetupPage() { > {step.actions ?? ( - + )} diff --git a/src/utils/rowyRun.ts b/src/utils/rowyRun.ts index 97323e32..028133f2 100644 --- a/src/utils/rowyRun.ts +++ b/src/utils/rowyRun.ts @@ -8,6 +8,7 @@ export interface IRowyRunRequestProps { params?: string[]; localhost?: boolean; json?: boolean; + signal?: AbortSignal; } export const rowyRun = async ({ @@ -18,6 +19,7 @@ export const rowyRun = async ({ params, localhost = false, json = true, + signal, }: IRowyRunRequestProps) => { const { method, path } = route; let url = (localhost ? "http://localhost:8080" : rowyRunUrl) + path; @@ -35,6 +37,7 @@ export const rowyRun = async ({ redirect: "follow", referrerPolicy: "no-referrer", body: body && method !== "GET" ? JSON.stringify(body) : null, // body data type must match "Content-Type" header + signal, }); if (json) return await response.json();