mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
Merge branch 'develop' into feat/JSON-code-completion
This commit is contained in:
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Rowy",
|
||||
"version": "2.3.2",
|
||||
"name": "rowy",
|
||||
"version": "2.4.0-rc.0",
|
||||
"homepage": "https://rowy.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -15,10 +15,10 @@
|
||||
"@hookform/resolvers": "^2.8.5",
|
||||
"@mdi/js": "^6.5.95",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@mui/icons-material": "^5.4.1",
|
||||
"@mui/lab": "^5.0.0-alpha.68",
|
||||
"@mui/material": "^5.4.1",
|
||||
"@mui/styles": "^5.4.1",
|
||||
"@mui/icons-material": "^5.4.2",
|
||||
"@mui/lab": "^5.0.0-alpha.69",
|
||||
"@mui/material": "^5.4.2",
|
||||
"@mui/styles": "^5.4.2",
|
||||
"@rowy/form-builder": "^0.5.3",
|
||||
"@rowy/multiselect": "^0.2.3",
|
||||
"@tinymce/tinymce-react": "^3.12.6",
|
||||
|
||||
@@ -12,6 +12,7 @@ import ErrorBoundary from "@src/components/ErrorBoundary";
|
||||
import Loading from "@src/components/Loading";
|
||||
import Navigation from "@src/components/Navigation";
|
||||
import Logo from "@src/assets/Logo";
|
||||
import RowyRunModal from "@src/components/RowyRunModal";
|
||||
|
||||
import SwrProvider from "@src/contexts/SwrContext";
|
||||
import ConfirmationProvider from "@src/components/ConfirmationDialog/Provider";
|
||||
@@ -65,6 +66,7 @@ export default function App() {
|
||||
<ConfirmationProvider>
|
||||
<SnackLogProvider>
|
||||
<CustomBrowserRouter>
|
||||
<RowyRunModal />
|
||||
<Suspense fallback={<Loading fullScreen />}>
|
||||
<Switch>
|
||||
<Route
|
||||
|
||||
44
src/assets/LogoRowyRun.tsx
Normal file
44
src/assets/LogoRowyRun.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { SVGProps } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
export interface ILogoRowyRunProps extends SVGProps<SVGSVGElement> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export default function LogoRowyRun({
|
||||
size = 1.5,
|
||||
...props
|
||||
}: ILogoRowyRunProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={Math.round(108 * size)}
|
||||
height={Math.round(26 * size)}
|
||||
viewBox="0 0 108 26"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-labelledby="rowy-run-logo-title"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<title id="rowy-run-logo-title">Rowy Run</title>
|
||||
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M32 7.75a6.25 6.25 0 1 1 0 12.5 6.25 6.25 0 0 1 0-12.5Zm0 2a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM20 20V8h6v2h-4v10h-2Zm24 0 3-9 3 9h2l4-12 5 11.5-2 4.5h2l7-16h-2l-4 9-4-9h-4l-3 9-3-9h-2l-3 9-3-9h-2l4 12h2Z"
|
||||
fill={theme.palette.text.primary}
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0 8v10a3 3 0 1 0 6 0v-7h7a3 3 0 1 0 0-6H8a2.997 2.997 0 0 0-2.5 1.341A3 3 0 0 0 0 8Zm10-2H8a2 2 0 0 0-1.995 1.85L6 8v2h4V6Zm-5 4V8a2 2 0 0 0-1.85-1.995L3 6a2 2 0 0 0-1.995 1.85L1 8v2h4Zm0 1H1v4h4v-4Zm-4 5v2a2 2 0 0 0 1.85 1.994L3 20a2 2 0 0 0 1.995-1.85L5 18v-2H1ZM11.001 6H13l.15.005A2 2 0 0 1 15 8l-.005.15A2 2 0 0 1 13 10h-1.999V6Z"
|
||||
fill={theme.palette.primary.main}
|
||||
/>
|
||||
<path
|
||||
d="M73.25 20h1.825v-8.375c.775-1.475 2.125-2.35 3.2-2.35.425 0 .925.075 1.35.225V7.775c-.275-.1-.725-.175-1.225-.175-1.25 0-2.65.85-3.325 2.175V7.85H73.25V20Zm17.65 0h1.85V7.85h-1.824L90.9 16.3c-.75 1.35-2.25 2.175-3.875 2.175-2.125 0-3.55-1.525-3.55-3.875V7.85H81.65v7c0 3.275 2 5.4 5 5.4 1.775 0 3.45-.825 4.25-2.175V20Zm7.007-12.15h-1.825V20h1.825v-8.475c.75-1.325 2.275-2.15 3.875-2.15 2.125 0 3.55 1.525 3.55 3.875V20h1.825v-7c0-3.275-2-5.4-5-5.4-1.775 0-3.45.825-4.25 2.15v-1.9Z"
|
||||
fill={theme.palette.primary.main}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 23">
|
||||
<g clip-path="url(#a)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 15V5A5 5 0 0 1 7.5.669 4.977 4.977 0 0 1 10 0h5a5 5 0 0 1 0 10h-5v5a5 5 0 0 1-10 0Z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 5a2 2 0 0 1 2-2h6c.721 0 1.354.382 1.705.955A8.212 8.212 0 0 1 34 2.75c1.573 0 3.044.44 4.295 1.205A2 2 0 0 1 40 3h2a2 2 0 0 1 1.897 1.368L45 7.675l1.103-3.307A2 2 0 0 1 48 3h2a2 2 0 0 1 1.897 1.368L53 7.675l1.103-3.307A2 2 0 0 1 56 3h4a2 2 0 0 1 1.828 1.188L64 9.076l2.172-4.888A2 2 0 0 1 68 3h2a2 2 0 0 1 1.832 2.802l-7 16A2 2 0 0 1 63 23h-2a2 2 0 0 1-1.828-2.812l1.643-3.697-2.568-5.907-2.35 7.049A2 2 0 0 1 54 19h-2a2 2 0 0 1-1.897-1.367L49 14.325l-1.103 3.308A2 2 0 0 1 46 19h-2a2 2 0 0 1-1.897-1.367l-.88-2.642A8.253 8.253 0 0 1 26 13.024V17a2 2 0 0 1-2.001 2h-2a2 2 0 0 1-2-2V5Z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M34 4.75a6.25 6.25 0 1 1 0 12.5 6.25 6.25 0 0 1 0-12.5Zm0 2a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM22 17V5h6v2h-4v10h-2Zm24 0 3-9 3 9h2l4-12 5 11.5-2 4.5h2l7-16h-2l-4 9-4-9h-4l-3 9-3-9h-2l-3 9-3-9h-2l4 12h2Z" fill="#000"/>
|
||||
<g fill-rule="evenodd" clip-rule="evenodd">
|
||||
<path d="M8 15v-3H2v3a3 3 0 1 0 6 0Zm-5-2h4v2l-.005.15A2 2 0 0 1 5 17l-.15-.006A2 2 0 0 1 3 15v-2Z" fill="url(#b)"/>
|
||||
<path d="M2 5v3h6V5a3 3 0 0 0-6 0Zm5 2H3V5l.005-.15A2 2 0 0 1 5 3l.15.005A2 2 0 0 1 7 5v2Z" fill="#4200FF"/>
|
||||
<path d="M8 13V7H2v6h6ZM3 8h4v4H3V8Z" fill="url(#c)"/>
|
||||
<path d="M15 2h-3v6h3a3 3 0 1 0 0-6Zm-1.999 5V3H15l.15.005A2 2 0 0 1 17 5l-.006.15A2 2 0 0 1 15 7h-1.999Z" fill="url(#d)"/>
|
||||
<path d="M7 5v3h6V2h-3a3 3 0 0 0-3 3Zm3-2h2v4H8V5l.005-.15A2 2 0 0 1 10 3Z" fill="url(#e)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="b" x1="2.5" y1="12.999" x2="2.5" y2="18" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0A"/>
|
||||
<stop offset="1" stop-color="#FA0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="2.488" y1="7.977" x2="2.488" y2="13" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4200FF"/>
|
||||
<stop offset="1" stop-color="#F0A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d" x1="13.017" y1="7.492" x2="18" y2="7.492" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0AF"/>
|
||||
<stop offset="1" stop-color="#0FA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e" x1="13" y1="2.498" x2="7.997" y2="2.498" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0AF"/>
|
||||
<stop offset="1" stop-color="#4200FF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h72v23H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
10
src/atoms/RowyRunModal.ts
Normal file
10
src/atoms/RowyRunModal.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { atom, useAtom } from "jotai";
|
||||
|
||||
export const rowyRunModalAtom = atom({ open: false, feature: "", version: "" });
|
||||
|
||||
export const useRowyRunModal = () => {
|
||||
const [, setOpen] = useAtom(rowyRunModalAtom);
|
||||
|
||||
return (feature: string = "", version: string = "") =>
|
||||
setOpen({ open: true, feature, version });
|
||||
};
|
||||
@@ -45,6 +45,11 @@ export default function CodeEditorHelper({
|
||||
key: "storage",
|
||||
description: `firebase Storage can be accessed through this, storage.bucket() returns default storage bucket of the firebase project.`,
|
||||
},
|
||||
{
|
||||
key: "rowy",
|
||||
description: `rowy provides a set of functions that are commonly used, such as easy access to GCP Secret Manager`,
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -108,17 +113,6 @@ export default function CodeEditorHelper({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
{/* <Button
|
||||
size="small"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
Examples & docs
|
||||
<InlineOpenInNewIcon />
|
||||
</Button> */}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
/**
|
||||
* Gets the secret defined in Google Cloud Secret
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function AccessDenied() {
|
||||
description={
|
||||
<>
|
||||
<Typography>
|
||||
You are currently signed in as {currentUser?.email}
|
||||
You are signed in as <strong>{currentUser?.email}</strong>
|
||||
</Typography>
|
||||
<Typography>
|
||||
You do not have access to this project. Please contact the project
|
||||
|
||||
101
src/components/RowyRunModal.tsx
Normal file
101
src/components/RowyRunModal.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAtom } from "jotai";
|
||||
import { rowyRunModalAtom } from "@src/atoms/RowyRunModal";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
Button,
|
||||
DialogContentText,
|
||||
Link as MuiLink,
|
||||
} from "@mui/material";
|
||||
|
||||
import Modal from "@src/components/Modal";
|
||||
import Logo from "@src/assets/LogoRowyRun";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { routes } from "@src/constants/routes";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
|
||||
export default function RowyRunModal() {
|
||||
const { userClaims } = useAppContext();
|
||||
const { settings } = useProjectContext();
|
||||
|
||||
const [state, setState] = useAtom(rowyRunModalAtom);
|
||||
const handleClose = () => setState((s) => ({ ...s, open: false }));
|
||||
|
||||
const showUpdateModal = state.version && settings?.rowyRunUrl;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={state.open}
|
||||
onClose={handleClose}
|
||||
title={
|
||||
<Logo
|
||||
size={2}
|
||||
style={{
|
||||
margin: "16px auto",
|
||||
display: "block",
|
||||
position: "relative",
|
||||
right: 44 / -2,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
maxWidth="xs"
|
||||
body={
|
||||
<>
|
||||
<Typography variant="h5" paragraph align="center">
|
||||
{showUpdateModal ? "Update" : "Set up"} Rowy Run to use{" "}
|
||||
{state.feature || "this feature"}
|
||||
</Typography>
|
||||
|
||||
{showUpdateModal && (
|
||||
<DialogContentText variant="body1" paragraph textAlign="center">
|
||||
{state.feature || "This feature"} requires Rowy Run v
|
||||
{state.version} or later.
|
||||
</DialogContentText>
|
||||
)}
|
||||
|
||||
<DialogContentText variant="body1" paragraph textAlign="center">
|
||||
Rowy Run is a Cloud Run instance that provides backend
|
||||
functionality, such as table action scripts, user management, and
|
||||
easy Cloud Function deployment.{" "}
|
||||
<MuiLink
|
||||
href={WIKI_LINKS.rowyRun}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more
|
||||
<InlineOpenInNewIcon />
|
||||
</MuiLink>
|
||||
</DialogContentText>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to={routes.projectSettings + "#rowyRun"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
onClick={handleClose}
|
||||
style={{ display: "flex" }}
|
||||
disabled={!userClaims?.roles.includes("ADMIN")}
|
||||
>
|
||||
Set up Rowy Run
|
||||
</Button>
|
||||
|
||||
{!userClaims?.roles.includes("ADMIN") && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
textAlign="center"
|
||||
color="error"
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Contact the project owner to set up Rowy Run
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import TwitterIcon from "@mui/icons-material/Twitter";
|
||||
import Logo from "@src/assets/Logo";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import { name, version } from "@root/package.json";
|
||||
import { version } from "@root/package.json";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
@@ -100,7 +100,7 @@ export default function About() {
|
||||
)}
|
||||
|
||||
<Typography display="block" color="textSecondary">
|
||||
{name} v{version}
|
||||
Rowy v{version}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
|
||||
import LogoRowyRun from "@src/assets/LogoRowyRun";
|
||||
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
||||
import { name } from "@root/package.json";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
|
||||
export default function RowyRun({
|
||||
@@ -88,8 +88,12 @@ export default function RowyRun({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
{name} Run is a Cloud Run instance that provides backend functionality,
|
||||
<LogoRowyRun
|
||||
style={{ display: "block", marginLeft: "auto", marginRight: "auto" }}
|
||||
/>
|
||||
|
||||
<Typography style={{ marginTop: 8 }}>
|
||||
Rowy Run is a Cloud Run instance that provides backend functionality,
|
||||
such as table action scripts, user management, and easy Cloud Function
|
||||
deployment.{" "}
|
||||
<Link
|
||||
@@ -137,7 +141,7 @@ export default function RowyRun({
|
||||
)}
|
||||
|
||||
<Typography display="block" color="textSecondary">
|
||||
{name} Run v{latestUpdate.deployedRowyRun}
|
||||
Rowy Run v{latestUpdate.deployedRowyRun}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
@@ -166,7 +170,7 @@ export default function RowyRun({
|
||||
>
|
||||
<Grid item xs={12} sm>
|
||||
<Typography>
|
||||
If you have not yet deployed {name} Run, click this button and
|
||||
If you have not yet deployed Rowy Run, click this button and
|
||||
follow the prompts on Cloud Shell.
|
||||
</Typography>
|
||||
</Grid>
|
||||
@@ -196,11 +200,10 @@ export default function RowyRun({
|
||||
color="success"
|
||||
style={{ fontSize: "1rem", verticalAlign: "text-top" }}
|
||||
/>
|
||||
|
||||
{name} Run is set up correctly
|
||||
Rowy Run is set up correctly
|
||||
</>
|
||||
) : verified === false ? (
|
||||
`${name} Run is not set up correctly`
|
||||
`Rowy Run is not set up correctly`
|
||||
) : (
|
||||
" "
|
||||
)
|
||||
|
||||
@@ -17,10 +17,12 @@ import Modal from "@src/components/Modal";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import routes from "@src/constants/routes";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { useRowyRunModal } from "@src/atoms/RowyRunModal";
|
||||
|
||||
export default function InviteUser() {
|
||||
const { roles: projectRoles, rowyRun } = useProjectContext();
|
||||
const { roles: projectRoles, rowyRun, settings } = useProjectContext();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const openRowyRunModal = useRowyRunModal();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [status, setStatus] = useState<"LOADING" | string>("");
|
||||
@@ -49,7 +51,11 @@ export default function InviteUser() {
|
||||
<>
|
||||
<Button
|
||||
aria-label="Invite user"
|
||||
onClick={() => setOpen(true)}
|
||||
onClick={
|
||||
settings?.rowyRunUrl
|
||||
? () => setOpen(true)
|
||||
: () => openRowyRunModal("Invite user")
|
||||
}
|
||||
variant="text"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
285
src/components/Setup/SetupLayout.tsx
Normal file
285
src/components/Setup/SetupLayout.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
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 disableBottomDivider>
|
||||
<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={25}>
|
||||
<Logo size={2} />
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
)}
|
||||
|
||||
<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}>
|
||||
<Typography variant="inherit">
|
||||
{step.description}
|
||||
</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>
|
||||
|
||||
<ScrollableDialogContent
|
||||
disableTopDivider={step.layout === "centered"}
|
||||
sx={{ overflowX: "auto" }}
|
||||
>
|
||||
<SwitchTransition mode="out-in">
|
||||
<SlideTransition key={stepId} appear timeout={100}>
|
||||
<Typography variant="inherit">
|
||||
{step.description}
|
||||
</Typography>
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
|
||||
<SwitchTransition mode="out-in">
|
||||
<SlideTransition key={stepId} appear timeout={150}>
|
||||
<Stack spacing={4}>{body}</Stack>
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
</ScrollableDialogContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step.layout !== "centered" && (
|
||||
<DialogActions>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
loading={continueButtonLoading}
|
||||
disabled={!completion[stepId]}
|
||||
>
|
||||
Continue
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
)}
|
||||
</Paper>
|
||||
</form>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
@@ -41,16 +41,22 @@ export default function SignInWithGoogle({
|
||||
<img
|
||||
src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg"
|
||||
alt="Google logo"
|
||||
width={20}
|
||||
height={20}
|
||||
width={18}
|
||||
height={18}
|
||||
style={{
|
||||
margin: (24 - 20) / 2,
|
||||
margin: (24 - 18) / 2,
|
||||
filter: props.disabled ? "grayscale(1)" : "",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={handleSignIn}
|
||||
loading={status === "LOADING"}
|
||||
style={{ minHeight: 40 }}
|
||||
sx={{
|
||||
minHeight: 40,
|
||||
"& .MuiButton-startIcon": { mr: 3 },
|
||||
"&.MuiButton-outlined": { pr: 3 },
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
Sign in with Google
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLocation, useHistory } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
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 { name } from "@root/package.json";
|
||||
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) {
|
||||
const { pathname } = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
const [isValidRowyRunUrl, setIsValidRowyRunUrl] = useState(
|
||||
completion.rowyRun
|
||||
);
|
||||
const [isLatestVersion, setIsLatestVersion] = useState(completion.rowyRun);
|
||||
|
||||
const [rowyRunUrl, setRowyRunUrl] = useState(paramsRowyRunUrl);
|
||||
const [latestVersion, setLatestVersion] = useState("");
|
||||
const [verificationStatus, setVerificationStatus] = useState<
|
||||
"IDLE" | "LOADING" | "FAIL"
|
||||
>("IDLE");
|
||||
|
||||
const verifyRowyRun = async () => {
|
||||
setVerificationStatus("LOADING");
|
||||
|
||||
try {
|
||||
const result = await checkRowyRun(rowyRunUrl);
|
||||
setVerificationStatus("IDLE");
|
||||
|
||||
if (result.isValidRowyRunUrl) setIsValidRowyRunUrl(true);
|
||||
|
||||
setLatestVersion(result.latestVersion);
|
||||
|
||||
if (result.isLatestVersion) {
|
||||
setIsLatestVersion(true);
|
||||
setCompletion((c) => ({ ...c, rowyRun: true }));
|
||||
history.replace({
|
||||
pathname,
|
||||
search: queryString.stringify({ rowyRunUrl }),
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`Failed to verify Rowy Run URL: ${e}`);
|
||||
setVerificationStatus("FAIL");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isValidRowyRunUrl && paramsRowyRunUrl) console.log(paramsRowyRunUrl);
|
||||
}, [paramsRowyRunUrl, isValidRowyRunUrl]);
|
||||
|
||||
const deployButton = window.location.hostname.includes(
|
||||
EXTERNAL_LINKS.rowyAppHostName
|
||||
) ? (
|
||||
<a
|
||||
href={EXTERNAL_LINKS.rowyRunDeploy}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
src="https://deploy.cloud.run/button.svg"
|
||||
alt="Run on Google Cloud"
|
||||
width={183}
|
||||
height={32}
|
||||
style={{ display: "block" }}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<Button href={WIKI_LINKS.rowyRun} target="_blank" rel="noopener noreferrer">
|
||||
Deploy instructions
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
{name} Run is a Google Cloud Run instance that provides backend
|
||||
functionality, such as table action scripts, user management, and easy
|
||||
Cloud Function deployment.
|
||||
</Typography>
|
||||
|
||||
<SetupItem
|
||||
status={isValidRowyRunUrl ? "complete" : "incomplete"}
|
||||
title={
|
||||
isValidRowyRunUrl
|
||||
? `Rowy Run is set up at: ${rowyRunUrl}`
|
||||
: "Deploy Rowy Run to your Google Cloud project."
|
||||
}
|
||||
>
|
||||
{!isValidRowyRunUrl && (
|
||||
<>
|
||||
{deployButton}
|
||||
|
||||
<div>
|
||||
<Typography variant="inherit" gutterBottom>
|
||||
Then paste the Rowy Run instance URL below:
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<TextField
|
||||
id="rowyRunUrl"
|
||||
label="Rowy Run instance URL"
|
||||
placeholder="https://rowy-backend-*.run.app"
|
||||
value={rowyRunUrl}
|
||||
onChange={(e) => setRowyRunUrl(e.target.value)}
|
||||
type="url"
|
||||
autoComplete="url"
|
||||
fullWidth
|
||||
error={verificationStatus === "FAIL"}
|
||||
helperText={
|
||||
verificationStatus === "FAIL" ? "Invalid URL" : " "
|
||||
}
|
||||
/>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={verificationStatus === "LOADING"}
|
||||
onClick={verifyRowyRun}
|
||||
>
|
||||
Verify
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SetupItem>
|
||||
{isValidRowyRunUrl && (
|
||||
<SetupItem
|
||||
status={isLatestVersion ? "complete" : "incomplete"}
|
||||
title={
|
||||
isLatestVersion
|
||||
? latestVersion
|
||||
? `Rowy Run is up to date: ${latestVersion}`
|
||||
: "Rowy Run is up to date."
|
||||
: `Update your Rowy Run instance. Latest version: ${latestVersion}`
|
||||
}
|
||||
>
|
||||
{!isLatestVersion && (
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
{deployButton}
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={verificationStatus === "LOADING"}
|
||||
onClick={verifyRowyRun}
|
||||
>
|
||||
Verify
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
)}
|
||||
</SetupItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const checkRowyRun = async (
|
||||
serviceUrl: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const result = {
|
||||
isValidRowyRunUrl: false,
|
||||
isLatestVersion: false,
|
||||
latestVersion: "",
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await rowyRun({ serviceUrl, route: runRoutes.version, signal });
|
||||
if (!res.version) return result;
|
||||
|
||||
result.isValidRowyRunUrl = true;
|
||||
|
||||
// https://docs.github.com/en/rest/reference/repos#get-the-latest-release
|
||||
const endpoint =
|
||||
EXTERNAL_LINKS.rowyRunGitHub.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;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
@@ -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,349 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Button,
|
||||
Link,
|
||||
Grid,
|
||||
} from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
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 DiffEditor from "@src/components/CodeEditor/DiffEditor";
|
||||
import CodeEditor from "@src/components/CodeEditor";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { CONFIG } from "@src/config/dbPaths";
|
||||
import {
|
||||
requiredRules,
|
||||
adminRules,
|
||||
utilFns,
|
||||
insecureRule,
|
||||
} from "@src/config/firestoreRules";
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
// import { useConfirmation } from "@src/components/ConfirmationDialog";
|
||||
|
||||
const insecureRuleRegExp = new RegExp(
|
||||
insecureRule
|
||||
.replace(/\//g, "\\/")
|
||||
.replace(/\*/g, "\\*")
|
||||
.replace(/\s{2,}/g, "\\s+")
|
||||
.replace(/\s/g, "\\s*")
|
||||
.replace(/\n/g, "\\s+")
|
||||
.replace(/;/g, ";?")
|
||||
);
|
||||
|
||||
export default function Step3Rules({
|
||||
rowyRunUrl,
|
||||
completion,
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
const { projectId, getAuthToken } = useAppContext();
|
||||
// const { requestConfirmation } = useConfirmation();
|
||||
|
||||
const [error, setError] = useState<string | false>(false);
|
||||
const [hasRules, setHasRules] = useState(completion.rules);
|
||||
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}" : ""
|
||||
}`.replace("\n", "");
|
||||
|
||||
const [currentRules, setCurrentRules] = useState("");
|
||||
useEffect(() => {
|
||||
if (rowyRunUrl && !hasRules && !currentRules)
|
||||
getAuthToken(true)
|
||||
.then((authToken) =>
|
||||
rowyRun({
|
||||
serviceUrl: rowyRunUrl,
|
||||
route: runRoutes.firestoreRules,
|
||||
authToken,
|
||||
})
|
||||
)
|
||||
.then((data) => {
|
||||
if (data?.code) {
|
||||
setError(data.code);
|
||||
setShowManualMode(true);
|
||||
} else {
|
||||
setCurrentRules(data?.source?.[0]?.content ?? "");
|
||||
}
|
||||
});
|
||||
}, [rowyRunUrl, hasRules, currentRules, getAuthToken]);
|
||||
|
||||
const hasInsecureRule = insecureRuleRegExp.test(currentRules);
|
||||
|
||||
const [newRules, setNewRules] = useState("");
|
||||
useEffect(() => {
|
||||
let rulesToInsert = rules;
|
||||
|
||||
if (currentRules.indexOf("function isDocOwner") > -1) {
|
||||
rulesToInsert = rulesToInsert.replace(/function isDocOwner[^}]*}/s, "");
|
||||
}
|
||||
if (currentRules.indexOf("function hasAnyRole") > -1) {
|
||||
rulesToInsert = rulesToInsert.replace(/function hasAnyRole[^}]*}/s, "");
|
||||
}
|
||||
|
||||
let inserted = currentRules.replace(
|
||||
/match\s*\/databases\/\{database\}\/documents\s*\{/,
|
||||
`match /databases/{database}/documents {\n` + rulesToInsert
|
||||
);
|
||||
|
||||
if (hasInsecureRule) inserted = inserted.replace(insecureRuleRegExp, "");
|
||||
|
||||
setNewRules(inserted);
|
||||
}, [currentRules, rules, hasInsecureRule]);
|
||||
|
||||
const [rulesStatus, setRulesStatus] = useState<"LOADING" | string>("");
|
||||
const setRules = async () => {
|
||||
setRulesStatus("LOADING");
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
if (!authToken) throw new Error("Failed to generate auth token");
|
||||
|
||||
const res = await rowyRun({
|
||||
serviceUrl: rowyRunUrl,
|
||||
route: runRoutes.setFirestoreRules,
|
||||
authToken,
|
||||
body: { ruleset: newRules },
|
||||
});
|
||||
if (!res.success) throw new Error(res.message);
|
||||
const isSuccessful = await checkRules(rowyRunUrl, authToken);
|
||||
if (isSuccessful) {
|
||||
setCompletion((c) => ({ ...c, rules: true }));
|
||||
setHasRules(true);
|
||||
}
|
||||
setRulesStatus("");
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setRulesStatus(e.message);
|
||||
}
|
||||
};
|
||||
const verifyRules = async () => {
|
||||
setRulesStatus("LOADING");
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
if (!authToken) throw new Error("Failed to generate auth token");
|
||||
|
||||
const isSuccessful = await checkRules(rowyRunUrl, authToken);
|
||||
if (isSuccessful) {
|
||||
setCompletion((c) => ({ ...c, rules: true }));
|
||||
setHasRules(true);
|
||||
}
|
||||
setRulesStatus("");
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setRulesStatus(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// const handleSkip = () => {
|
||||
// requestConfirmation({
|
||||
// title: "Skip rules",
|
||||
// body: "This might prevent you or other users in your project from accessing firestore data on Rowy",
|
||||
// confirm: "Skip",
|
||||
// cancel: "cancel",
|
||||
// handleConfirm: async () => {
|
||||
// setCompletion((c) => ({ ...c, rules: true }));
|
||||
// setHasRules(true);
|
||||
// },
|
||||
// });
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
{name} 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>
|
||||
|
||||
{!hasRules && error !== "security-rules/not-found" && (
|
||||
<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>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
sx={{ fontSize: 18, mr: 11 / 8, verticalAlign: "sub" }}
|
||||
/>
|
||||
We removed an insecure rule that allows anyone to access any part of
|
||||
your database
|
||||
</Typography>
|
||||
|
||||
<DiffEditor
|
||||
original={currentRules}
|
||||
modified={newRules}
|
||||
containerProps={{ sx: { width: "100%" } }}
|
||||
minHeight={400}
|
||||
options={{ renderValidationDecorations: "off" }}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="inherit"
|
||||
color={
|
||||
rulesStatus !== "LOADING" && rulesStatus ? "error" : undefined
|
||||
}
|
||||
>
|
||||
Please verify the new rules first.
|
||||
</Typography>
|
||||
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={setRules}
|
||||
loading={rulesStatus === "LOADING"}
|
||||
>
|
||||
Set Firestore Rules
|
||||
</LoadingButton>
|
||||
{rulesStatus !== "LOADING" && typeof rulesStatus === "string" && (
|
||||
<Typography variant="caption" color="error">
|
||||
{rulesStatus}
|
||||
</Typography>
|
||||
)}
|
||||
{!showManualMode && (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
onClick={() => setShowManualMode(true)}
|
||||
>
|
||||
Alternatively, add these rules in the Firebase Console
|
||||
</Link>
|
||||
)}
|
||||
</SetupItem>
|
||||
)}
|
||||
|
||||
{!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",
|
||||
|
||||
"& .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>
|
||||
</Grid>
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
{hasRules && (
|
||||
<SetupItem status="complete" title="Firestore Rules are set up." />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const checkRules = async (
|
||||
rowyRunUrl: string,
|
||||
authToken: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
if (!authToken) return false;
|
||||
|
||||
try {
|
||||
const res = await rowyRun({
|
||||
serviceUrl: rowyRunUrl,
|
||||
route: runRoutes.firestoreRules,
|
||||
authToken,
|
||||
signal,
|
||||
});
|
||||
const rules = res?.source?.[0]?.content || "";
|
||||
if (!rules) return false;
|
||||
|
||||
const sanitizedRules = rules.replace(/\s{2,}/g, " ").replace(/\n/g, " ");
|
||||
const hasRules =
|
||||
sanitizedRules.includes(
|
||||
requiredRules.replace(/\s{2,}/g, " ").replace(/\n/g, " ")
|
||||
) &&
|
||||
sanitizedRules.includes(
|
||||
utilFns.replace(/\s{2,}/g, " ").replace(/\n/g, " ")
|
||||
);
|
||||
return hasRules;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import { Typography, Button } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
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{" "}
|
||||
{name}.
|
||||
</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;
|
||||
}
|
||||
};
|
||||
@@ -1,17 +1,29 @@
|
||||
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";
|
||||
import ThumbDownOffIcon from "@mui/icons-material/ThumbDownOffAlt";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
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!",
|
||||
description:
|
||||
"You can now continue to Rowy and create a table from your Firestore collections.",
|
||||
body: StepFinish,
|
||||
} as ISetupStep;
|
||||
|
||||
function StepFinish() {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,11 +39,6 @@ export default function Step6Finish() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
You can now continue to {name} and create a table from your Firestore
|
||||
collections.
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
component="fieldset"
|
||||
spacing={1}
|
||||
@@ -67,6 +74,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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
141
src/components/Setup/Steps/StepRules.tsx
Normal file
141
src/components/Setup/Steps/StepRules.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
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",
|
||||
description: (
|
||||
<>
|
||||
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.
|
||||
</>
|
||||
),
|
||||
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 (
|
||||
<>
|
||||
<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"}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
108
src/components/Setup/Steps/StepStorageRules.tsx
Normal file
108
src/components/Setup/Steps/StepStorageRules.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
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",
|
||||
description:
|
||||
"Image and File fields store files in Firebase Storage. Your users will need read and write access.",
|
||||
body: StepStorageRules,
|
||||
} as ISetupStep;
|
||||
|
||||
const rules = RULES_START + REQUIRED_RULES + RULES_END;
|
||||
|
||||
function StepStorageRules({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
const { projectId } = useAppContext();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
return (
|
||||
<>
|
||||
<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()}
|
||||
>
|
||||
Mark as done
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
status={isComplete ? "complete" : "incomplete"}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,47 @@
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
|
||||
import { FormControlLabel, Checkbox, Typography, Link } from "@mui/material";
|
||||
import {
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Typography,
|
||||
Link,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
|
||||
export default function Step0Welcome({
|
||||
completion,
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
export default {
|
||||
id: "welcome",
|
||||
layout: "centered",
|
||||
shortTitle: "Welcome",
|
||||
title: "Welcome",
|
||||
description: (
|
||||
<>
|
||||
Get started with Rowy in just a few minutes.
|
||||
<br />
|
||||
<br />
|
||||
We have no access to your data and it always stays on your Firebase
|
||||
project.
|
||||
</>
|
||||
),
|
||||
body: StepWelcome,
|
||||
} as ISetupStep;
|
||||
|
||||
function StepWelcome({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
const { projectId } = useAppContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
You’ll be up and running in just a few minutes.
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Configure your project’s backend functionality, Firestore Rules, and
|
||||
user management.
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Project: <b>{projectId}</b>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="inherit">
|
||||
Project: <code>{projectId}</code>
|
||||
</Typography>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={completion.welcome}
|
||||
onChange={(e) =>
|
||||
setCompletion((c) => ({ ...c, welcome: e.target.checked }))
|
||||
}
|
||||
checked={isComplete}
|
||||
onChange={(e) => setComplete(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
@@ -67,6 +76,16 @@ export default function Step0Welcome({
|
||||
m: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
disabled={!isComplete}
|
||||
type="submit"
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { createElement, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import _isEmpty from "lodash/isEmpty";
|
||||
import _set from "lodash/set";
|
||||
import createPersistedState from "use-persisted-state";
|
||||
|
||||
import { Stack, FormControlLabel, Switch } from "@mui/material";
|
||||
@@ -44,7 +45,16 @@ export default function Form({ values }: IFormProps) {
|
||||
// Get initial values from fields config. This won’t be written to the db
|
||||
// when the SideDrawer is opened. Only dirty fields will be written
|
||||
const initialValues = fields.reduce(
|
||||
(a, { key, type }) => ({ ...a, [key]: getFieldProp("initialValue", type) }),
|
||||
(a, { key, type }) => {
|
||||
const initialValue = getFieldProp("initialValue", type);
|
||||
const nextValues = { ...a };
|
||||
if (key.indexOf('.') !== -1) {
|
||||
_set(nextValues, key, initialValue);
|
||||
} else {
|
||||
nextValues[key] = initialValue;
|
||||
}
|
||||
return nextValues;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const { ref: docRef, ...rowValues } = values;
|
||||
|
||||
@@ -76,6 +76,8 @@ export default function DefaultValueInput({
|
||||
fieldName,
|
||||
...props
|
||||
}: IDefaultValueInputProps) {
|
||||
const { settings } = useProjectContext();
|
||||
|
||||
const _type =
|
||||
type !== FieldType.derivative
|
||||
? type
|
||||
@@ -98,11 +100,6 @@ export default function DefaultValueInput({
|
||||
onChange={(e) => handleChange("defaultValue.type")(e.target.value)}
|
||||
fullWidth
|
||||
sx={{ mb: 1 }}
|
||||
SelectProps={{
|
||||
MenuProps: {
|
||||
sx: { "& .MuiListItemText-root": { whiteSpace: "normal" } },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="undefined">
|
||||
<ListItemText
|
||||
@@ -126,10 +123,30 @@ export default function DefaultValueInput({
|
||||
secondary="Set a specific default value for all cells in this column."
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="dynamic">
|
||||
<MenuItem
|
||||
value="dynamic"
|
||||
disabled={!settings?.rowyRunUrl}
|
||||
sx={{
|
||||
"&.Mui-disabled": { opacity: 1, color: "text.disabled" },
|
||||
"&.Mui-disabled .MuiListItemText-secondary": {
|
||||
color: "text.disabled",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={`Dynamic (Requires ${name} Cloud Functions)`}
|
||||
secondary={`Write code to set the default value using this table’s ${name} Cloud Function. Setup is required.`}
|
||||
primary={
|
||||
settings?.rowyRunUrl ? (
|
||||
"Dynamic"
|
||||
) : (
|
||||
<>
|
||||
Dynamic —{" "}
|
||||
<Typography color="error" variant="inherit" component="span">
|
||||
Requires Rowy Run setup
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
}
|
||||
secondary="Write code to set the default value using Rowy Run"
|
||||
/>
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
|
||||
@@ -5,18 +5,27 @@ import LogsIcon from "@src/assets/icons/CloudLogs";
|
||||
import CloudLogsModal from "./CloudLogsModal";
|
||||
|
||||
import { modalAtom } from "@src/atoms/Table";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { useRowyRunModal } from "@src/atoms/RowyRunModal";
|
||||
|
||||
export default function CloudLogs() {
|
||||
const [modal, setModal] = useAtom(modalAtom);
|
||||
const open = modal === "cloudLogs";
|
||||
const setOpen = (open: boolean) => setModal(open ? "cloudLogs" : "");
|
||||
|
||||
const { settings } = useProjectContext();
|
||||
const openRowyRunModal = useRowyRunModal();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableHeaderButton
|
||||
title="Cloud logs"
|
||||
icon={<LogsIcon />}
|
||||
onClick={() => setOpen(true)}
|
||||
onClick={
|
||||
settings?.rowyRunUrl
|
||||
? () => setOpen(true)
|
||||
: () => openRowyRunModal("Cloud logs")
|
||||
}
|
||||
/>
|
||||
|
||||
{open && (
|
||||
|
||||
@@ -19,9 +19,10 @@ import { emptyExtensionObject, IExtension, ExtensionType } from "./utils";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { analytics } from "@src/analytics";
|
||||
import { modalAtom } from "@src/atoms/Table";
|
||||
import { useRowyRunModal } from "@src/atoms/RowyRunModal";
|
||||
|
||||
export default function Extensions() {
|
||||
const { tableState, tableActions, rowyRun } = useProjectContext();
|
||||
const { tableState, tableActions, rowyRun, settings } = useProjectContext();
|
||||
const appContext = useAppContext();
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
|
||||
@@ -45,6 +46,16 @@ export default function Extensions() {
|
||||
const snackLogContext = useSnackLogContext();
|
||||
const edited = !_isEqual(currentExtensionObjects, localExtensionsObjects);
|
||||
|
||||
const openRowyRunModal = useRowyRunModal();
|
||||
if (!settings?.rowyRunUrl)
|
||||
return (
|
||||
<TableHeaderButton
|
||||
title="Extensions"
|
||||
onClick={() => openRowyRunModal("Extensions")}
|
||||
icon={<ExtensionIcon />}
|
||||
/>
|
||||
);
|
||||
|
||||
const handleOpen = () => {
|
||||
if (tableState?.config.sparks) {
|
||||
// migration is required
|
||||
|
||||
@@ -9,6 +9,7 @@ import { isCollectionGroup } from "@src/utils/fns";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
|
||||
import Modal from "@src/components/Modal";
|
||||
import { useRowyRunModal } from "@src/atoms/RowyRunModal";
|
||||
|
||||
export default function ReExecute() {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -17,7 +18,18 @@ export default function ReExecute() {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const { tableState } = useProjectContext();
|
||||
const { tableState, settings } = useProjectContext();
|
||||
|
||||
const openRowyRunModal = useRowyRunModal();
|
||||
if (!settings?.rowyRunUrl)
|
||||
return (
|
||||
<TableHeaderButton
|
||||
title="Force refresh"
|
||||
onClick={() => openRowyRunModal()}
|
||||
icon={<LoopIcon />}
|
||||
/>
|
||||
);
|
||||
|
||||
const query: any = isCollectionGroup()
|
||||
? db.collectionGroup(tableState?.tablePath!)
|
||||
: db.collection(tableState?.tablePath!);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { analytics } from "@src/analytics";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { modalAtom } from "@src/atoms/Table";
|
||||
import { useRowyRunModal } from "@src/atoms/RowyRunModal";
|
||||
|
||||
export default function Webhooks() {
|
||||
const { tableState, table, tableActions, rowyRun, compatibleRowyRunVersion } =
|
||||
@@ -40,7 +41,15 @@ export default function Webhooks() {
|
||||
index?: number;
|
||||
} | null>(null);
|
||||
|
||||
if (!compatibleRowyRunVersion?.({ minVersion: "1.2.0" })) return null;
|
||||
const openRowyRunModal = useRowyRunModal();
|
||||
if (!compatibleRowyRunVersion?.({ minVersion: "1.2.0" }))
|
||||
return (
|
||||
<TableHeaderButton
|
||||
title="Webhooks"
|
||||
onClick={() => openRowyRunModal("Webhooks", "1.2.0")}
|
||||
icon={<WebhookIcon />}
|
||||
/>
|
||||
);
|
||||
|
||||
const edited = !_isEqual(currentWebhooks, localWebhooksObjects);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import Confirmation from "@src/components/Confirmation";
|
||||
import { Table } from "@src/contexts/ProjectContext";
|
||||
import { routes } from "@src/constants/routes";
|
||||
import { db } from "@src/firebase";
|
||||
import { name } from "@root/package.json";
|
||||
import {
|
||||
SETTINGS,
|
||||
TABLE_SCHEMAS,
|
||||
@@ -106,7 +105,7 @@ export default function DeleteMenu({ clearDialog, data }: IDeleteMenuProps) {
|
||||
body: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
This will only delete the {name} configuration data.
|
||||
This will only delete the Rowy configuration data.
|
||||
</DialogContentText>
|
||||
<DialogContentText>
|
||||
You will not lose any data in your Firestore collection{" "}
|
||||
|
||||
@@ -2,14 +2,12 @@ import _find from "lodash/find";
|
||||
import { Field, FieldType } from "@rowy/form-builder";
|
||||
import { TableSettingsDialogModes } from "./index";
|
||||
|
||||
import { Link, Typography } from "@mui/material";
|
||||
import { Link, ListItemText, Typography } from "@mui/material";
|
||||
import OpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import WarningIcon from "@mui/icons-material/WarningAmber";
|
||||
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { name } from "@root/package.json";
|
||||
import { FieldType as TableFieldType } from "@src/constants/fields";
|
||||
import InputAdornment from "@mui/material/InputAdornment";
|
||||
|
||||
export const tableSettings = (
|
||||
mode: TableSettingsDialogModes | null,
|
||||
@@ -18,7 +16,7 @@ export const tableSettings = (
|
||||
tables:
|
||||
| { label: string; value: any; section: string; collection: string }[]
|
||||
| undefined,
|
||||
collections: string[]
|
||||
collections: string[] | null
|
||||
): Field[] =>
|
||||
[
|
||||
// Step 1: Collection
|
||||
@@ -31,43 +29,32 @@ export const tableSettings = (
|
||||
options: [
|
||||
{
|
||||
label: (
|
||||
<div>
|
||||
Primary collection
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
display="block"
|
||||
sx={{
|
||||
width: 470,
|
||||
whiteSpace: "normal",
|
||||
".MuiSelect-select &": { display: "none" },
|
||||
}}
|
||||
>
|
||||
Connect this table to the <b>single collection</b> matching the
|
||||
collection name entered below
|
||||
</Typography>
|
||||
</div>
|
||||
<ListItemText
|
||||
primary="Primary collection"
|
||||
secondary={
|
||||
<>
|
||||
Connect this table to the <b>single collection</b> matching
|
||||
the collection name entered below
|
||||
</>
|
||||
}
|
||||
style={{ maxWidth: 470 }}
|
||||
/>
|
||||
),
|
||||
value: "primaryCollection",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div>
|
||||
Collection group
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
display="block"
|
||||
sx={{
|
||||
width: 470,
|
||||
whiteSpace: "normal",
|
||||
".MuiSelect-select &": { display: "none" },
|
||||
}}
|
||||
>
|
||||
Connect this table to <b>all collections and subcollections</b>{" "}
|
||||
matching the collection name entered below
|
||||
</Typography>
|
||||
</div>
|
||||
<ListItemText
|
||||
primary="Collection group"
|
||||
secondary={
|
||||
<>
|
||||
Connect this table to{" "}
|
||||
<b>all collections and subcollections</b> matching the
|
||||
collection name entered below
|
||||
</>
|
||||
}
|
||||
style={{ maxWidth: 470 }}
|
||||
/>
|
||||
),
|
||||
value: "collectionGroup",
|
||||
},
|
||||
@@ -89,71 +76,120 @@ export const tableSettings = (
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
step: "collection",
|
||||
type: FieldType.singleSelect,
|
||||
name: "collection",
|
||||
label: "Collection",
|
||||
labelPlural: "collections",
|
||||
options: collections,
|
||||
itemRenderer: (option) => <code key={option.value}>{option.label}</code>,
|
||||
freeText: true,
|
||||
required: true,
|
||||
assistiveText: (
|
||||
<>
|
||||
{mode === TableSettingsDialogModes.update ? (
|
||||
Array.isArray(collections)
|
||||
? {
|
||||
step: "collection",
|
||||
type: FieldType.singleSelect,
|
||||
name: "collection",
|
||||
label: "Collection",
|
||||
labelPlural: "collections",
|
||||
options: collections,
|
||||
itemRenderer: (option) => (
|
||||
<code key={option.value}>{option.label}</code>
|
||||
),
|
||||
freeText: true,
|
||||
required: true,
|
||||
assistiveText: (
|
||||
<>
|
||||
<WarningIcon
|
||||
color="warning"
|
||||
aria-label="Warning"
|
||||
sx={{ fontSize: 16, mr: 0.5, verticalAlign: "middle" }}
|
||||
/>
|
||||
You can change which Firestore collection to display. Data in the
|
||||
new collection must be compatible with the existing columns.
|
||||
{mode === TableSettingsDialogModes.update ? (
|
||||
<>
|
||||
<WarningIcon
|
||||
color="warning"
|
||||
aria-label="Warning"
|
||||
sx={{ fontSize: 16, mr: 0.5, verticalAlign: "middle" }}
|
||||
/>
|
||||
You can change which Firestore collection to display. Data in
|
||||
the new collection must be compatible with the existing
|
||||
columns.
|
||||
</>
|
||||
) : (
|
||||
"Choose which Firestore collection to display."
|
||||
)}{" "}
|
||||
<Link
|
||||
href={`https://console.firebase.google.com/project/_/firestore/data`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Your collections
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
"Choose which Firestore collection to display."
|
||||
)}{" "}
|
||||
<Link
|
||||
href={`https://console.firebase.google.com/project/_/firestore/data`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Your collections
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
AddButtonProps: {
|
||||
children: "Create collection or use custom path…",
|
||||
},
|
||||
AddDialogProps: {
|
||||
title: "Create collection or use custom path",
|
||||
textFieldLabel: (
|
||||
<>
|
||||
Collection name
|
||||
<Typography variant="caption" display="block">
|
||||
If this collection does not exist, it won’t be created until you
|
||||
add a row to the table
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
TextFieldProps: {
|
||||
sx: { "& .MuiInputBase-input": { fontFamily: "mono" } },
|
||||
},
|
||||
// https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields
|
||||
validation: [
|
||||
["matches", /^[^\s]+$/, "Collection name cannot have spaces"],
|
||||
["notOneOf", [".", ".."], "Collection name cannot be . or .."],
|
||||
[
|
||||
"test",
|
||||
"double-underscore",
|
||||
"Collection name cannot begin and end with __",
|
||||
(value) => !value.startsWith("__") && !value.endsWith("__"),
|
||||
],
|
||||
],
|
||||
},
|
||||
),
|
||||
AddButtonProps: {
|
||||
children: "Create collection or use custom path…",
|
||||
},
|
||||
AddDialogProps: {
|
||||
title: "Create collection or use custom path",
|
||||
textFieldLabel: (
|
||||
<>
|
||||
Collection name
|
||||
<Typography variant="caption" display="block">
|
||||
If this collection does not exist, it won’t be created until
|
||||
you add a row to the table
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
TextFieldProps: {
|
||||
sx: { "& .MuiInputBase-input": { fontFamily: "mono" } },
|
||||
},
|
||||
// https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields
|
||||
validation: [
|
||||
["matches", /^[^\s]+$/, "Collection name cannot have spaces"],
|
||||
["notOneOf", [".", ".."], "Collection name cannot be . or .."],
|
||||
[
|
||||
"test",
|
||||
"double-underscore",
|
||||
"Collection name cannot begin and end with __",
|
||||
(value) => !value.startsWith("__") && !value.endsWith("__"),
|
||||
],
|
||||
],
|
||||
}
|
||||
: {
|
||||
step: "collection",
|
||||
type: FieldType.shortText,
|
||||
name: "collection",
|
||||
label: "Collection name",
|
||||
required: true,
|
||||
assistiveText: (
|
||||
<>
|
||||
{mode === TableSettingsDialogModes.update ? (
|
||||
<>
|
||||
<WarningIcon
|
||||
color="warning"
|
||||
aria-label="Warning"
|
||||
sx={{ fontSize: 16, mr: 0.5, verticalAlign: "middle" }}
|
||||
/>
|
||||
You can change which Firestore collection to display. Data in
|
||||
the new collection must be compatible with the existing
|
||||
columns.
|
||||
</>
|
||||
) : (
|
||||
"Type the name of the Firestore collection to display."
|
||||
)}{" "}
|
||||
<Link
|
||||
href={`https://console.firebase.google.com/project/_/firestore/data`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Your collections
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
sx: { "& .MuiInputBase-input": { fontFamily: "mono" } },
|
||||
// https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields
|
||||
validation: [
|
||||
["matches", /^[^\s]+$/, "Collection name cannot have spaces"],
|
||||
["notOneOf", [".", ".."], "Collection name cannot be . or .."],
|
||||
[
|
||||
"test",
|
||||
"double-underscore",
|
||||
"Collection name cannot begin and end with __",
|
||||
(value) => !value.startsWith("__") && !value.endsWith("__"),
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
// Step 2: Display
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function TableSettings({
|
||||
const { data: collections } = useSWR(
|
||||
"firebaseCollections",
|
||||
() => rowyRun?.({ route: runRoutes.listCollections }),
|
||||
{ fallbackData: [], revalidateIfStale: false, dedupingInterval: 60_000 }
|
||||
{ revalidateIfStale: false, dedupingInterval: 60_000 }
|
||||
);
|
||||
|
||||
const open = mode !== null;
|
||||
@@ -166,7 +166,7 @@ export default function TableSettings({
|
||||
})),
|
||||
["section", "label"]
|
||||
),
|
||||
Array.isArray(collections) ? collections.filter((x) => x !== CONFIG) : []
|
||||
Array.isArray(collections) ? collections.filter((x) => x !== CONFIG) : null
|
||||
);
|
||||
const customComponents = {
|
||||
tableId: {
|
||||
|
||||
@@ -88,23 +88,31 @@ export default function ActionFab({
|
||||
};
|
||||
|
||||
const handleRun = async (actionParams = null) => {
|
||||
setIsRunning(true);
|
||||
const data = fnParams(actionParams);
|
||||
let result;
|
||||
try {
|
||||
setIsRunning(true);
|
||||
const data = fnParams(actionParams);
|
||||
let result;
|
||||
|
||||
if (callableName === "actionScript") {
|
||||
result = await handleActionScript(data);
|
||||
} else {
|
||||
result = await handleCallableAction(data);
|
||||
}
|
||||
const { message, success } = result;
|
||||
setIsRunning(false);
|
||||
enqueueSnackbar(
|
||||
typeof message === "string" ? message : JSON.stringify(message),
|
||||
{
|
||||
variant: success ? "success" : "error",
|
||||
if (callableName === "actionScript") {
|
||||
result = await handleActionScript(data);
|
||||
} else {
|
||||
result = await handleCallableAction(data);
|
||||
}
|
||||
);
|
||||
const { message, success } = result ?? {};
|
||||
enqueueSnackbar(
|
||||
typeof message === "string" ? message : JSON.stringify(message),
|
||||
{
|
||||
variant: success ? "success" : "error",
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
enqueueSnackbar(`Failed to run action. Check the column settings.`, {
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const needsParams =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import _get from "lodash/get";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Stack,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
Radio,
|
||||
Typography,
|
||||
InputLabel,
|
||||
Link,
|
||||
Link as MuiLink,
|
||||
Checkbox,
|
||||
FormHelperText,
|
||||
Fab,
|
||||
@@ -32,6 +33,7 @@ import FormFieldSnippets from "./FormFieldSnippets";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import actionDefs from "!!raw-loader!./action.d.ts";
|
||||
import { RUN_ACTION_TEMPLATE, UNDO_ACTION_TEMPLATE } from "./templates";
|
||||
@@ -42,13 +44,15 @@ const diagnosticsOptions = {
|
||||
noSuggestionDiagnostics: true,
|
||||
};
|
||||
|
||||
import { routes } from "constants/routes";
|
||||
|
||||
const CodeEditor = lazy(
|
||||
() =>
|
||||
import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */)
|
||||
);
|
||||
|
||||
const Settings = ({ config, onChange }) => {
|
||||
const { tableState, roles, compatibleRowyRunVersion } = useProjectContext();
|
||||
const { tableState, roles,settings, compatibleRowyRunVersion } = useProjectContext();
|
||||
const { projectId } = useAppContext();
|
||||
const [activeStep, setActiveStep] = useState<
|
||||
"requirements" | "friction" | "action" | "undo" | "customization"
|
||||
@@ -290,17 +294,19 @@ const Settings = ({ config, onChange }) => {
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Write JavaScript code below that will be executed by
|
||||
Rowy Run.{" "}
|
||||
<Link
|
||||
href={WIKI_LINKS.rowyRun}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Requires Rowy Run setup
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>
|
||||
{!settings?.rowyRunUrl && (
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={routes.projectSettings + "#rowyRun"}
|
||||
color="error"
|
||||
>
|
||||
Requires Rowy Run setup →
|
||||
</MuiLink>
|
||||
)}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
disabled={!settings?.rowyRunUrl}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="cloudFunction"
|
||||
@@ -310,14 +316,14 @@ const Settings = ({ config, onChange }) => {
|
||||
<Typography variant="inherit">Callable</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
A{" "}
|
||||
<Link
|
||||
<MuiLink
|
||||
href="https://firebase.google.com/docs/functions/callable"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
callable function
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>{" "}
|
||||
</MuiLink>{" "}
|
||||
you’ve deployed on your Firestore or Google Cloud
|
||||
project
|
||||
</Typography>
|
||||
@@ -339,25 +345,25 @@ const Settings = ({ config, onChange }) => {
|
||||
<>
|
||||
Write the name of the callable function you’ve deployed to
|
||||
your project.{" "}
|
||||
<Link
|
||||
<MuiLink
|
||||
href={`https://console.firebase.google.com/project/${projectId}/functions/list`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View your callable functions
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>
|
||||
</MuiLink>
|
||||
<br />
|
||||
Your callable function must be compatible with Rowy Action
|
||||
columns.{" "}
|
||||
<Link
|
||||
<MuiLink
|
||||
href={WIKI_LINKS.fieldTypesAction + "#callable"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View requirements
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>
|
||||
</MuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -19,9 +19,16 @@ import { db } from "@src/firebase";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { TABLE_SCHEMAS } from "@src/config/dbPaths";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { useRowyRunModal } from "@src/atoms/RowyRunModal";
|
||||
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
const { tables } = useProjectContext();
|
||||
const { tables, settings } = useProjectContext();
|
||||
|
||||
const openRowyRunModal = useRowyRunModal();
|
||||
useEffect(() => {
|
||||
if (!settings?.rowyRunUrl) openRowyRunModal("Connect Table fields");
|
||||
}, [settings?.rowyRunUrl]);
|
||||
|
||||
const tableOptions = _sortBy(
|
||||
tables?.map((table) => ({
|
||||
label: table.name,
|
||||
|
||||
@@ -30,7 +30,7 @@ export const config: IFieldConfig = {
|
||||
initialValue: [],
|
||||
icon: <ConnectTableIcon />,
|
||||
description:
|
||||
"Connects to an existing table to fetch a snapshot of values from a row. Requires Algolia setup.",
|
||||
"Connects to an existing table to fetch a snapshot of values from a row. Requires Rowy Run and Algolia setup.",
|
||||
TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, {
|
||||
anchorOrigin: { horizontal: "left", vertical: "bottom" },
|
||||
transparent: true,
|
||||
@@ -38,5 +38,6 @@ export const config: IFieldConfig = {
|
||||
TableEditor: NullEditor as any,
|
||||
SideDrawerField,
|
||||
settings: Settings,
|
||||
requireConfiguration: true,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { lazy, Suspense, useEffect } from "react";
|
||||
import { ISettingsProps } from "../types";
|
||||
|
||||
import { Grid, InputLabel, FormHelperText } from "@mui/material";
|
||||
@@ -10,6 +10,7 @@ import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { useRowyRunModal } from "@src/atoms/RowyRunModal";
|
||||
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
@@ -33,7 +34,14 @@ export default function Settings({
|
||||
onBlur,
|
||||
errors,
|
||||
}: ISettingsProps) {
|
||||
const { tableState, compatibleRowyRunVersion } = useProjectContext();
|
||||
|
||||
const { tableState, compatibleRowyRunVersion ,settings } = useProjectContext();
|
||||
const openRowyRunModal = useRowyRunModal();
|
||||
useEffect(() => {
|
||||
if (!settings?.rowyRunUrl) openRowyRunModal("Derivative fields");
|
||||
}, [settings?.rowyRunUrl]);
|
||||
|
||||
if (!tableState?.columns) return null;
|
||||
|
||||
if (!tableState?.columns) return <></>;
|
||||
const columns = Object.values(tableState.columns);
|
||||
|
||||
@@ -14,14 +14,14 @@ export const config: IFieldConfig = {
|
||||
initialValue: "",
|
||||
initializable: true,
|
||||
icon: <DerivativeIcon />,
|
||||
requireConfiguration: true,
|
||||
description:
|
||||
"Value derived from the rest of the row’s values. Displayed using any other field type. Requires Cloud Function set up.",
|
||||
"Value derived from the rest of the row’s values. Displayed using any other field type. Requires Rowy Run set up.",
|
||||
TableCell: withBasicCell(BasicCell),
|
||||
TableEditor: NullEditor as any,
|
||||
SideDrawerField: BasicCell as any,
|
||||
contextMenuActions: ContextMenuActions,
|
||||
settings: Settings,
|
||||
settingsValidator,
|
||||
requireConfiguration: true,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -367,6 +367,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
auditChange("DELETE_ROW", rowId, {})
|
||||
);
|
||||
};
|
||||
|
||||
// rowyRun access
|
||||
const _rowyRun: IProjectContext["rowyRun"] = async (args) => {
|
||||
const { service, ...rest } = args;
|
||||
@@ -382,20 +383,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
...rest,
|
||||
});
|
||||
} else {
|
||||
enqueueSnackbar(`Rowy Run${service ? ` ${service}` : ""} is not set up`, {
|
||||
variant: "error",
|
||||
action: (
|
||||
<Button
|
||||
href={WIKI_LINKS.rowyRun}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Docs
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
return { success: false, error: "rowyRun is not setup" };
|
||||
console.log("Rowy Run is not set up", args);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect } from "react";
|
||||
import { name } from "@root/package.json";
|
||||
|
||||
export default function useDocumentTitle(projectId: string, title?: string) {
|
||||
useEffect(() => {
|
||||
document.title = [
|
||||
title,
|
||||
projectId,
|
||||
name + (window.location.hostname === "localhost" ? " (localhost)" : ""),
|
||||
"Rowy",
|
||||
window.location.hostname === "localhost" ? "localhost" : "",
|
||||
]
|
||||
.filter((x) => x)
|
||||
.join(" • ");
|
||||
@@ -14,7 +14,8 @@ export default function useDocumentTitle(projectId: string, title?: string) {
|
||||
return () => {
|
||||
document.title = [
|
||||
projectId,
|
||||
name + (window.location.hostname === "localhost" ? " (localhost)" : ""),
|
||||
"Rowy",
|
||||
window.location.hostname === "localhost" ? "localhost" : "",
|
||||
]
|
||||
.filter((x) => x)
|
||||
.join(" • ");
|
||||
|
||||
@@ -75,11 +75,14 @@ export default function useUpdateCheck() {
|
||||
|
||||
// Only store the latest release
|
||||
if (compare(resRowy.tag_name, version, ">")) newState.rowy = resRowy;
|
||||
if (compare(resRowyRun.tag_name, deployedRowyRun.version, ">"))
|
||||
if (
|
||||
deployedRowyRun &&
|
||||
compare(resRowyRun.tag_name, deployedRowyRun.version, ">")
|
||||
)
|
||||
newState.rowyRun = resRowyRun;
|
||||
|
||||
// Save deployed version
|
||||
newState.deployedRowyRun = deployedRowyRun.version;
|
||||
newState.deployedRowyRun = deployedRowyRun?.version ?? "";
|
||||
|
||||
setLatestUpdate(newState);
|
||||
setLoading(false);
|
||||
|
||||
@@ -10,7 +10,6 @@ import { signOut } from "@src/utils/auth";
|
||||
import { auth } from "../../firebase";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { name } from "@root/package.json";
|
||||
|
||||
export default function ImpersonatorAuthPage() {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
@@ -58,7 +57,7 @@ export default function ImpersonatorAuthPage() {
|
||||
test permissions and access controls.
|
||||
</Typography>
|
||||
<Typography variant="inherit" component="span">
|
||||
Make sure the {name} Run service account has the{" "}
|
||||
Make sure the Rowy Run service account has the{" "}
|
||||
<b>Service Account Token Creator</b> IAM role.
|
||||
</Typography>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,6 @@ import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import AuthLayout from "@src/components/Auth/AuthLayout";
|
||||
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { name } from "@root/package.json";
|
||||
|
||||
export default function AuthSetupGuide() {
|
||||
return (
|
||||
@@ -12,7 +11,7 @@ export default function AuthSetupGuide() {
|
||||
title="Set up Firebase Authentication"
|
||||
description={
|
||||
<>
|
||||
To sign in to {name}, first set up Firebase Authentication in the
|
||||
To sign in to Rowy, first set up Firebase Authentication in the
|
||||
Firebase Console.
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import MarketingBanner from "@src/components/Auth/MarketingBanner";
|
||||
import AuthLayout from "@src/components/Auth/AuthLayout";
|
||||
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { name } from "@root/package.json";
|
||||
|
||||
export default function DeployPage() {
|
||||
const { search } = useLocation();
|
||||
@@ -51,7 +50,7 @@ export default function DeployPage() {
|
||||
title="Get started"
|
||||
description={
|
||||
<>
|
||||
Set up {name} on your Google Cloud or Firebase project with a
|
||||
Set up Rowy on your Google Cloud or Firebase project with a
|
||||
one-click deploy button.
|
||||
<br />
|
||||
<br />
|
||||
@@ -148,7 +147,7 @@ export default function DeployPage() {
|
||||
</div>
|
||||
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
By setting up {name}, you agree to our{" "}
|
||||
By setting up Rowy, you agree to our{" "}
|
||||
<Link
|
||||
href={EXTERNAL_LINKS.terms}
|
||||
target="_blank"
|
||||
|
||||
@@ -14,7 +14,6 @@ import Customization from "@src/components/Settings/ProjectSettings/Customizatio
|
||||
import { SETTINGS, PUBLIC_SETTINGS } from "@src/config/dbPaths";
|
||||
import useDoc from "@src/hooks/useDoc";
|
||||
import { db } from "@src/firebase";
|
||||
import { name } from "@root/package.json";
|
||||
|
||||
export interface IProjectSettingsChildProps {
|
||||
settings: Record<string, any>;
|
||||
@@ -68,7 +67,7 @@ export default function ProjectSettingsPage() {
|
||||
|
||||
const sections = [
|
||||
{ title: "About", Component: About },
|
||||
{ title: `${name} Run`, Component: RowyRun, props: childProps },
|
||||
{ title: `Rowy Run`, Component: RowyRun, props: childProps },
|
||||
{ title: "Authentication", Component: Authentication, props: childProps },
|
||||
{ title: "Customization", Component: Customization, props: childProps },
|
||||
];
|
||||
|
||||
@@ -1,426 +1,25 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { use100vh } from "react-div-100vh";
|
||||
import { SwitchTransition } from "react-transition-group";
|
||||
import { useLocation, Link } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
useMediaQuery,
|
||||
Paper,
|
||||
Stepper,
|
||||
Step,
|
||||
StepButton,
|
||||
MobileStepper,
|
||||
IconButton,
|
||||
Typography,
|
||||
Stack,
|
||||
DialogActions,
|
||||
Button,
|
||||
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";
|
||||
import SetupLayout from "@src/components/Setup/SetupLayout";
|
||||
import StepWelcome from "@src/components/Setup/Steps/StepWelcome";
|
||||
import StepRules from "@src/components/Setup/Steps/StepRules";
|
||||
import StepStorageRules from "@src/components/Setup/Steps/StepStorageRules";
|
||||
import StepFinish from "@src/components/Setup/Steps/StepFinish";
|
||||
|
||||
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 Step0Welcome from "@src/components/Setup/Step0Welcome";
|
||||
import Step1RowyRun, { checkRowyRun } from "@src/components/Setup/Step1RowyRun";
|
||||
// prettier-ignore
|
||||
// prettier-ignore
|
||||
import Step2ProjectOwner, { checkProjectOwner } from "@src/components/Setup/Step2ProjectOwner";
|
||||
import Step3Rules, { checkRules } from "@src/components/Setup/Step3Rules";
|
||||
import Step4Migrate, { checkMigrate } from "@src/components/Setup/Step4Migrate";
|
||||
import Step5Finish from "@src/components/Setup/Step6Finish";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import routes from "@src/constants/routes";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { analytics } from "analytics";
|
||||
|
||||
export interface ISetupStep {
|
||||
id: string;
|
||||
layout?: "centered";
|
||||
shortTitle: string;
|
||||
title: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
body: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ISetupStepBodyProps {
|
||||
completion: Record<string, boolean>;
|
||||
setCompletion: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
||||
checkAllSteps: typeof checkAllSteps;
|
||||
rowyRunUrl: string;
|
||||
}
|
||||
|
||||
const BASE_WIDTH = 1024;
|
||||
|
||||
const checkAllSteps = async (
|
||||
rowyRunUrl: string,
|
||||
currentUser: firebase.default.User | null | undefined,
|
||||
userRoles: string[] | null,
|
||||
authToken: string,
|
||||
signal: AbortSignal
|
||||
) => {
|
||||
console.log("Check all steps");
|
||||
const completion: Record<string, boolean> = {};
|
||||
|
||||
const rowyRunValidation = await checkRowyRun(rowyRunUrl, signal);
|
||||
if (rowyRunValidation.isValidRowyRunUrl) {
|
||||
if (rowyRunValidation.isLatestVersion) completion.rowyRun = true;
|
||||
|
||||
const promises = [
|
||||
checkProjectOwner(rowyRunUrl, currentUser, userRoles, signal).then(
|
||||
(projectOwner) => {
|
||||
if (projectOwner) completion.projectOwner = true;
|
||||
}
|
||||
),
|
||||
checkRules(rowyRunUrl, authToken, signal).then((rules) => {
|
||||
if (rules) completion.rules = true;
|
||||
}),
|
||||
checkMigrate(rowyRunUrl, authToken, signal).then((requiresMigration) => {
|
||||
if (requiresMigration) completion.migrate = false;
|
||||
}),
|
||||
];
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
return completion;
|
||||
};
|
||||
const steps = [StepWelcome, StepRules, StepStorageRules, StepFinish];
|
||||
|
||||
export default function SetupPage() {
|
||||
const { currentUser, userRoles, getAuthToken } = useAppContext();
|
||||
|
||||
const fullScreenHeight = use100vh() ?? 0;
|
||||
const isMobile = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
|
||||
|
||||
const { search } = useLocation();
|
||||
const params = queryString.parse(search);
|
||||
const rowyRunUrl = decodeURIComponent((params.rowyRunUrl as string) || "");
|
||||
|
||||
const [stepId, setStepId] = useState("welcome");
|
||||
const [completion, setCompletion] = useState<Record<string, boolean>>({
|
||||
welcome: false,
|
||||
rowyRun: false,
|
||||
serviceAccount: false,
|
||||
projectOwner: false,
|
||||
rules: false,
|
||||
storageRules: false,
|
||||
});
|
||||
|
||||
const [checkingAllSteps, setCheckingAllSteps] = useState(false);
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
if (rowyRunUrl) {
|
||||
setCheckingAllSteps(true);
|
||||
getAuthToken().then((authToken) =>
|
||||
checkAllSteps(
|
||||
rowyRunUrl,
|
||||
currentUser,
|
||||
userRoles,
|
||||
authToken,
|
||||
signal
|
||||
).then((result) => {
|
||||
if (!signal.aborted) {
|
||||
setCompletion((c) => ({ ...c, ...result }));
|
||||
setCheckingAllSteps(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return () => controller.abort();
|
||||
}, [rowyRunUrl, currentUser, userRoles, getAuthToken]);
|
||||
|
||||
const stepProps = { completion, setCompletion, checkAllSteps, rowyRunUrl };
|
||||
|
||||
const steps: ISetupStep[] = [
|
||||
{
|
||||
id: "welcome",
|
||||
layout: "centered" as "centered",
|
||||
shortTitle: "Welcome",
|
||||
title: `Welcome to ${name}`,
|
||||
body: <Step0Welcome {...stepProps} />,
|
||||
actions: completion.welcome ? (
|
||||
<LoadingButton
|
||||
loading={checkingAllSteps}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Get started
|
||||
</LoadingButton>
|
||||
) : (
|
||||
<Tooltip title="Please accept the terms and conditions">
|
||||
<div>
|
||||
<LoadingButton
|
||||
loading={checkingAllSteps}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled
|
||||
>
|
||||
Get started
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "rowyRun",
|
||||
shortTitle: `${name} Run`,
|
||||
title: `Set up ${name} Run`,
|
||||
body: <Step1RowyRun {...stepProps} />,
|
||||
},
|
||||
{
|
||||
id: "projectOwner",
|
||||
shortTitle: `Project owner`,
|
||||
title: `Set up project owner`,
|
||||
body: <Step2ProjectOwner {...stepProps} />,
|
||||
},
|
||||
{
|
||||
id: "rules",
|
||||
shortTitle: `Rules`,
|
||||
title: `Set up Firestore Rules`,
|
||||
body: <Step3Rules {...stepProps} />,
|
||||
},
|
||||
completion.migrate !== undefined
|
||||
? {
|
||||
id: "migrate",
|
||||
shortTitle: `Migrate`,
|
||||
title: `Migrate to ${name} (optional)`,
|
||||
body: <Step4Migrate {...stepProps} />,
|
||||
}
|
||||
: ({} as ISetupStep),
|
||||
{
|
||||
id: "finish",
|
||||
layout: "centered" as "centered",
|
||||
shortTitle: `Finish`,
|
||||
title: `You’re all set up!`,
|
||||
body: <Step5Finish />,
|
||||
actions: (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component={Link}
|
||||
to={routes.home}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
Continue to {name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
].filter((x) => x.id);
|
||||
|
||||
const step =
|
||||
steps.find((step) => step.id === (stepId || steps[0].id)) ?? steps[0];
|
||||
const stepIndex = steps.findIndex(
|
||||
(step) => step.id === (stepId || steps[0].id)
|
||||
);
|
||||
const listedSteps = steps.filter((step) => step.layout !== "centered");
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<BrandedBackground />
|
||||
<Paper
|
||||
component="main"
|
||||
elevation={4}
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
alpha(theme.palette.background.paper, 0.5),
|
||||
backdropFilter: "blur(20px) saturate(150%)",
|
||||
|
||||
maxWidth: BASE_WIDTH,
|
||||
width: (theme) => `calc(100vw - ${theme.spacing(2)})`,
|
||||
maxHeight: (theme) =>
|
||||
`calc(${
|
||||
fullScreenHeight > 0 ? `${fullScreenHeight}px` : "100vh"
|
||||
} - ${theme.spacing(
|
||||
2
|
||||
)} - env(safe-area-inset-top) - env(safe-area-inset-bottom))`,
|
||||
height: BASE_WIDTH * 0.75,
|
||||
resize: "both",
|
||||
|
||||
p: 0,
|
||||
"& > *, & > .MuiDialogContent-root": { px: { xs: 2, sm: 4 } },
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
"& .MuiTypography-inherit, & .MuiDialogContent-root": {
|
||||
typography: "body1",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{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">
|
||||
{step.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}>{step.body}</Stack>
|
||||
</ScrollableDialogContent>
|
||||
</SlideTransition>
|
||||
</SwitchTransition>
|
||||
</>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
handleContinue();
|
||||
} catch (e: any) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<DialogActions>
|
||||
{step.actions ?? (
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
loading={checkingAllSteps}
|
||||
disabled={!completion[stepId]}
|
||||
>
|
||||
Continue
|
||||
</LoadingButton>
|
||||
)}
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Paper>
|
||||
</Wrapper>
|
||||
<SetupLayout
|
||||
steps={steps}
|
||||
completion={completion}
|
||||
setCompletion={setCompletion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -405,6 +405,22 @@ export const components = (theme: Theme): ThemeOptions => {
|
||||
MuiListItem: {
|
||||
defaultProps: { dense: true },
|
||||
},
|
||||
MuiListItemText: {
|
||||
defaultProps: {
|
||||
secondaryTypographyProps: { variant: "caption" },
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
".MuiMenu-list &": { whiteSpace: "normal" },
|
||||
},
|
||||
primary: {
|
||||
".MuiSelect-select &": theme.typography.body2,
|
||||
},
|
||||
secondary: {
|
||||
".MuiSelect-select &": { display: "none" },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenu: {
|
||||
styleOverrides: {
|
||||
list: { padding: theme.spacing(0.5, 0) },
|
||||
@@ -1008,9 +1024,10 @@ export const components = (theme: Theme): ThemeOptions => {
|
||||
},
|
||||
},
|
||||
|
||||
"&:hover .MuiCheckbox-root, &:hover .MuiRadio-root": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
"&:hover .MuiCheckbox-root:not(.Mui-disabled), &:hover .MuiRadio-root:not(.Mui-disabled)":
|
||||
{
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
label: {
|
||||
marginTop: 10,
|
||||
|
||||
174
yarn.lock
174
yarn.lock
@@ -1654,10 +1654,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa"
|
||||
integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==
|
||||
|
||||
"@date-io/core@^2.11.0":
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.11.0.tgz#28580cda1c8228ab2c7ed6aee673ef0495f913e6"
|
||||
integrity sha512-DvPBnNoeuLaoSJZaxgpu54qzRhRKjSYVyQjhznTFrllKuDpm0sDFjHo6lvNLCM/cfMx2gb2PM2zY2kc9C8nmuw==
|
||||
"@date-io/core@^2.13.1":
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.13.1.tgz#f041765aff5c55fbc7e37fdd75fc1792733426d6"
|
||||
integrity sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg==
|
||||
|
||||
"@date-io/date-fns@1.x":
|
||||
version "1.3.13"
|
||||
@@ -1666,33 +1666,33 @@
|
||||
dependencies:
|
||||
"@date-io/core" "^1.3.13"
|
||||
|
||||
"@date-io/date-fns@^2.11.0":
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.11.0.tgz#142fbf954eda7ad66514af7a2802d78c4ea40053"
|
||||
integrity sha512-mPQ71plBeFrArvBSHtjWMHXA89IUbZ6kuo2dsjlRC/1uNOybo91spIb+wTu03NxKTl8ut07s0jJ9svF71afpRg==
|
||||
"@date-io/date-fns@^2.13.1":
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.13.1.tgz#19d8a245dab61c03c95ba492d679d98d2b0b4af5"
|
||||
integrity sha512-8fmfwjiLMpFLD+t4NBwDx0eblWnNcgt4NgfT/uiiQTGI81fnPu9tpBMYdAcuWxaV7LLpXgzLBx1SYWAMDVUDQQ==
|
||||
dependencies:
|
||||
"@date-io/core" "^2.11.0"
|
||||
"@date-io/core" "^2.13.1"
|
||||
|
||||
"@date-io/dayjs@^2.11.0":
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.11.0.tgz#41f4b4b9629612e6012accffd848875d1aeffb74"
|
||||
integrity sha512-w67vRK56NZJIKhJM/CrNbfnIcuMvR3ApfxzNZiCZ5w29sxgBDeKuX4M+P7A9r5HXOMGcsOcpgaoTDINNGkdpGQ==
|
||||
"@date-io/dayjs@^2.13.1":
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.13.1.tgz#98461d22ee98179b9f2dca3b36f1b618704ae593"
|
||||
integrity sha512-5bL4WWWmlI4uGZVScANhHJV7Mjp93ec2gNeUHDqqLaMZhp51S0NgD25oqj/k0LqBn1cdU2MvzNpk/ObMmVv5cQ==
|
||||
dependencies:
|
||||
"@date-io/core" "^2.11.0"
|
||||
"@date-io/core" "^2.13.1"
|
||||
|
||||
"@date-io/luxon@^2.11.1":
|
||||
version "2.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.11.1.tgz#31a72f7b5e163c74e8a3b29d8f16c4c30de6ed43"
|
||||
integrity sha512-JUXo01kdPQxLORxqdENrgdUhooKgDUggsNRSdi2BcUhASIY2KGwwWXu8ikVHHGkw+DUF4FOEKGfkQd0RHSvX6g==
|
||||
"@date-io/luxon@^2.13.1":
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.13.1.tgz#3701b3cabfffda5102af302979aa6e58acfda91a"
|
||||
integrity sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg==
|
||||
dependencies:
|
||||
"@date-io/core" "^2.11.0"
|
||||
"@date-io/core" "^2.13.1"
|
||||
|
||||
"@date-io/moment@^2.11.0":
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.11.0.tgz#850f8dd090d401845b39276d034dbabe20224ef5"
|
||||
integrity sha512-QSL+83qezQ9Ty0dtFgAkk6eC0GMl/lgYfDajeVUDB3zVA2A038hzczRLBg29ifnBGhQMPABxuOafgWwhDjlarg==
|
||||
"@date-io/moment@^2.13.1":
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.13.1.tgz#122a51e4bdedf71ff3babb264427737dc022c1e6"
|
||||
integrity sha512-XX1X/Tlvl3TdqQy2j0ZUtEJV6Rl8tOyc5WOS3ki52He28Uzme4Ro/JuPWTMBDH63weSWIZDlbR7zBgp3ZA2y1A==
|
||||
dependencies:
|
||||
"@date-io/core" "^2.11.0"
|
||||
"@date-io/core" "^2.13.1"
|
||||
|
||||
"@emotion/babel-plugin@^11.3.0":
|
||||
version "11.3.0"
|
||||
@@ -2429,55 +2429,55 @@
|
||||
"@monaco-editor/loader" "^1.2.0"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/base@5.0.0-alpha.68":
|
||||
version "5.0.0-alpha.68"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.68.tgz#d93d77e662bc8dce47c9415fc6cbcac6658efab7"
|
||||
integrity sha512-q+3gX6EHuM/AyOn8fkoANQxSzIHBeuNsrGgb7SPP0y7NuM+4ZHG/b9882+OfHcilaSqPDWUQoLbphcBpw/m/RA==
|
||||
"@mui/base@5.0.0-alpha.69":
|
||||
version "5.0.0-alpha.69"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.69.tgz#8511198d760de0795870f5ec63e53db73ba801ec"
|
||||
integrity sha512-IxUUj/lkilCTNBIybQxyQGW/zpxFp490G0QBQJgRp9TJkW2PWSTLvAH7gcH0YHd0L2TAf1TRgfdemoRseMzqQA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.0"
|
||||
"@emotion/is-prop-valid" "^1.1.1"
|
||||
"@mui/utils" "^5.4.1"
|
||||
"@mui/utils" "^5.4.2"
|
||||
"@popperjs/core" "^2.4.4"
|
||||
clsx "^1.1.1"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
|
||||
"@mui/icons-material@^5.4.1":
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.4.1.tgz#20901e9a09154355b7a832180a90717938c675c4"
|
||||
integrity sha512-koiq9q2GfjXRUWcC5fEi1b+EA4vfJHgIaAdBHlkOrBx2cnmmazQcyib501eodPfaZGx9BikrhivODaNQYQq8hA==
|
||||
"@mui/icons-material@^5.4.2":
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.4.2.tgz#b2fd2c6c81d2d275e17ce40bd50c63cb197d324b"
|
||||
integrity sha512-7c+G3jBT+e+pN0a9DJ0Bd8Kr1Vy6os5Q1yd2aXcwuhlRI3uzJBLJ8sX6FSWoh5DSEBchb7Bsk1uHz6U0YN9l+Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.0"
|
||||
|
||||
"@mui/lab@^5.0.0-alpha.68":
|
||||
version "5.0.0-alpha.68"
|
||||
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.68.tgz#a5034a8f749f3f4f0a1b8613515bed4054ade3e6"
|
||||
integrity sha512-wvszkLsgXgl3kMPVpHNm9pRYld9/2r0MYRlJUEh2GWwjBPE3dDTOIF2IHgZ3WqRBnJMitzUVt7v5Lu9/grjrIQ==
|
||||
"@mui/lab@^5.0.0-alpha.69":
|
||||
version "5.0.0-alpha.69"
|
||||
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.69.tgz#c1130e6ed5edc579c5d88a762e79935b01181cb1"
|
||||
integrity sha512-VrTcmXTS9UlTsp40nIZ/R/HBHOvxP2lvgSY9zLSn5XPhMQEMD1H0wTJ68mEuCm18cnl1sbcUOCMfWx9io/u5zg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.0"
|
||||
"@date-io/date-fns" "^2.11.0"
|
||||
"@date-io/dayjs" "^2.11.0"
|
||||
"@date-io/luxon" "^2.11.1"
|
||||
"@date-io/moment" "^2.11.0"
|
||||
"@mui/base" "5.0.0-alpha.68"
|
||||
"@mui/system" "^5.4.1"
|
||||
"@mui/utils" "^5.4.1"
|
||||
"@date-io/date-fns" "^2.13.1"
|
||||
"@date-io/dayjs" "^2.13.1"
|
||||
"@date-io/luxon" "^2.13.1"
|
||||
"@date-io/moment" "^2.13.1"
|
||||
"@mui/base" "5.0.0-alpha.69"
|
||||
"@mui/system" "^5.4.2"
|
||||
"@mui/utils" "^5.4.2"
|
||||
clsx "^1.1.1"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
react-transition-group "^4.4.2"
|
||||
rifm "^0.12.1"
|
||||
|
||||
"@mui/material@^5.4.1":
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.4.1.tgz#05d3f726771c413dc430163d7c508edfcee04807"
|
||||
integrity sha512-SxAT43UAjFTBBpJrN+oGrv40xP1uCa5Z49NfHt3m93xYeFzbxKOk0V9IKU7zlUjbsaVQ0i+o24yF5GULZmynlA==
|
||||
"@mui/material@^5.4.2":
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.4.2.tgz#04ea6632d7ca600a2ae528f6f140ef0af9c01434"
|
||||
integrity sha512-jmeLWEO6AA6g7HErhI3MXVGaMZtqDZjDwcHCg24WY954wO38Xn0zJ53VfpFc44ZTJLV9Ejd7ci9fLlG/HmJCeg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.0"
|
||||
"@mui/base" "5.0.0-alpha.68"
|
||||
"@mui/system" "^5.4.1"
|
||||
"@mui/types" "^7.1.1"
|
||||
"@mui/utils" "^5.4.1"
|
||||
"@mui/base" "5.0.0-alpha.69"
|
||||
"@mui/system" "^5.4.2"
|
||||
"@mui/types" "^7.1.2"
|
||||
"@mui/utils" "^5.4.2"
|
||||
"@types/react-transition-group" "^4.4.4"
|
||||
clsx "^1.1.1"
|
||||
csstype "^3.0.10"
|
||||
@@ -2486,34 +2486,34 @@
|
||||
react-is "^17.0.2"
|
||||
react-transition-group "^4.4.2"
|
||||
|
||||
"@mui/private-theming@^5.4.1":
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.4.1.tgz#5fa6490f35e78781239f1944ae80a7006c5a7648"
|
||||
integrity sha512-Xbc4MXFZxv0A3hoc4TSDBhzjhstppKfc+gQcTMqqBZQP7KjnmxF+wO7rEPQuYRBihjCqQBdrHIGMLsKWrhkZkQ==
|
||||
"@mui/private-theming@^5.4.2":
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.4.2.tgz#f0a05f908456a2f7b87ccb6fc3b6e1faae9d89e6"
|
||||
integrity sha512-mlPDYYko4wIcwXjCPEmOWbNTT4DZ6h9YHdnRtQPnWM28+TRUHEo7SbydnnmVDQLRXUfaH4Y6XtEHIfBNPE/SLg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.0"
|
||||
"@mui/utils" "^5.4.1"
|
||||
"@mui/utils" "^5.4.2"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/styled-engine@^5.4.1":
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.4.1.tgz#1427738e71c087f7005547e17d4a59de75597850"
|
||||
integrity sha512-CFLNJkopRoAuShkgUZOTBVxdTlKu4w6L4kOwPi4r3QB2XXS6O5kyLHSsg9huUbtOYk5Dv5UZyUSc5pw4J7ezdg==
|
||||
"@mui/styled-engine@^5.4.2":
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.4.2.tgz#e04903e06bd49fd10072a44ff38e13f5481bb64d"
|
||||
integrity sha512-tz9p3aRtzXHKAg7x3BgP0hVQEoGKaxNCFxsJ+d/iqEHYvywWFSs6oxqYAvDHIRpvMlUZyPNoTrkcNnbdMmH/ng==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.0"
|
||||
"@emotion/cache" "^11.7.1"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/styles@^5.4.1":
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.4.1.tgz#994171da902267184fffa19896ee5bbb07d4d783"
|
||||
integrity sha512-ekw2NBC06re0H9SvCA1XgtFcghB8AQdGPXD3mjIz5ik+X+LvR+f2TeoCpJpkKp7UQdcNn6uuYi6BO6irTiQhdw==
|
||||
"@mui/styles@^5.4.2":
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.4.2.tgz#e0dadfc5de8255605f23c2f909f3669f0911bb88"
|
||||
integrity sha512-BX75fNHmRF51yove9dBkH28gpSFjClOPDEnUwLTghPYN913OsqViS/iuCd61dxzygtEEmmeYuWfQjxu/F6vF5g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.0"
|
||||
"@emotion/hash" "^0.8.0"
|
||||
"@mui/private-theming" "^5.4.1"
|
||||
"@mui/types" "^7.1.1"
|
||||
"@mui/utils" "^5.4.1"
|
||||
"@mui/private-theming" "^5.4.2"
|
||||
"@mui/types" "^7.1.2"
|
||||
"@mui/utils" "^5.4.2"
|
||||
clsx "^1.1.1"
|
||||
csstype "^3.0.10"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
@@ -2527,29 +2527,29 @@
|
||||
jss-plugin-vendor-prefixer "^10.8.2"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/system@^5.4.1":
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.4.1.tgz#cf253369fbf1d960c792f0ec068fa28af81be3d4"
|
||||
integrity sha512-07JBYf9iQdxIHZU8cFOLoxBnkQDUPLb7UBhNxo4998yEqpWFJ00WKgEVYBKvPl0X+MRU/20wqFz6yGIuCx4AeA==
|
||||
"@mui/system@^5.4.2":
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.4.2.tgz#8166e406ba4628950bd79cec8159de25d5aef162"
|
||||
integrity sha512-QegBVu6fxUNov1X9bWc1MZUTeV3A5g9PIpli7d0kzkGfq6JzrJWuPlhSPZ+6hlWmWky+bbAXhU65Qz8atWxDGw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.0"
|
||||
"@mui/private-theming" "^5.4.1"
|
||||
"@mui/styled-engine" "^5.4.1"
|
||||
"@mui/types" "^7.1.1"
|
||||
"@mui/utils" "^5.4.1"
|
||||
"@mui/private-theming" "^5.4.2"
|
||||
"@mui/styled-engine" "^5.4.2"
|
||||
"@mui/types" "^7.1.2"
|
||||
"@mui/utils" "^5.4.2"
|
||||
clsx "^1.1.1"
|
||||
csstype "^3.0.10"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/types@^7.1.1":
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.1.tgz#9cf159dc60a101ee336e6ec74193a4f5f97f6160"
|
||||
integrity sha512-33hbHFLCwenTpS+T4m4Cz7cQ/ng5g+IgtINkw1uDBVvi1oM83VNt/IGzWIQNPK8H2pr0WIfkmboD501bVdYsPw==
|
||||
"@mui/types@^7.1.2":
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.2.tgz#4f3678ae77a7a3efab73b6e040469cc6df2144ac"
|
||||
integrity sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ==
|
||||
|
||||
"@mui/utils@^5.4.1":
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.4.1.tgz#feb365ce9a4426587510f0943fd6d6e1889e06e6"
|
||||
integrity sha512-5HzM+ZjlQqbSp7UTOvLlhAjkWB+o9Z4NzO0W+yhZ1KnxITr+zr/MBzYmmQ3kyvhui8pyhgRDoTcVgwb+02ZUZA==
|
||||
"@mui/utils@^5.4.2":
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.4.2.tgz#3edda8f80de235418fff0424ee66e2a49793ec01"
|
||||
integrity sha512-646dBCC57MXTo/Gf3AnZSHRHznaTETQq5x7AWp5FRQ4jPeyT4WSs18cpJVwkV01cAHKh06pNQTIufIALIWCL5g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.0"
|
||||
"@types/prop-types" "^15.7.4"
|
||||
@@ -17352,9 +17352,9 @@ url-parse-lax@^3.0.0:
|
||||
prepend-http "^2.0.0"
|
||||
|
||||
url-parse@^1.4.3, url-parse@^1.5.1:
|
||||
version "1.5.7"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a"
|
||||
integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
|
||||
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
|
||||
dependencies:
|
||||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user