Setup: add step 2

This commit is contained in:
Sidney Alcantara
2021-09-15 22:07:40 +10:00
parent 877ffdb7a4
commit ae12a067df
4 changed files with 252 additions and 91 deletions

View File

@@ -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>
Youll easily set up backend functionality, Firestore Rules, and user
management.
</Typography>
<Typography variant="body1">
Youll 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 }}
/>
</>
);
}

View File

@@ -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,

View 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;
};

View File

@@ -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>
Youll easily set up backend functionality, Firestore Rules, and
user management.
</Typography>
<Typography variant="body1">
Youll 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: `Youre 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>