mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
restructure SetupPage to allow for multiple flows
This commit is contained in:
@@ -52,7 +52,7 @@ const UserSettingsPage = lazy(() => import("./pages/Settings/UserSettings" /* we
|
||||
// prettier-ignore
|
||||
const UserManagementPage = lazy(() => import("./pages/Settings/UserManagement" /* webpackChunkName: "UserManagementPage" */));
|
||||
// prettier-ignore
|
||||
const SetupPage = lazy(() => import("@src/pages/Setup" /* webpackChunkName: "SetupPage" */));
|
||||
const BasicSetupPage = lazy(() => import("@src/pages/Setup/BasicSetup" /* webpackChunkName: "BasicSetupPage" */));
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -97,7 +97,7 @@ export default function App() {
|
||||
<Route
|
||||
exact
|
||||
path={routes.setup}
|
||||
render={() => <SetupPage />}
|
||||
render={() => <BasicSetupPage />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
|
||||
@@ -35,8 +35,8 @@ export default function CodeEditorHelper({
|
||||
description: `firebase Storage can be accessed through this, storage.bucket() returns default storage bucket of the firebase project.`,
|
||||
},
|
||||
{
|
||||
key: "utilFns",
|
||||
description: `utilFns provides a set of functions that are commonly used, such as easy access to GCP Secret Manager`,
|
||||
key: "RULES_UTILS",
|
||||
description: `RULES_UTILS provides a set of functions that are commonly used, such as easy access to GCP Secret Manager`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
2
src/components/CodeEditor/extensions.d.ts
vendored
2
src/components/CodeEditor/extensions.d.ts
vendored
@@ -24,7 +24,7 @@ type ExtensionContext = {
|
||||
requiredFields: string[];
|
||||
extensionBody: any;
|
||||
};
|
||||
utilFns: any;
|
||||
RULES_UTILS: any;
|
||||
};
|
||||
|
||||
// extension body definition
|
||||
|
||||
2
src/components/CodeEditor/utils.d.ts
vendored
2
src/components/CodeEditor/utils.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* utility functions
|
||||
*/
|
||||
declare namespace utilFns {
|
||||
declare namespace RULES_UTILS {
|
||||
/**
|
||||
* Sends out an email through sendGrid
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { ISetupStep } from "../types";
|
||||
|
||||
import { Typography, Stack, RadioGroup, Radio } from "@mui/material";
|
||||
import { Typography, Stack, RadioGroup, Radio, Button } from "@mui/material";
|
||||
import ThumbUpIcon from "@mui/icons-material/ThumbUpAlt";
|
||||
import ThumbUpOffIcon from "@mui/icons-material/ThumbUpOffAlt";
|
||||
import ThumbDownIcon from "@mui/icons-material/ThumbDownAlt";
|
||||
@@ -9,8 +11,17 @@ import ThumbDownOffIcon from "@mui/icons-material/ThumbDownOffAlt";
|
||||
|
||||
import { analytics } from "analytics";
|
||||
import { db } from "@src/firebase";
|
||||
import { routes } from "@src/constants/routes";
|
||||
|
||||
export default function Step6Finish() {
|
||||
export default {
|
||||
id: "finish",
|
||||
layout: "centered",
|
||||
shortTitle: "Finish",
|
||||
title: "You’re all set up!",
|
||||
body: StepFinish,
|
||||
} as ISetupStep;
|
||||
|
||||
function StepFinish() {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,6 +77,16 @@ export default function Step6Finish() {
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
component={Link}
|
||||
to={routes.auth}
|
||||
>
|
||||
Sign in to your Rowy project
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
140
src/components/Setup/BasicSetup/StepRules.tsx
Normal file
140
src/components/Setup/BasicSetup/StepRules.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useState } from "react";
|
||||
import { useSnackbar } from "notistack";
|
||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Button,
|
||||
Grid,
|
||||
} from "@mui/material";
|
||||
import CopyIcon from "@src/assets/icons/Copy";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
|
||||
import SetupItem from "../SetupItem";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { CONFIG } from "@src/config/dbPaths";
|
||||
import {
|
||||
RULES_START,
|
||||
RULES_END,
|
||||
REQUIRED_RULES,
|
||||
ADMIN_RULES,
|
||||
RULES_UTILS,
|
||||
} from "@src/config/firestoreRules";
|
||||
|
||||
export default {
|
||||
id: "rules",
|
||||
shortTitle: "Firestore Rules",
|
||||
title: "Set up Firestore Rules",
|
||||
body: StepRules,
|
||||
} as ISetupStep;
|
||||
|
||||
function StepRules({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
const { projectId } = useAppContext();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [adminRule, setAdminRule] = useState(true);
|
||||
|
||||
const rules =
|
||||
RULES_START +
|
||||
(adminRule ? ADMIN_RULES : "") +
|
||||
REQUIRED_RULES +
|
||||
RULES_UTILS +
|
||||
RULES_END;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
Rowy configuration is stored in the <code>{CONFIG}</code> collection on
|
||||
Firestore. Your users will need read access to this collection and
|
||||
admins will need write access.
|
||||
</Typography>
|
||||
|
||||
<SetupItem
|
||||
status="incomplete"
|
||||
title="Add the following rules to enable access to Rowy configuration:"
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={adminRule}
|
||||
onChange={(e) => setAdminRule(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Allow admins to read and write all documents"
|
||||
sx={{ "&&": { ml: -11 / 8, mb: -11 / 8 }, width: "100%" }}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
component="pre"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 400,
|
||||
resize: "both",
|
||||
overflow: "auto",
|
||||
|
||||
"& .comment": { color: "info.dark" },
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: rules.replace(
|
||||
/(\/\/.*$)/gm,
|
||||
`<span class="comment">$1</span>`
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<CopyIcon />}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(rules);
|
||||
enqueueSnackbar("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href={`https://console.firebase.google.com/project/${
|
||||
projectId || "_"
|
||||
}/firestore/rules`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Set up in Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</SetupItem>
|
||||
|
||||
<SetupItem
|
||||
title={
|
||||
isComplete ? (
|
||||
"Marked as done"
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<DoneIcon />}
|
||||
onClick={() => setComplete()}
|
||||
>
|
||||
Mark as done
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
status={isComplete ? "complete" : "incomplete"}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
112
src/components/Setup/BasicSetup/StepStorageRules.tsx
Normal file
112
src/components/Setup/BasicSetup/StepStorageRules.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useSnackbar } from "notistack";
|
||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
|
||||
import { Typography, Button, Grid } from "@mui/material";
|
||||
import CopyIcon from "@src/assets/icons/Copy";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
|
||||
import SetupItem from "../SetupItem";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import {
|
||||
RULES_START,
|
||||
RULES_END,
|
||||
REQUIRED_RULES,
|
||||
} from "@src/config/storageRules";
|
||||
|
||||
export default {
|
||||
id: "storageRules",
|
||||
shortTitle: "Storage Rules",
|
||||
title: "Set up Firebase Storage Rules",
|
||||
body: StepStorageRules,
|
||||
} as ISetupStep;
|
||||
|
||||
const rules = RULES_START + REQUIRED_RULES + RULES_END;
|
||||
|
||||
function StepStorageRules({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
const { projectId } = useAppContext();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
Image and File fields store files in Firebase Storage. Your users will
|
||||
need read and write access.
|
||||
</Typography>
|
||||
|
||||
<SetupItem
|
||||
status="incomplete"
|
||||
title="Add the following rules to allow users to access Firebase Storage:"
|
||||
>
|
||||
<Typography
|
||||
component="pre"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 250,
|
||||
resize: "both",
|
||||
overflow: "auto",
|
||||
|
||||
"& .comment": { color: "info.dark" },
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: rules.replace(
|
||||
/(\/\/.*$)/gm,
|
||||
`<span class="comment">$1</span>`
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<CopyIcon />}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(rules);
|
||||
enqueueSnackbar("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href={`https://console.firebase.google.com/project/${
|
||||
projectId || "_"
|
||||
}/firestore/rules`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Set up in Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</SetupItem>
|
||||
|
||||
<SetupItem
|
||||
title={
|
||||
isComplete ? (
|
||||
"Marked as done"
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<DoneIcon />}
|
||||
onClick={() => setComplete()}
|
||||
sx={{ mt: -0.5 }}
|
||||
>
|
||||
Mark as done
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
status={isComplete ? "complete" : "incomplete"}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
91
src/components/Setup/BasicSetup/StepWelcome.tsx
Normal file
91
src/components/Setup/BasicSetup/StepWelcome.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
|
||||
import {
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Typography,
|
||||
Link,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
|
||||
export default {
|
||||
id: "welcome",
|
||||
layout: "centered",
|
||||
shortTitle: "Welcome",
|
||||
title: "Welcome",
|
||||
body: StepWelcome,
|
||||
} as ISetupStep;
|
||||
|
||||
function StepWelcome({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
const { projectId } = useAppContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Typography variant="body1" paragraph>
|
||||
Get started with Rowy in just a few minutes.
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
We have no access to your data and it always stays on your Firebase
|
||||
project.
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
Project: <code>{projectId}</code>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={isComplete}
|
||||
onChange={(e) => setComplete(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
I agree to the{" "}
|
||||
<Link
|
||||
href={EXTERNAL_LINKS.terms}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="body2"
|
||||
color="text.primary"
|
||||
>
|
||||
Terms and Conditions
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href={EXTERNAL_LINKS.privacy}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="body2"
|
||||
color="text.primary"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{
|
||||
pr: 1,
|
||||
textAlign: "left",
|
||||
alignItems: "flex-start",
|
||||
p: 0,
|
||||
m: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
disabled={!isComplete}
|
||||
type="submit"
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
|
||||
import { Typography, Link, Button } from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
import SetupItem from "../SetupItem";
|
||||
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function Step1Oauth({
|
||||
completion,
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
export default {
|
||||
id: "oauth",
|
||||
shortTitle: "Access",
|
||||
title: "Allow Firebase access",
|
||||
body: StepOauth,
|
||||
} as ISetupStep;
|
||||
|
||||
function StepOauth({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
@@ -44,7 +48,7 @@ export default function Step1Oauth({
|
||||
height="20"
|
||||
/>
|
||||
}
|
||||
onClick={() => setCompletion((c) => ({ ...c, oauth: true }))}
|
||||
onClick={() => setComplete()}
|
||||
>
|
||||
Sign in with Google
|
||||
</Button>
|
||||
80
src/components/Setup/RowyAppSetup/StepProject.tsx
Normal file
80
src/components/Setup/RowyAppSetup/StepProject.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
|
||||
import {
|
||||
useMediaQuery,
|
||||
Typography,
|
||||
Stack,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Divider,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "../SetupItem";
|
||||
|
||||
export default {
|
||||
id: "project",
|
||||
shortTitle: "Project",
|
||||
title: "Select project",
|
||||
body: StepProject,
|
||||
} as ISetupStep;
|
||||
|
||||
function StepProject({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
const isMobile = useMediaQuery((theme: any) => theme.breakpoints.down("md"));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
Select which Firebase project to set up Rowy on.
|
||||
</Typography>
|
||||
|
||||
<SetupItem
|
||||
title="Select an existing project or create a new one."
|
||||
status="incomplete"
|
||||
>
|
||||
<Stack
|
||||
spacing={2}
|
||||
direction={isMobile ? "column" : "row"}
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
>
|
||||
<TextField
|
||||
label="Project"
|
||||
select
|
||||
fullWidth
|
||||
style={isMobile ? undefined : { minWidth: 300 }}
|
||||
helperText={isMobile ? undefined : " "}
|
||||
SelectProps={{
|
||||
displayEmpty: true,
|
||||
renderValue: (v: any) =>
|
||||
v || (
|
||||
<Typography color="text.disabled">
|
||||
Select a project…
|
||||
</Typography>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<MenuItem value="lorem">lorem</MenuItem>
|
||||
<MenuItem value="ipsum">ipsum</MenuItem>
|
||||
<MenuItem value="dolor">dolor</MenuItem>
|
||||
<MenuItem value="sit">sit</MenuItem>
|
||||
<MenuItem value="amet">amet</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<Divider orientation={isMobile ? "horizontal" : "vertical"} flexItem>
|
||||
OR
|
||||
</Divider>
|
||||
|
||||
<Button
|
||||
onClick={() => setComplete()}
|
||||
style={isMobile ? undefined : { minWidth: 300 }}
|
||||
>
|
||||
Create project in Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
</Stack>
|
||||
</SetupItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
@@ -14,24 +14,32 @@ import InfoIcon from "@mui/icons-material/InfoOutlined";
|
||||
import CopyIcon from "@src/assets/icons/Copy";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
import SetupItem from "../SetupItem";
|
||||
import DiffEditor from "@src/components/CodeEditor/DiffEditor";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { CONFIG } from "@src/config/dbPaths";
|
||||
import {
|
||||
requiredRules,
|
||||
adminRules,
|
||||
utilFns,
|
||||
insecureRule,
|
||||
RULES_START,
|
||||
RULES_END,
|
||||
REQUIRED_RULES,
|
||||
ADMIN_RULES,
|
||||
RULES_UTILS,
|
||||
INSECURE_RULES,
|
||||
} from "@src/config/firestoreRules";
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
// import { useConfirmation } from "@src/components/ConfirmationDialog";
|
||||
|
||||
export default {
|
||||
id: "rules",
|
||||
shortTitle: "Firestore Rules",
|
||||
title: "Set up Firestore Rules",
|
||||
body: StepRules,
|
||||
} as ISetupStep;
|
||||
|
||||
const insecureRuleRegExp = new RegExp(
|
||||
insecureRule
|
||||
.replace(/\//g, "\\/")
|
||||
INSECURE_RULES.replace(/\//g, "\\/")
|
||||
.replace(/\*/g, "\\*")
|
||||
.replace(/\s{2,}/g, "\\s+")
|
||||
.replace(/\s/g, "\\s*")
|
||||
@@ -39,25 +47,23 @@ const insecureRuleRegExp = new RegExp(
|
||||
.replace(/;/g, ";?")
|
||||
);
|
||||
|
||||
export default function Step3Rules({
|
||||
function StepRules({
|
||||
rowyRunUrl,
|
||||
completion,
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
isComplete,
|
||||
setComplete,
|
||||
}: ISetupStepBodyProps & { rowyRunUrl: string }) {
|
||||
const { projectId, getAuthToken } = useAppContext();
|
||||
// const { requestConfirmation } = useConfirmation();
|
||||
|
||||
const [error, setError] = useState<string | false>(false);
|
||||
const [hasRules, setHasRules] = useState(completion.rules);
|
||||
const [hasRules, setHasRules] = useState(isComplete);
|
||||
const [adminRule, setAdminRule] = useState(true);
|
||||
const [showManualMode, setShowManualMode] = useState(false);
|
||||
|
||||
const rules = `${
|
||||
error === "security-rules/not-found"
|
||||
? `rules_version = '2';\n\nservice cloud.firestore {\n match /databases/{database}/documents {\n`
|
||||
: ""
|
||||
}${adminRule ? adminRules : ""}${requiredRules}${utilFns}${
|
||||
error === "security-rules/not-found" ? " }\n}" : ""
|
||||
const rules = `${error === "security-rules/not-found" ? RULES_START : ""}${
|
||||
adminRule ? ADMIN_RULES : ""
|
||||
}${REQUIRED_RULES}${RULES_UTILS}${
|
||||
error === "security-rules/not-found" ? RULES_END : ""
|
||||
}`.replace("\n", "");
|
||||
|
||||
const [currentRules, setCurrentRules] = useState("");
|
||||
@@ -120,7 +126,7 @@ export default function Step3Rules({
|
||||
if (!res.success) throw new Error(res.message);
|
||||
const isSuccessful = await checkRules(rowyRunUrl, authToken);
|
||||
if (isSuccessful) {
|
||||
setCompletion((c) => ({ ...c, rules: true }));
|
||||
setComplete();
|
||||
setHasRules(true);
|
||||
}
|
||||
setRulesStatus("");
|
||||
@@ -137,7 +143,7 @@ export default function Step3Rules({
|
||||
|
||||
const isSuccessful = await checkRules(rowyRunUrl, authToken);
|
||||
if (isSuccessful) {
|
||||
setCompletion((c) => ({ ...c, rules: true }));
|
||||
setComplete();
|
||||
setHasRules(true);
|
||||
}
|
||||
setRulesStatus("");
|
||||
@@ -154,7 +160,7 @@ export default function Step3Rules({
|
||||
// confirm: "Skip",
|
||||
// cancel: "cancel",
|
||||
// handleConfirm: async () => {
|
||||
// setCompletion((c) => ({ ...c, rules: true }));
|
||||
// setComplete();
|
||||
// setHasRules(true);
|
||||
// },
|
||||
// });
|
||||
@@ -215,6 +221,7 @@ export default function Step3Rules({
|
||||
color="primary"
|
||||
onClick={setRules}
|
||||
loading={rulesStatus === "LOADING"}
|
||||
style={{ position: "sticky", bottom: 8 }}
|
||||
>
|
||||
Set Firestore Rules
|
||||
</LoadingButton>
|
||||
@@ -236,75 +243,92 @@ export default function Step3Rules({
|
||||
)}
|
||||
|
||||
{!hasRules && showManualMode && (
|
||||
<SetupItem
|
||||
status="incomplete"
|
||||
title={
|
||||
error === "security-rules/not-found"
|
||||
? "Add the following rules in the Firebase Console to enable access to Rowy configuration:"
|
||||
: "Alternatively, you can add these rules in the Firebase Console."
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="pre"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 400,
|
||||
resize: "both",
|
||||
overflow: "auto",
|
||||
<>
|
||||
<SetupItem
|
||||
status="incomplete"
|
||||
title={
|
||||
error === "security-rules/not-found"
|
||||
? "Add the following rules in the Firebase Console to enable access to Rowy configuration:"
|
||||
: "Alternatively, you can add these rules in the Firebase Console."
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="pre"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 400,
|
||||
resize: "both",
|
||||
overflow: "auto",
|
||||
|
||||
"& .comment": { color: "info.dark" },
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: rules.replace(
|
||||
/(\/\/.*$)/gm,
|
||||
`<span class="comment">$1</span>`
|
||||
),
|
||||
}}
|
||||
/>
|
||||
"& .comment": { color: "info.dark" },
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: rules.replace(
|
||||
/(\/\/.*$)/gm,
|
||||
`<span class="comment">$1</span>`
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<CopyIcon />}
|
||||
onClick={() => navigator.clipboard.writeText(rules)}
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
<div>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<CopyIcon />}
|
||||
onClick={() => navigator.clipboard.writeText(rules)}
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href={`https://console.firebase.google.com/project/${
|
||||
projectId || "_"
|
||||
}/firestore/rules`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Set up in Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
{rulesStatus !== "LOADING" &&
|
||||
typeof rulesStatus === "string" && (
|
||||
<Typography variant="caption" color="error">
|
||||
{rulesStatus}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</SetupItem>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
href={`https://console.firebase.google.com/project/${
|
||||
projectId || "_"
|
||||
}/firestore/rules`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={verifyRules}
|
||||
loading={rulesStatus === "LOADING"}
|
||||
>
|
||||
Verify
|
||||
</LoadingButton>
|
||||
{rulesStatus !== "LOADING" && typeof rulesStatus === "string" && (
|
||||
<Typography variant="caption" color="error">
|
||||
{rulesStatus}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</SetupItem>
|
||||
<SetupItem
|
||||
status="incomplete"
|
||||
title={
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={verifyRules}
|
||||
loading={rulesStatus === "LOADING"}
|
||||
>
|
||||
Verify
|
||||
</LoadingButton>
|
||||
}
|
||||
>
|
||||
{rulesStatus !== "LOADING" && typeof rulesStatus === "string" && (
|
||||
<Typography variant="caption" color="error">
|
||||
{rulesStatus}
|
||||
</Typography>
|
||||
)}
|
||||
</SetupItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasRules && (
|
||||
@@ -334,10 +358,10 @@ export const checkRules = async (
|
||||
const sanitizedRules = rules.replace(/\s{2,}/g, " ").replace(/\n/g, " ");
|
||||
const hasRules =
|
||||
sanitizedRules.includes(
|
||||
requiredRules.replace(/\s{2,}/g, " ").replace(/\n/g, " ")
|
||||
REQUIRED_RULES.replace(/\s{2,}/g, " ").replace(/\n/g, " ")
|
||||
) &&
|
||||
sanitizedRules.includes(
|
||||
utilFns.replace(/\s{2,}/g, " ").replace(/\n/g, " ")
|
||||
RULES_UTILS.replace(/\s{2,}/g, " ").replace(/\n/g, " ")
|
||||
);
|
||||
return hasRules;
|
||||
} catch (e: any) {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
|
||||
import {
|
||||
FormControlLabel,
|
||||
@@ -8,20 +8,17 @@ import {
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export interface IStep0WelcomeProps extends ISetupStepBodyProps {
|
||||
handleContinue: () => void;
|
||||
}
|
||||
|
||||
export default function Step0Welcome({
|
||||
completion,
|
||||
setCompletion,
|
||||
handleContinue,
|
||||
}: IStep0WelcomeProps) {
|
||||
const { projectId } = useAppContext();
|
||||
export default {
|
||||
id: "welcome",
|
||||
layout: "centered",
|
||||
shortTitle: "Welcome",
|
||||
title: "Welcome",
|
||||
body: StepWelcome,
|
||||
} as ISetupStep;
|
||||
|
||||
function StepWelcome({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
@@ -32,18 +29,13 @@ export default function Step0Welcome({
|
||||
We have no access to your data and it always stays on your Firebase
|
||||
project.
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Project: <b>{projectId}</b>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={completion.welcome}
|
||||
onChange={(e) =>
|
||||
setCompletion((c) => ({ ...c, welcome: e.target.checked }))
|
||||
}
|
||||
checked={isComplete}
|
||||
onChange={(e) => setComplete(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
@@ -83,8 +75,8 @@ export default function Step0Welcome({
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
disabled={!completion.welcome}
|
||||
onClick={handleContinue}
|
||||
disabled={!isComplete}
|
||||
type="submit"
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
@@ -1,32 +1,37 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLocation, useHistory } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
|
||||
import { Button, Typography, Stack, TextField } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
import SetupItem from "../SetupItem";
|
||||
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function Step1RowyRun({
|
||||
completion,
|
||||
setCompletion,
|
||||
rowyRunUrl: paramsRowyRunUrl,
|
||||
}: ISetupStepBodyProps) {
|
||||
export default {
|
||||
id: "rowyRun",
|
||||
shortTitle: "Rowy Run",
|
||||
title: "Set up Rowy Run",
|
||||
body: StepRowyRun,
|
||||
} as ISetupStep;
|
||||
|
||||
function StepRowyRun({
|
||||
isComplete,
|
||||
setComplete,
|
||||
}: // rowyRunUrl: paramsRowyRunUrl,
|
||||
ISetupStepBodyProps) {
|
||||
const { pathname } = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
const [isValidRowyRunUrl, setIsValidRowyRunUrl] = useState(
|
||||
completion.rowyRun
|
||||
);
|
||||
const [isLatestVersion, setIsLatestVersion] = useState(completion.rowyRun);
|
||||
const [isValidRowyRunUrl, setIsValidRowyRunUrl] = useState(isComplete);
|
||||
const [isLatestVersion, setIsLatestVersion] = useState(isComplete);
|
||||
|
||||
const [rowyRunUrl, setRowyRunUrl] = useState(paramsRowyRunUrl);
|
||||
const [rowyRunUrl, setRowyRunUrl] = useState("paramsRowyRunUrl");
|
||||
const [latestVersion, setLatestVersion] = useState("");
|
||||
const [verificationStatus, setVerificationStatus] = useState<
|
||||
"IDLE" | "LOADING" | "FAIL"
|
||||
@@ -45,7 +50,7 @@ export default function Step1RowyRun({
|
||||
|
||||
if (result.isLatestVersion) {
|
||||
setIsLatestVersion(true);
|
||||
setCompletion((c) => ({ ...c, rowyRun: true }));
|
||||
setComplete();
|
||||
history.replace({
|
||||
pathname,
|
||||
search: queryString.stringify({ rowyRunUrl }),
|
||||
@@ -57,9 +62,9 @@ export default function Step1RowyRun({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isValidRowyRunUrl && paramsRowyRunUrl) console.log(paramsRowyRunUrl);
|
||||
}, [paramsRowyRunUrl, isValidRowyRunUrl]);
|
||||
// useEffect(() => {
|
||||
// if (!isValidRowyRunUrl && paramsRowyRunUrl) console.log(paramsRowyRunUrl);
|
||||
// }, [paramsRowyRunUrl, isValidRowyRunUrl]);
|
||||
|
||||
const deployButton = window.location.hostname.includes(
|
||||
EXTERNAL_LINKS.rowyAppHostName
|
||||
@@ -31,8 +31,17 @@ export default function SetupItem({
|
||||
<ArrowIcon aria-label="Item" color="primary" />
|
||||
)}
|
||||
|
||||
<Stack spacing={2} alignItems="flex-start" style={{ flexGrow: 1 }}>
|
||||
<Typography variant="inherit">{title}</Typography>
|
||||
<Stack
|
||||
spacing={2}
|
||||
alignItems="flex-start"
|
||||
style={{ flexGrow: 1, minWidth: 0 }}
|
||||
>
|
||||
<Typography
|
||||
variant="inherit"
|
||||
sx={{ "& .MuiButton-root": { mt: -0.5 } }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
{children}
|
||||
</Stack>
|
||||
|
||||
269
src/components/Setup/SetupLayout.tsx
Normal file
269
src/components/Setup/SetupLayout.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { useState, createElement } from "react";
|
||||
import { use100vh } from "react-div-100vh";
|
||||
import { SwitchTransition } from "react-transition-group";
|
||||
import type { ISetupStep } from "./types";
|
||||
|
||||
import {
|
||||
useMediaQuery,
|
||||
Paper,
|
||||
Stepper,
|
||||
Step,
|
||||
StepButton,
|
||||
MobileStepper,
|
||||
IconButton,
|
||||
Typography,
|
||||
Stack,
|
||||
DialogActions,
|
||||
} 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";
|
||||
|
||||
import BrandedBackground, { Wrapper } from "@src/assets/BrandedBackground";
|
||||
import Logo from "@src/assets/Logo";
|
||||
import ScrollableDialogContent from "@src/components/Modal/ScrollableDialogContent";
|
||||
import { SlideTransition } from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { analytics } from "analytics";
|
||||
|
||||
const BASE_WIDTH = 1024;
|
||||
|
||||
export interface ISetupLayoutProps {
|
||||
steps: ISetupStep[];
|
||||
completion: Record<string, boolean>;
|
||||
setCompletion: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
||||
continueButtonLoading?: boolean;
|
||||
}
|
||||
|
||||
export default function SetupLayout({
|
||||
steps,
|
||||
completion,
|
||||
setCompletion,
|
||||
continueButtonLoading = false,
|
||||
}: ISetupLayoutProps) {
|
||||
const fullScreenHeight = use100vh() ?? 0;
|
||||
const isMobile = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
|
||||
|
||||
// Store current step’s ID to prevent confusion
|
||||
const [stepId, setStepId] = useState("welcome");
|
||||
// Get current step object
|
||||
const step =
|
||||
steps.find((step) => step.id === (stepId || steps[0].id)) ?? steps[0];
|
||||
// Get current step index
|
||||
const stepIndex = steps.indexOf(step);
|
||||
const listedSteps = steps.filter((step) => step.layout !== "centered");
|
||||
|
||||
// Continue goes to the next incomplete step
|
||||
const handleContinue = () => {
|
||||
let nextIncompleteStepIndex = stepIndex + 1;
|
||||
while (completion[steps[nextIncompleteStepIndex]?.id]) {
|
||||
// console.log("iteration", steps[nextIncompleteStepIndex]?.id);
|
||||
nextIncompleteStepIndex++;
|
||||
}
|
||||
|
||||
const nextStepId = steps[nextIncompleteStepIndex].id;
|
||||
analytics.logEvent("setup_step", { step: nextStepId });
|
||||
setStepId(nextStepId);
|
||||
};
|
||||
|
||||
// Inject props into step.body
|
||||
const body = createElement(step.body, {
|
||||
completion,
|
||||
setCompletion,
|
||||
isComplete: completion[step.id],
|
||||
setComplete: (value: boolean = true) =>
|
||||
setCompletion((c) => ({ ...c, [step.id]: value })),
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<BrandedBackground />
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
handleContinue();
|
||||
} catch (e: any) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
component="main"
|
||||
elevation={4}
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
alpha(theme.palette.background.paper, 0.75),
|
||||
backdropFilter: "blur(20px) saturate(150%)",
|
||||
|
||||
maxWidth: BASE_WIDTH,
|
||||
width: (theme) => `calc(100vw - ${theme.spacing(2)})`,
|
||||
height: (theme) =>
|
||||
`calc(${
|
||||
fullScreenHeight > 0 ? `${fullScreenHeight}px` : "100vh"
|
||||
} - ${theme.spacing(
|
||||
2
|
||||
)} - env(safe-area-inset-top) - env(safe-area-inset-bottom))`,
|
||||
resize: "both",
|
||||
|
||||
p: 0,
|
||||
"& > *, & > .MuiDialogContent-root": { px: { xs: 2, sm: 4 } },
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
"& .MuiTypography-inherit, & .MuiDialogContent-root": {
|
||||
typography: "body1",
|
||||
},
|
||||
|
||||
"& p": {
|
||||
maxWidth: "70ch",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{stepId === "welcome" ? null : !isMobile ? (
|
||||
<Stepper
|
||||
activeStep={stepIndex - 1}
|
||||
nonLinear
|
||||
sx={{
|
||||
mt: 2.5,
|
||||
mb: 3,
|
||||
"& .MuiStep-root:first-child": { pl: 0 },
|
||||
"& .MuiStep-root:last-child": { pr: 0 },
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{listedSteps.map(({ id, shortTitle }, i) => (
|
||||
<Step key={id} completed={completion[id]}>
|
||||
<StepButton
|
||||
onClick={() => setStepId(id)}
|
||||
disabled={i > 0 && !completion[listedSteps[i - 1]?.id]}
|
||||
sx={{ py: 2, my: -2, borderRadius: 1 }}
|
||||
>
|
||||
{shortTitle}
|
||||
</StepButton>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
) : (
|
||||
<MobileStepper
|
||||
variant="dots"
|
||||
steps={listedSteps.length}
|
||||
activeStep={stepIndex - 1}
|
||||
backButton={
|
||||
<IconButton
|
||||
aria-label="Previous step"
|
||||
disabled={stepIndex === 0}
|
||||
onClick={() => setStepId(steps[stepIndex - 1].id)}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</IconButton>
|
||||
}
|
||||
nextButton={
|
||||
<IconButton
|
||||
aria-label="Next step"
|
||||
disabled={!completion[stepId]}
|
||||
onClick={() => setStepId(steps[stepIndex + 1].id)}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
</IconButton>
|
||||
}
|
||||
position="static"
|
||||
sx={{
|
||||
background: "none",
|
||||
p: 0,
|
||||
"& .MuiMobileStepper-dot": { mx: 0.5 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step.layout === "centered" ? (
|
||||
<ScrollableDialogContent disableTopDivider>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
spacing={3}
|
||||
sx={{
|
||||
minHeight: "100%",
|
||||
maxWidth: 440,
|
||||
margin: "0 auto",
|
||||
textAlign: "center",
|
||||
py: 3,
|
||||
}}
|
||||
>
|
||||
{stepId === "welcome" && (
|
||||
<SwitchTransition mode="out-in">
|
||||
<SlideTransition key={stepId} appear timeout={50}>
|
||||
<Logo size={2} />
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
)}
|
||||
|
||||
<SwitchTransition mode="out-in">
|
||||
<SlideTransition key={stepId} appear timeout={100}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{ mb: 1, typography: { xs: "h5", md: "h4" } }}
|
||||
>
|
||||
{step.title}
|
||||
</Typography>
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
|
||||
<SwitchTransition mode="out-in">
|
||||
<SlideTransition key={stepId} appear timeout={150}>
|
||||
<Stack spacing={4} alignItems="center">
|
||||
{body}
|
||||
</Stack>
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
</Stack>
|
||||
</ScrollableDialogContent>
|
||||
) : (
|
||||
<>
|
||||
<SwitchTransition mode="out-in">
|
||||
<SlideTransition key={stepId} appear timeout={50}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{ mb: 1, typography: { xs: "h5", md: "h4" } }}
|
||||
>
|
||||
{step.title}
|
||||
</Typography>
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
|
||||
<SwitchTransition mode="out-in">
|
||||
<SlideTransition key={stepId} appear timeout={100}>
|
||||
<ScrollableDialogContent
|
||||
disableTopDivider={step.layout === "centered"}
|
||||
sx={{ overflowX: "auto" }}
|
||||
>
|
||||
<Stack spacing={4}>{body}</Stack>
|
||||
</ScrollableDialogContent>
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step.layout !== "centered" && (
|
||||
<DialogActions>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
loading={continueButtonLoading}
|
||||
disabled={!completion[stepId]}
|
||||
>
|
||||
Continue
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
)}
|
||||
</Paper>
|
||||
</form>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Divider,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function Step1Oauth({
|
||||
completion,
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
Select which Firebase project to set up Rowy on.
|
||||
</Typography>
|
||||
|
||||
<SetupItem
|
||||
title="Select an existing project or create a new one."
|
||||
status="incomplete"
|
||||
>
|
||||
<TextField label="Project" select style={{ minWidth: 200 }}>
|
||||
<MenuItem value="lorem">lorem</MenuItem>
|
||||
<MenuItem value="ipsum">ipsum</MenuItem>
|
||||
<MenuItem value="dolor">dolor</MenuItem>
|
||||
<MenuItem value="sit">sit</MenuItem>
|
||||
<MenuItem value="amet">amet</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<Divider style={{ width: 200 }}>OR</Divider>
|
||||
|
||||
<Button onClick={() => setCompletion((c) => ({ ...c, project: true }))}>
|
||||
Create project in Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
</SetupItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import { Typography, Stack, Button, IconButton } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
import SignInWithGoogle from "./SignInWithGoogle";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import CopyIcon from "@src/assets/icons/Copy";
|
||||
|
||||
export default function Step2ProjectOwner({
|
||||
rowyRunUrl,
|
||||
completion,
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
const { projectId, currentUser, getAuthToken } = useAppContext();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
useEffect(() => {
|
||||
rowyRun({ serviceUrl: 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 [isDomainAuthorized, setIsDomainAuthorized] = useState(
|
||||
!!currentUser || completion.projectOwner
|
||||
);
|
||||
const isSignedIn = currentUser?.email?.toLowerCase() === email.toLowerCase();
|
||||
const [hasRoles, setHasRoles] = useState<boolean | "LOADING" | string>(
|
||||
completion.projectOwner
|
||||
);
|
||||
|
||||
const setRoles = async () => {
|
||||
setHasRoles("LOADING");
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await rowyRun({
|
||||
route: runRoutes.setOwnerRoles,
|
||||
serviceUrl: 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 (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
The project owner requires full access to manage this project. The
|
||||
default project owner is the Google Cloud account used to deploy Rowy
|
||||
Run: <b style={{ userSelect: "all" }}>{email}</b>
|
||||
</Typography>
|
||||
|
||||
<SetupItem
|
||||
status={isSignedIn || isDomainAuthorized ? "complete" : "incomplete"}
|
||||
title={
|
||||
isSignedIn || isDomainAuthorized
|
||||
? "Firebase Authentication is set up."
|
||||
: "Check that Firebase Authentication is set up with:"
|
||||
}
|
||||
>
|
||||
{!(isSignedIn || isDomainAuthorized) && (
|
||||
<>
|
||||
<ol>
|
||||
<li>the Google auth provider enabled and</li>
|
||||
<li>
|
||||
this domain authorized:{" "}
|
||||
<b style={{ userSelect: "all" }}>{window.location.hostname}</b>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(window.location.hostname)
|
||||
}
|
||||
>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<Stack spacing={1} direction="row">
|
||||
<Button
|
||||
href={`https://console.firebase.google.com/project/${
|
||||
projectId || "_"
|
||||
}/authentication/providers`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Set up in Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setIsDomainAuthorized(true)}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</SetupItem>
|
||||
|
||||
{isDomainAuthorized && (
|
||||
<SetupItem
|
||||
status={isSignedIn ? "complete" : "incomplete"}
|
||||
title={
|
||||
isSignedIn ? (
|
||||
`You’re signed in as the project owner.`
|
||||
) : (
|
||||
<>
|
||||
Sign in as the project owner: <b>{email}</b>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
{!isSignedIn && (
|
||||
<SignInWithGoogle
|
||||
matchEmail={email}
|
||||
loading={!email ? true : undefined}
|
||||
/>
|
||||
)}
|
||||
</SetupItem>
|
||||
)}
|
||||
|
||||
{isSignedIn && (
|
||||
<SetupItem
|
||||
status={hasRoles === true ? "complete" : "incomplete"}
|
||||
title={
|
||||
hasRoles === true
|
||||
? "The project owner has the admin and owner roles."
|
||||
: "Assign the admin and owner roles to the project owner."
|
||||
}
|
||||
>
|
||||
{hasRoles !== true && (
|
||||
<div>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={hasRoles === "LOADING"}
|
||||
onClick={setRoles}
|
||||
>
|
||||
Assign roles
|
||||
</LoadingButton>
|
||||
|
||||
{typeof hasRoles === "string" && hasRoles !== "LOADING" && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="error"
|
||||
display="block"
|
||||
sx={{ mt: 0.5 }}
|
||||
>
|
||||
{hasRoles}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SetupItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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({
|
||||
serviceUrl: 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;
|
||||
}
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import { Typography } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { CONFIG } from "@src/config/dbPaths";
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
|
||||
export default function Step4Migrate({
|
||||
rowyRunUrl,
|
||||
completion,
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
const { getAuthToken } = useAppContext();
|
||||
|
||||
const [status, setStatus] = useState<"LOADING" | boolean | string>(
|
||||
completion.migrate
|
||||
);
|
||||
|
||||
const migrate = async () => {
|
||||
setStatus("LOADING");
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const res = await rowyRun({
|
||||
route: runRoutes.migrateFT2Rowy,
|
||||
serviceUrl: rowyRunUrl,
|
||||
authToken,
|
||||
});
|
||||
if (!res.success) throw new Error(res.message);
|
||||
|
||||
const check = await checkMigrate(rowyRunUrl, authToken);
|
||||
if (!check.migrationRequired) {
|
||||
setCompletion((c) => ({ ...c, migrate: true }));
|
||||
setStatus(true);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setStatus(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
It looks like you’ve previously configured your Firestore database for
|
||||
Firetable. You can migrate this configuration, including your tables to{" "}
|
||||
Rowy.
|
||||
</Typography>
|
||||
|
||||
<SetupItem
|
||||
status={status === true ? "complete" : "incomplete"}
|
||||
title={
|
||||
status === true ? (
|
||||
<>
|
||||
Configuration migrated to the <code>{CONFIG}</code> collection.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Migrate your configuration to the <code>{CONFIG}</code>{" "}
|
||||
collection.
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
{status !== true && (
|
||||
<>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={status === "LOADING"}
|
||||
onClick={migrate}
|
||||
>
|
||||
Migrate
|
||||
</LoadingButton>
|
||||
{status !== "LOADING" && typeof status === "string" && (
|
||||
<Typography variant="caption" color="error">
|
||||
{status}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SetupItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const checkMigrate = async (
|
||||
rowyRunUrl: string,
|
||||
authToken: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
if (!authToken) return false;
|
||||
|
||||
try {
|
||||
const res = await rowyRun({
|
||||
serviceUrl: rowyRunUrl,
|
||||
route: runRoutes.checkFT2Rowy,
|
||||
authToken,
|
||||
signal,
|
||||
});
|
||||
return res.migrationRequired;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
15
src/components/Setup/types.d.ts
vendored
Normal file
15
src/components/Setup/types.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface ISetupStep {
|
||||
id: string;
|
||||
layout?: "centered";
|
||||
shortTitle: string;
|
||||
title: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
body: React.ComponentType<ISetupStepBodyProps>;
|
||||
}
|
||||
|
||||
export interface ISetupStepBodyProps {
|
||||
completion: Record<string, boolean>;
|
||||
setCompletion: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
||||
isComplete: boolean;
|
||||
setComplete: (value: boolean = true) => void;
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
import { CONFIG, USERS, PUBLIC_SETTINGS } from "./dbPaths";
|
||||
|
||||
export const requiredRules = `
|
||||
export const RULES_START = `rules_version = '2';
|
||||
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
`;
|
||||
|
||||
export const RULES_END = `
|
||||
}
|
||||
}`;
|
||||
|
||||
export const REQUIRED_RULES = `
|
||||
// Rowy: Allow signed in users to read Rowy configuration and admins to write
|
||||
match /${CONFIG}/{docId} {
|
||||
allow read: if request.auth != null;
|
||||
@@ -21,14 +31,14 @@ export const requiredRules = `
|
||||
}
|
||||
` as const;
|
||||
|
||||
export const adminRules = `
|
||||
export const ADMIN_RULES = `
|
||||
// Allow admins to read and write all documents
|
||||
match /{document=**} {
|
||||
allow read, write: if hasAnyRole(["ADMIN", "OWNER"]);
|
||||
}
|
||||
` as const;
|
||||
|
||||
export const utilFns = `
|
||||
export const RULES_UTILS = `
|
||||
// Rowy: Utility functions
|
||||
function isDocOwner(docId) {
|
||||
return request.auth != null && (request.auth.uid == resource.id || request.auth.uid == docId);
|
||||
@@ -38,7 +48,7 @@ export const utilFns = `
|
||||
}
|
||||
` as const;
|
||||
|
||||
export const insecureRule = `
|
||||
export const INSECURE_RULES = `
|
||||
match /{document=**} {
|
||||
allow read, write: if true;
|
||||
}
|
||||
|
||||
16
src/config/storageRules.ts
Normal file
16
src/config/storageRules.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const RULES_START = `rules_version = '2';
|
||||
|
||||
service firebase.storage {
|
||||
match /b/{bucket}/o {
|
||||
`;
|
||||
|
||||
export const RULES_END = `
|
||||
}
|
||||
}`;
|
||||
|
||||
export const REQUIRED_RULES = `
|
||||
// Rowy: Allow signed in users with Roles to read and write to Storage
|
||||
match /{allPaths=**} {
|
||||
allow read, write: if request.auth.token.roles.size() > 0;
|
||||
}
|
||||
`;
|
||||
25
src/pages/Setup/BasicSetup.tsx
Normal file
25
src/pages/Setup/BasicSetup.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import SetupLayout from "@src/components/Setup/SetupLayout";
|
||||
import StepWelcome from "@src/components/Setup/BasicSetup/StepWelcome";
|
||||
import StepRules from "@src/components/Setup/BasicSetup/StepRules";
|
||||
import StepStorageRules from "@src/components/Setup/BasicSetup/StepStorageRules";
|
||||
import StepFinish from "@src/components/Setup/BasicSetup/StepFinish";
|
||||
|
||||
const steps = [StepWelcome, StepRules, StepStorageRules, StepFinish];
|
||||
|
||||
export default function BasicSetupPage() {
|
||||
const [completion, setCompletion] = useState<Record<string, boolean>>({
|
||||
welcome: false,
|
||||
rules: false,
|
||||
storageRules: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<SetupLayout
|
||||
steps={steps}
|
||||
completion={completion}
|
||||
setCompletion={setCompletion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
34
src/pages/Setup/RowyAppSetup.tsx
Normal file
34
src/pages/Setup/RowyAppSetup.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import SetupLayout from "@src/components/Setup/SetupLayout";
|
||||
import StepWelcome from "@src/components/Setup/RowyAppSetup/StepWelcome";
|
||||
import StepOauth from "@src/components/Setup/RowyAppSetup/StepOauth";
|
||||
import StepProject from "@src/components/Setup/RowyAppSetup/StepProject";
|
||||
import StepRules from "@src/components/Setup/RowyAppSetup/StepRules";
|
||||
import StepStorageRules from "@src/components/Setup/BasicSetup/StepStorageRules";
|
||||
import StepFinish from "@src/components/Setup/BasicSetup/StepFinish";
|
||||
|
||||
const steps = [
|
||||
StepWelcome,
|
||||
StepOauth,
|
||||
StepProject,
|
||||
StepRules,
|
||||
StepStorageRules,
|
||||
StepFinish,
|
||||
];
|
||||
|
||||
export default function RowyAppSetupPage() {
|
||||
const [completion, setCompletion] = useState<Record<string, boolean>>({
|
||||
welcome: false,
|
||||
rules: false,
|
||||
storageRules: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<SetupLayout
|
||||
steps={steps}
|
||||
completion={completion}
|
||||
setCompletion={setCompletion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -28,12 +28,11 @@ import Logo from "@src/assets/Logo";
|
||||
import ScrollableDialogContent from "@src/components/Modal/ScrollableDialogContent";
|
||||
import { SlideTransition } from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import Step0Welcome from "@src/components/Setup/Step0Welcome";
|
||||
import StepWelcome from "@src/components/Setup/RowyAppSetup/StepWelcome";
|
||||
import Step1Oauth from "@src/components/Setup/Step1Oauth";
|
||||
import Step2Project from "@src/components/Setup/Step2Project";
|
||||
// prettier-ignore
|
||||
import Step2ProjectOwner, { checkProjectOwner } from "@src/components/Setup/Step2ProjectOwner";
|
||||
import Step3Rules, { checkRules } from "@src/components/Setup/Step3Rules";
|
||||
// import Step3Rules, { checkRules } from "@src/components/Setup/Step3Rules";
|
||||
import Step4Migrate, { checkMigrate } from "@src/components/Setup/Step4Migrate";
|
||||
import Step5Finish from "@src/components/Setup/Step6Finish";
|
||||
|
||||
@@ -157,8 +156,7 @@ export default function SetupPage() {
|
||||
layout: "centered" as "centered",
|
||||
shortTitle: "Welcome",
|
||||
title: `Welcome`,
|
||||
body: <Step0Welcome handleContinue={handleContinue} {...stepProps} />,
|
||||
actions: <></>,
|
||||
body: <StepWelcome {...stepProps} />,
|
||||
},
|
||||
// {
|
||||
// id: "rowyRun",
|
||||
@@ -182,19 +180,19 @@ export default function SetupPage() {
|
||||
id: "project",
|
||||
shortTitle: `Project`,
|
||||
title: `Select project`,
|
||||
body: <Step2Project {...stepProps} />,
|
||||
body: <Step1Oauth {...stepProps} />,
|
||||
},
|
||||
{
|
||||
id: "rules",
|
||||
shortTitle: `Firestore Rules`,
|
||||
title: `Set up Firestore Rules`,
|
||||
body: <Step3Rules {...stepProps} />,
|
||||
body: <Step1Oauth {...stepProps} />,
|
||||
},
|
||||
{
|
||||
id: "storageRules",
|
||||
shortTitle: `Storage Rules`,
|
||||
title: `Set up Firestore Rules`,
|
||||
body: <Step3Rules {...stepProps} />,
|
||||
body: <Step1Oauth {...stepProps} />,
|
||||
},
|
||||
{
|
||||
id: "finish",
|
||||
@@ -214,7 +212,7 @@ export default function SetupPage() {
|
||||
// </Button>
|
||||
// ),
|
||||
},
|
||||
].filter((x) => x.id);
|
||||
];
|
||||
|
||||
const step =
|
||||
steps.find((step) => step.id === (stepId || steps[0].id)) ?? steps[0];
|
||||
Reference in New Issue
Block a user