mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
Setup: add step 2
This commit is contained in:
@@ -3,36 +3,55 @@ import { ISetupStepBodyProps } from "pages/Setup";
|
||||
import { FormControlLabel, Checkbox, Typography, Link } from "@mui/material";
|
||||
import OpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
|
||||
export default function Welcome({
|
||||
completion,
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
const { projectId } = useAppContext();
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={completion.welcome}
|
||||
onChange={(e) =>
|
||||
setCompletion((c) => ({ ...c, welcome: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<Typography sx={{ mt: 1.25, mb: 0.5 }}>
|
||||
I agree to the terms and conditions
|
||||
</Typography>
|
||||
<Link display="block" variant="body2" color="text.secondary">
|
||||
Read the simple English version
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
<Link display="block" variant="body2" color="text.secondary">
|
||||
Read the full terms and conditions
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ pr: 1, textAlign: "left", alignItems: "flex-start", p: 0, m: 0 }}
|
||||
/>
|
||||
<>
|
||||
<div>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Get up and running in around 5 minutes.
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
You’ll easily set up backend functionality, Firestore Rules, and user
|
||||
management.
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
You’ll set up the project: <b>{projectId}</b>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={completion.welcome}
|
||||
onChange={(e) =>
|
||||
setCompletion((c) => ({ ...c, welcome: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<Typography sx={{ mt: 1.25, mb: 0.5 }}>
|
||||
I agree to the terms and conditions
|
||||
</Typography>
|
||||
<Link display="block" variant="body2" color="text.secondary">
|
||||
Read the simple English version
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
<Link display="block" variant="body2" color="text.secondary">
|
||||
Read the full terms and conditions
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ pr: 1, textAlign: "left", alignItems: "flex-start", p: 0, m: 0 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { runRepoUrl, RunRoutes } from "constants/runRoutes";
|
||||
|
||||
export default function Step1RowyRun({
|
||||
@@ -34,7 +35,7 @@ export default function Step1RowyRun({
|
||||
setVerificationStatus("loading");
|
||||
|
||||
try {
|
||||
const result = await checkCompletionRowyRun(rowyRunUrl);
|
||||
const result = await checkRowyRun(rowyRunUrl);
|
||||
setVerificationStatus("pass");
|
||||
|
||||
if (result.isValidRowyRunUrl) setIsValidRowyRunUrl(true);
|
||||
@@ -81,6 +82,12 @@ export default function Step1RowyRun({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
{name} Run is a Google Cloud Run instance that provides back-end
|
||||
functionality, such as table action scripts, user management, and easy
|
||||
Cloud Function deployment.
|
||||
</Typography>
|
||||
|
||||
<SetupItem
|
||||
status={isValidRowyRunUrl ? "complete" : "incomplete"}
|
||||
title={
|
||||
@@ -131,7 +138,6 @@ export default function Step1RowyRun({
|
||||
</>
|
||||
)}
|
||||
</SetupItem>
|
||||
|
||||
{isValidRowyRunUrl && (
|
||||
<SetupItem
|
||||
status={isLatestVersion ? "complete" : "incomplete"}
|
||||
@@ -162,7 +168,7 @@ export default function Step1RowyRun({
|
||||
);
|
||||
}
|
||||
|
||||
export const checkCompletionRowyRun = async (rowyRunUrl: string) => {
|
||||
export const checkRowyRun = async (rowyRunUrl: string) => {
|
||||
const result = {
|
||||
isValidRowyRunUrl: false,
|
||||
isLatestVersion: false,
|
||||
|
||||
157
src/components/Setup/Step2ServiceAccount.tsx
Normal file
157
src/components/Setup/Step2ServiceAccount.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ISetupStepBodyProps } from "pages/Setup";
|
||||
|
||||
import { Typography, Link, Stack } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
import InfoIcon from "@mui/icons-material/InfoOutlined";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { RunRoutes } from "constants/runRoutes";
|
||||
|
||||
export default function Step2ServiceAccount({
|
||||
rowyRunUrl,
|
||||
completion,
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
const [hasAllRoles, setHasAllRoles] = useState(completion.serviceAccount);
|
||||
const [verificationStatus, setVerificationStatus] = useState<
|
||||
"idle" | "loading" | "pass" | "fail"
|
||||
>("idle");
|
||||
|
||||
const { projectId } = useAppContext();
|
||||
const [region, setRegion] = useState("");
|
||||
useEffect(() => {
|
||||
fetch(rowyRunUrl + RunRoutes.region.path, {
|
||||
method: RunRoutes.region.method,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => setRegion(data.region))
|
||||
.catch((e) => console.error(e));
|
||||
}, []);
|
||||
|
||||
const verifyRoles = async () => {
|
||||
setVerificationStatus("loading");
|
||||
try {
|
||||
const result = await checkServiceAccount(rowyRunUrl);
|
||||
if (result) {
|
||||
setVerificationStatus("pass");
|
||||
setHasAllRoles(true);
|
||||
setCompletion((c) => ({ ...c, serviceAccount: true }));
|
||||
} else {
|
||||
setVerificationStatus("fail");
|
||||
setHasAllRoles(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setVerificationStatus("fail");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
{name} Run uses the{" "}
|
||||
<Link
|
||||
href="https://firebase.google.com/docs/admin/setup"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Firebase Admin SDK
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="https://github.com/googleapis/google-cloud-node"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Google Cloud SDKs
|
||||
</Link>{" "}
|
||||
to make changes to your Firestore database, authenticated with a{" "}
|
||||
<Link
|
||||
href="https://firebase.google.com/support/guides/service-accounts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
service account
|
||||
</Link>
|
||||
. Rowy Run operates exclusively on your GCP project and we will never
|
||||
have access to your service account or any of your data.
|
||||
</Typography>
|
||||
|
||||
<SetupItem
|
||||
status={hasAllRoles ? "complete" : "incomplete"}
|
||||
title={
|
||||
hasAllRoles
|
||||
? "Rowy Run has access to a service account with all the required IAM roles:"
|
||||
: "Set up a service account with the following IAM roles:"
|
||||
}
|
||||
>
|
||||
<ul style={{ marginTop: 0 }}>
|
||||
<li>Service Account User – required to deploy Cloud Functions</li>
|
||||
<li>Firebase Authentication Admin</li>
|
||||
<li>Firestore Service Agent</li>
|
||||
</ul>
|
||||
|
||||
{!hasAllRoles && (
|
||||
<>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<LoadingButton
|
||||
loading={!region}
|
||||
href={`https://console.cloud.google.com/run/deploy/${region}/rowy-run?project=${projectId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
Set Up Service Account
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={verifyRoles}
|
||||
loading={verificationStatus === "loading"}
|
||||
>
|
||||
Verify
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" spacing={1}>
|
||||
<InfoIcon aria-label="Info" color="action" sx={{ mt: -0.25 }} />
|
||||
<Typography variant="body2">
|
||||
On the Google Cloud Console page, click the Security tab to set
|
||||
the service account for Rowy Run.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Link
|
||||
href="https://cloud.google.com/iam/docs/understanding-roles"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="body2"
|
||||
>
|
||||
Learn about IAM roles
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>
|
||||
</SetupItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -27,21 +27,20 @@ import Logo from "assets/Logo";
|
||||
import ScrollableDialogContent from "components/Modal/ScrollableDialogContent";
|
||||
import { SlideTransition } from "components/Modal/SlideTransition";
|
||||
|
||||
import Step0Welcome from "@src/components/Setup/Step0Welcome";
|
||||
import Step1RowyRun, {
|
||||
checkCompletionRowyRun,
|
||||
} from "@src/components/Setup/Step1RowyRun";
|
||||
import Step0Welcome from "components/Setup/Step0Welcome";
|
||||
import Step1RowyRun, { checkRowyRun } from "components/Setup/Step1RowyRun";
|
||||
// prettier-ignore
|
||||
import Step2ServiceAccount, { checkServiceAccount } from "components/Setup/Step2ServiceAccount";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { name } from "@root/package.json";
|
||||
import routes from "constants/routes";
|
||||
|
||||
export interface ISetupStep {
|
||||
id: string;
|
||||
layout?: "centered" | "step";
|
||||
layout?: "centered";
|
||||
shortTitle: string;
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
body: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
@@ -60,9 +59,12 @@ const checkAllSteps = async (
|
||||
console.log("Check all steps");
|
||||
const completion: Record<string, boolean> = {};
|
||||
|
||||
const checkRowyRun = await checkCompletionRowyRun(rowyRunUrl);
|
||||
if (checkRowyRun.isValidRowyRunUrl) {
|
||||
if (checkRowyRun.isLatestVersion) completion.rowyRun = true;
|
||||
const rowyRunValidation = await checkRowyRun(rowyRunUrl);
|
||||
if (rowyRunValidation.isValidRowyRunUrl) {
|
||||
if (rowyRunValidation.isLatestVersion) completion.rowyRun = true;
|
||||
|
||||
const serviceAccount = await checkServiceAccount(rowyRunUrl);
|
||||
if (serviceAccount) completion.serviceAccount = true;
|
||||
}
|
||||
|
||||
if (Object.keys(completion).length > 0)
|
||||
@@ -70,7 +72,6 @@ const checkAllSteps = async (
|
||||
};
|
||||
|
||||
export default function SetupPage() {
|
||||
const { projectId } = useAppContext();
|
||||
const fullScreenHeight = use100vh() ?? 0;
|
||||
const isMobile = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
|
||||
|
||||
@@ -85,7 +86,6 @@ export default function SetupPage() {
|
||||
serviceAccount: false,
|
||||
signIn: false,
|
||||
rules: false,
|
||||
migrate: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -97,23 +97,9 @@ export default function SetupPage() {
|
||||
const steps: ISetupStep[] = [
|
||||
{
|
||||
id: "welcome",
|
||||
layout: "centered",
|
||||
layout: "centered" as "centered",
|
||||
shortTitle: "Welcome",
|
||||
title: `Welcome to ${name}`,
|
||||
description: (
|
||||
<>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Get up and running in around 5 minutes.
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
You’ll easily set up backend functionality, Firestore Rules, and
|
||||
user management.
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
You’ll set up the project: <b>{projectId}</b>
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
body: <Step0Welcome {...stepProps} />,
|
||||
actions: completion.welcome ? (
|
||||
<Button variant="contained" color="primary" type="submit">
|
||||
@@ -133,15 +119,13 @@ export default function SetupPage() {
|
||||
id: "rowyRun",
|
||||
shortTitle: `${name} Run`,
|
||||
title: `Set Up ${name} Run`,
|
||||
description: `${name} Run is a Google Cloud Run instance that provides back-end functionality, such as table action scripts, user management, and easy Cloud Function deployment.`,
|
||||
body: <Step1RowyRun {...stepProps} />,
|
||||
},
|
||||
{
|
||||
id: "serviceAccount",
|
||||
shortTitle: `Service Account`,
|
||||
title: `Set Up Service Account`,
|
||||
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`,
|
||||
body: <Step2ServiceAccount {...stepProps} />,
|
||||
},
|
||||
{
|
||||
id: "signIn",
|
||||
@@ -157,20 +141,26 @@ export default function SetupPage() {
|
||||
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: "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`,
|
||||
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`,
|
||||
body: `x`,
|
||||
}
|
||||
: ({} as ISetupStep),
|
||||
{
|
||||
id: "finish",
|
||||
layout: "centered",
|
||||
layout: "centered" as "centered",
|
||||
shortTitle: `Finish`,
|
||||
title: `You’re all set up!`,
|
||||
description: `You can now create a table from your Firestore collections or continue to ${name}.`,
|
||||
body: <div>x</div>,
|
||||
body: (
|
||||
<div>
|
||||
You can now create a table from your Firestore collections or continue
|
||||
to {name}
|
||||
</div>
|
||||
),
|
||||
actions: (
|
||||
<>
|
||||
<Button variant="contained" color="primary">
|
||||
@@ -182,7 +172,7 @@ export default function SetupPage() {
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
].filter((x) => x.id);
|
||||
|
||||
const step =
|
||||
steps.find((step) => step.id === (stepId || steps[0].id)) ?? steps[0];
|
||||
@@ -212,7 +202,7 @@ export default function SetupPage() {
|
||||
alpha(theme.palette.background.paper, 0.5),
|
||||
backdropFilter: "blur(20px) saturate(150%)",
|
||||
|
||||
maxWidth: (theme) => theme.breakpoints.values.md,
|
||||
maxWidth: 840,
|
||||
width: "100%",
|
||||
maxHeight: (theme) =>
|
||||
`calc(${
|
||||
@@ -220,7 +210,7 @@ export default function SetupPage() {
|
||||
} - ${theme.spacing(
|
||||
2
|
||||
)} - env(safe-area-inset-top) - env(safe-area-inset-bottom))`,
|
||||
height: (theme) => theme.breakpoints.values.md * 0.75,
|
||||
height: 840 * 0.75,
|
||||
|
||||
p: 0,
|
||||
"& > *": { px: { xs: 2, sm: 4 } },
|
||||
@@ -312,19 +302,13 @@ export default function SetupPage() {
|
||||
|
||||
<SwitchTransition mode="out-in">
|
||||
<SlideTransition key={stepId} appear timeout={100}>
|
||||
<div>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{ mb: 1, typography: { xs: "h5", md: "h4" } }}
|
||||
>
|
||||
{step.title}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="inherit" component="div">
|
||||
{step.description}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{ mb: 1, typography: { xs: "h5", md: "h4" } }}
|
||||
>
|
||||
{step.title}
|
||||
</Typography>
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
|
||||
@@ -354,12 +338,7 @@ export default function SetupPage() {
|
||||
<SwitchTransition mode="out-in">
|
||||
<SlideTransition key={stepId} appear timeout={100}>
|
||||
<ScrollableDialogContent disableTopDivider>
|
||||
<Stack spacing={4}>
|
||||
<Typography variant="body1" style={{ maxWidth: "70ch" }}>
|
||||
{step.description}
|
||||
</Typography>
|
||||
{step.body}
|
||||
</Stack>
|
||||
<Stack spacing={4}>{step.body}</Stack>
|
||||
</ScrollableDialogContent>
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
|
||||
Reference in New Issue
Block a user