Merge branch 'develop' into feat/JSON-code-completion

This commit is contained in:
Shams
2022-03-06 14:57:54 +11:00
committed by GitHub
52 changed files with 1280 additions and 1671 deletions

View File

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

View File

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

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

View File

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

View File

@@ -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>
);
}

View File

@@ -24,7 +24,7 @@ type ExtensionContext = {
requiredFields: string[];
extensionBody: any;
};
utilFns: any;
RULES_UTILS: any;
};
// extension body definition

View File

@@ -1,7 +1,7 @@
/**
* utility functions
*/
declare namespace utilFns {
declare namespace RULES_UTILS {
/**
* Gets the secret defined in Google Cloud Secret
*/

View File

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

View 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&nbsp;Run
</Typography>
)}
</>
}
/>
);
}

View File

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

View File

@@ -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" }}
/>
&nbsp;
{name} Run is set up correctly
&nbsp; Rowy Run is set up correctly
</>
) : verified === false ? (
`${name} Run is not set up correctly`
`Rowy Run is not set up correctly`
) : (
" "
)

View File

@@ -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 />}

View File

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

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

View File

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

View File

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

View File

@@ -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 ? (
`Youre 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;
}
};

View File

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

View File

@@ -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 youve 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;
}
};

View File

@@ -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: "Youre 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>
</>
);
}

View 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"}
/>
</>
);
}

View 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"}
/>
</>
);
}

View File

@@ -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>
Youll be up and running in just a few minutes.
</Typography>
<Typography variant="body1" gutterBottom>
Configure your projects 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
View 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;
}

View File

@@ -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 wont 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;

View File

@@ -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 tables ${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>

View File

@@ -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 && (

View File

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

View File

@@ -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!);

View File

@@ -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);

View File

@@ -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{" "}

View File

@@ -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 wont 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 wont 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
{

View File

@@ -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: {

View File

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

View File

@@ -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&nbsp;
</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>{" "}
youve deployed on your Firestore or Google Cloud
project
</Typography>
@@ -339,25 +345,25 @@ const Settings = ({ config, onChange }) => {
<>
Write the name of the callable function youve 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>
</>
}
/>

View File

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

View File

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

View File

@@ -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);

View File

@@ -14,14 +14,14 @@ export const config: IFieldConfig = {
initialValue: "",
initializable: true,
icon: <DerivativeIcon />,
requireConfiguration: true,
description:
"Value derived from the rest of the rows values. Displayed using any other field type. Requires Cloud Function set up.",
"Value derived from the rest of the rows 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;

View File

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

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

View File

@@ -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);
}
};

View File

@@ -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(" • ");

View File

@@ -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);

View File

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

View File

@@ -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.
</>
}

View File

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

View File

@@ -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 },
];

View File

@@ -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: `Youre 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}
/>
);
}

View File

@@ -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
View File

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