diff --git a/package.json b/package.json index 9d3ec168..4be4413d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "Rowy", - "version": "2.3.2", + "name": "rowy", + "version": "2.4.0-rc.0", "homepage": "https://rowy.io", "repository": { "type": "git", @@ -15,10 +15,10 @@ "@hookform/resolvers": "^2.8.5", "@mdi/js": "^6.5.95", "@monaco-editor/react": "^4.3.1", - "@mui/icons-material": "^5.4.1", - "@mui/lab": "^5.0.0-alpha.68", - "@mui/material": "^5.4.1", - "@mui/styles": "^5.4.1", + "@mui/icons-material": "^5.4.2", + "@mui/lab": "^5.0.0-alpha.69", + "@mui/material": "^5.4.2", + "@mui/styles": "^5.4.2", "@rowy/form-builder": "^0.5.3", "@rowy/multiselect": "^0.2.3", "@tinymce/tinymce-react": "^3.12.6", diff --git a/src/App.tsx b/src/App.tsx index ba5d5041..5c1aec54 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { + }> { + size?: number; +} + +export default function LogoRowyRun({ + size = 1.5, + ...props +}: ILogoRowyRunProps) { + const theme = useTheme(); + + return ( + + Rowy Run + + + + + + ); +} diff --git a/src/assets/logo-sticker.svg b/src/assets/logo-sticker.svg deleted file mode 100644 index b6696ee0..00000000 --- a/src/assets/logo-sticker.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/atoms/RowyRunModal.ts b/src/atoms/RowyRunModal.ts new file mode 100644 index 00000000..d62d7409 --- /dev/null +++ b/src/atoms/RowyRunModal.ts @@ -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 }); +}; diff --git a/src/components/CodeEditor/CodeEditorHelper.tsx b/src/components/CodeEditor/CodeEditorHelper.tsx index eb5e589a..6e4a092b 100644 --- a/src/components/CodeEditor/CodeEditorHelper.tsx +++ b/src/components/CodeEditor/CodeEditorHelper.tsx @@ -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({ - {/* */} ); } diff --git a/src/components/CodeEditor/extensions.d.ts b/src/components/CodeEditor/extensions.d.ts index ff93e2e6..c1c65d4d 100644 --- a/src/components/CodeEditor/extensions.d.ts +++ b/src/components/CodeEditor/extensions.d.ts @@ -24,7 +24,7 @@ type ExtensionContext = { requiredFields: string[]; extensionBody: any; }; - utilFns: any; + RULES_UTILS: any; }; // extension body definition diff --git a/src/components/CodeEditor/utils.d.ts b/src/components/CodeEditor/utils.d.ts index b93b7c93..38ab70d6 100644 --- a/src/components/CodeEditor/utils.d.ts +++ b/src/components/CodeEditor/utils.d.ts @@ -1,7 +1,7 @@ /** * utility functions */ -declare namespace utilFns { +declare namespace RULES_UTILS { /** * Gets the secret defined in Google Cloud Secret */ diff --git a/src/components/Home/AccessDenied.tsx b/src/components/Home/AccessDenied.tsx index 5c6b9024..a718cbbd 100644 --- a/src/components/Home/AccessDenied.tsx +++ b/src/components/Home/AccessDenied.tsx @@ -19,7 +19,7 @@ export default function AccessDenied() { description={ <> - You are currently signed in as {currentUser?.email} + You are signed in as {currentUser?.email} You do not have access to this project. Please contact the project diff --git a/src/components/RowyRunModal.tsx b/src/components/RowyRunModal.tsx new file mode 100644 index 00000000..5b68bac4 --- /dev/null +++ b/src/components/RowyRunModal.tsx @@ -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 ( + + } + maxWidth="xs" + body={ + <> + + {showUpdateModal ? "Update" : "Set up"} Rowy Run to use{" "} + {state.feature || "this feature"} + + + {showUpdateModal && ( + + {state.feature || "This feature"} requires Rowy Run v + {state.version} or later. + + )} + + + Rowy Run is a Cloud Run instance that provides backend + functionality, such as table action scripts, user management, and + easy Cloud Function deployment.{" "} + + Learn more + + + + + + + {!userClaims?.roles.includes("ADMIN") && ( + + Contact the project owner to set up Rowy Run + + )} + + } + /> + ); +} diff --git a/src/components/Settings/ProjectSettings/About.tsx b/src/components/Settings/ProjectSettings/About.tsx index 4cdd6c5c..db1804db 100644 --- a/src/components/Settings/ProjectSettings/About.tsx +++ b/src/components/Settings/ProjectSettings/About.tsx @@ -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() { )} - {name} v{version} + Rowy v{version} diff --git a/src/components/Settings/ProjectSettings/RowyRun.tsx b/src/components/Settings/ProjectSettings/RowyRun.tsx index 901adf51..01f61073 100644 --- a/src/components/Settings/ProjectSettings/RowyRun.tsx +++ b/src/components/Settings/ProjectSettings/RowyRun.tsx @@ -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 ( <> - - {name} Run is a Cloud Run instance that provides backend functionality, + + + + Rowy Run is a Cloud Run instance that provides backend functionality, such as table action scripts, user management, and easy Cloud Function deployment.{" "} - {name} Run v{latestUpdate.deployedRowyRun} + Rowy Run v{latestUpdate.deployedRowyRun} @@ -166,7 +170,7 @@ export default function RowyRun({ > - 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. @@ -196,11 +200,10 @@ export default function RowyRun({ color="success" style={{ fontSize: "1rem", verticalAlign: "text-top" }} /> -   - {name} Run is set up correctly +   Rowy Run is set up correctly ) : verified === false ? ( - `${name} Run is not set up correctly` + `Rowy Run is not set up correctly` ) : ( " " ) diff --git a/src/components/Settings/UserManagement/InviteUser.tsx b/src/components/Settings/UserManagement/InviteUser.tsx index 0f534dcd..c51ca215 100644 --- a/src/components/Settings/UserManagement/InviteUser.tsx +++ b/src/components/Settings/UserManagement/InviteUser.tsx @@ -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() { <> - ); - - return ( - <> - - {name} Run is a Google Cloud Run instance that provides backend - functionality, such as table action scripts, user management, and easy - Cloud Function deployment. - - - - {!isValidRowyRunUrl && ( - <> - {deployButton} - -
- - Then paste the Rowy Run instance URL below: - - - - setRowyRunUrl(e.target.value)} - type="url" - autoComplete="url" - fullWidth - error={verificationStatus === "FAIL"} - helperText={ - verificationStatus === "FAIL" ? "Invalid URL" : " " - } - /> - - Verify - - -
- - )} -
- {isValidRowyRunUrl && ( - - {!isLatestVersion && ( - - {deployButton} - - Verify - - - )} - - )} - - ); -} - -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; - } -}; diff --git a/src/components/Setup/Step2ProjectOwner.tsx b/src/components/Setup/Step2ProjectOwner.tsx deleted file mode 100644 index b61ee872..00000000 --- a/src/components/Setup/Step2ProjectOwner.tsx +++ /dev/null @@ -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( - 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 ( - <> - - The project owner requires full access to manage this project. The - default project owner is the Google Cloud account used to deploy Rowy - Run: {email} - - - - {!(isSignedIn || isDomainAuthorized) && ( - <> -
    -
  1. the Google auth provider enabled and
  2. -
  3. - this domain authorized:{" "} - {window.location.hostname} - - navigator.clipboard.writeText(window.location.hostname) - } - > - - -
  4. -
- - - - - - - - )} -
- - {isDomainAuthorized && ( - - Sign in as the project owner: {email} - - ) - } - > - {!isSignedIn && ( - - )} - - )} - - {isSignedIn && ( - - {hasRoles !== true && ( -
- - Assign roles - - - {typeof hasRoles === "string" && hasRoles !== "LOADING" && ( - - {hasRoles} - - )} -
- )} -
- )} - - ); -} - -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; - } -}; diff --git a/src/components/Setup/Step3Rules.tsx b/src/components/Setup/Step3Rules.tsx deleted file mode 100644 index 56ab2509..00000000 --- a/src/components/Setup/Step3Rules.tsx +++ /dev/null @@ -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(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 ( - <> - - {name} configuration is stored in the {CONFIG} collection - on Firestore. Your users will need read access to this collection and - admins will need write access. - - - {!hasRules && error !== "security-rules/not-found" && ( - - setAdminRule(e.target.checked)} - /> - } - label="Allow admins to read and write all documents" - sx={{ "&&": { ml: -11 / 8, mb: -11 / 8 }, width: "100%" }} - /> - - - - We removed an insecure rule that allows anyone to access any part of - your database - - - - - - Please verify the new rules first. - - - - Set Firestore Rules - - {rulesStatus !== "LOADING" && typeof rulesStatus === "string" && ( - - {rulesStatus} - - )} - {!showManualMode && ( - setShowManualMode(true)} - > - Alternatively, add these rules in the Firebase Console - - )} - - )} - - {!hasRules && showManualMode && ( - - $1` - ), - }} - /> - -
- - - - - - - - - - - - Verify - - {rulesStatus !== "LOADING" && typeof rulesStatus === "string" && ( - - {rulesStatus} - - )} - - -
-
- )} - - {hasRules && ( - - )} - - ); -} - -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; - } -}; diff --git a/src/components/Setup/Step4Migrate.tsx b/src/components/Setup/Step4Migrate.tsx deleted file mode 100644 index 676a553d..00000000 --- a/src/components/Setup/Step4Migrate.tsx +++ /dev/null @@ -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 ( - <> - - It looks like you’ve previously configured your Firestore database for - Firetable. You can migrate this configuration, including your tables to{" "} - {name}. - - - - Configuration migrated to the {CONFIG} collection. - - ) : ( - <> - Migrate your configuration to the {CONFIG}{" "} - collection. - - ) - } - > - {status !== true && ( - <> - - Migrate - - {status !== "LOADING" && typeof status === "string" && ( - - {status} - - )} - - )} - - - ); -} - -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; - } -}; diff --git a/src/components/Setup/Step6Finish.tsx b/src/components/Setup/Steps/StepFinish.tsx similarity index 73% rename from src/components/Setup/Step6Finish.tsx rename to src/components/Setup/Steps/StepFinish.tsx index aaa0b0eb..24ab4f67 100644 --- a/src/components/Setup/Step6Finish.tsx +++ b/src/components/Setup/Steps/StepFinish.tsx @@ -1,17 +1,29 @@ import { useState, useEffect } from "react"; import { useSnackbar } from "notistack"; +import { Link } from "react-router-dom"; +import type { ISetupStep } from "../types"; -import { Typography, Stack, RadioGroup, Radio } from "@mui/material"; +import { Typography, Stack, RadioGroup, Radio, Button } from "@mui/material"; import ThumbUpIcon from "@mui/icons-material/ThumbUpAlt"; import ThumbUpOffIcon from "@mui/icons-material/ThumbUpOffAlt"; import ThumbDownIcon from "@mui/icons-material/ThumbDownAlt"; import ThumbDownOffIcon from "@mui/icons-material/ThumbDownOffAlt"; -import { name } from "@root/package.json"; import { analytics } from "analytics"; import { db } from "@src/firebase"; +import { routes } from "@src/constants/routes"; -export default function Step6Finish() { +export default { + id: "finish", + layout: "centered", + shortTitle: "Finish", + title: "You’re all set up!", + description: + "You can now continue to Rowy and create a table from your Firestore collections.", + body: StepFinish, +} as ISetupStep; + +function StepFinish() { const { enqueueSnackbar } = useSnackbar(); useEffect(() => { @@ -27,11 +39,6 @@ export default function Step6Finish() { return ( <> - - You can now continue to {name} and create a table from your Firestore - collections. - - + + ); } diff --git a/src/components/Setup/Steps/StepRules.tsx b/src/components/Setup/Steps/StepRules.tsx new file mode 100644 index 00000000..8a70af32 --- /dev/null +++ b/src/components/Setup/Steps/StepRules.tsx @@ -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 {CONFIG} 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 ( + <> + + setAdminRule(e.target.checked)} + /> + } + label="Allow admins to read and write all documents" + sx={{ "&&": { ml: -11 / 8, mb: -11 / 8 }, width: "100%" }} + /> + + $1` + ), + }} + /> + +
+ + + + + + + + + +
+
+ + } + onClick={() => setComplete()} + > + Mark as done + + ) + } + status={isComplete ? "complete" : "incomplete"} + /> + + ); +} diff --git a/src/components/Setup/Steps/StepStorageRules.tsx b/src/components/Setup/Steps/StepStorageRules.tsx new file mode 100644 index 00000000..f1a91675 --- /dev/null +++ b/src/components/Setup/Steps/StepStorageRules.tsx @@ -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 ( + <> + + $1` + ), + }} + /> + +
+ + + + + + + + + +
+
+ + } + onClick={() => setComplete()} + > + Mark as done + + ) + } + status={isComplete ? "complete" : "incomplete"} + /> + + ); +} diff --git a/src/components/Setup/Step0Welcome.tsx b/src/components/Setup/Steps/StepWelcome.tsx similarity index 52% rename from src/components/Setup/Step0Welcome.tsx rename to src/components/Setup/Steps/StepWelcome.tsx index da1c6d0b..42a725b3 100644 --- a/src/components/Setup/Step0Welcome.tsx +++ b/src/components/Setup/Steps/StepWelcome.tsx @@ -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. +
+
+ 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 ( <> -
- - You’ll be up and running in just a few minutes. - - - Configure your project’s backend functionality, Firestore Rules, and - user management. - - - Project: {projectId} - -
+ + Project: {projectId} + - 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, }} /> + + ); } diff --git a/src/components/Setup/types.d.ts b/src/components/Setup/types.d.ts new file mode 100644 index 00000000..a4e3037f --- /dev/null +++ b/src/components/Setup/types.d.ts @@ -0,0 +1,15 @@ +export interface ISetupStep { + id: string; + layout?: "centered"; + shortTitle: string; + title: React.ReactNode; + description?: React.ReactNode; + body: React.ComponentType; +} + +export interface ISetupStepBodyProps { + completion: Record; + setCompletion: React.Dispatch>>; + isComplete: boolean; + setComplete: (value: boolean = true) => void; +} diff --git a/src/components/SideDrawer/Form/index.tsx b/src/components/SideDrawer/Form/index.tsx index 6895fc2b..2236bd7f 100644 --- a/src/components/SideDrawer/Form/index.tsx +++ b/src/components/SideDrawer/Form/index.tsx @@ -2,6 +2,7 @@ import { createElement, useEffect } from "react"; import { useForm } from "react-hook-form"; import _sortBy from "lodash/sortBy"; import _isEmpty from "lodash/isEmpty"; +import _set from "lodash/set"; import createPersistedState from "use-persisted-state"; import { Stack, FormControlLabel, Switch } from "@mui/material"; @@ -44,7 +45,16 @@ export default function Form({ values }: IFormProps) { // Get initial values from fields config. This won’t be written to the db // when the SideDrawer is opened. Only dirty fields will be written const initialValues = fields.reduce( - (a, { key, type }) => ({ ...a, [key]: getFieldProp("initialValue", type) }), + (a, { key, type }) => { + const initialValue = getFieldProp("initialValue", type); + const nextValues = { ...a }; + if (key.indexOf('.') !== -1) { + _set(nextValues, key, initialValue); + } else { + nextValues[key] = initialValue; + } + return nextValues; + }, {} ); const { ref: docRef, ...rowValues } = values; diff --git a/src/components/Table/ColumnMenu/FieldSettings/DefaultValueInput.tsx b/src/components/Table/ColumnMenu/FieldSettings/DefaultValueInput.tsx index c4275a42..8989ccfd 100644 --- a/src/components/Table/ColumnMenu/FieldSettings/DefaultValueInput.tsx +++ b/src/components/Table/ColumnMenu/FieldSettings/DefaultValueInput.tsx @@ -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" } }, - }, - }} > - + + Dynamic —{" "} + + Requires Rowy Run setup + + + ) + } + secondary="Write code to set the default value using Rowy Run" /> diff --git a/src/components/TableHeader/CloudLogs/index.tsx b/src/components/TableHeader/CloudLogs/index.tsx index 8179ebf6..1f3d5b53 100644 --- a/src/components/TableHeader/CloudLogs/index.tsx +++ b/src/components/TableHeader/CloudLogs/index.tsx @@ -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 ( <> } - onClick={() => setOpen(true)} + onClick={ + settings?.rowyRunUrl + ? () => setOpen(true) + : () => openRowyRunModal("Cloud logs") + } /> {open && ( diff --git a/src/components/TableHeader/Extensions/index.tsx b/src/components/TableHeader/Extensions/index.tsx index dfb7dfe9..d6891824 100644 --- a/src/components/TableHeader/Extensions/index.tsx +++ b/src/components/TableHeader/Extensions/index.tsx @@ -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 ( + openRowyRunModal("Extensions")} + icon={} + /> + ); + const handleOpen = () => { if (tableState?.config.sparks) { // migration is required diff --git a/src/components/TableHeader/ReExecute.tsx b/src/components/TableHeader/ReExecute.tsx index 0e3da88d..729c0bcf 100644 --- a/src/components/TableHeader/ReExecute.tsx +++ b/src/components/TableHeader/ReExecute.tsx @@ -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 ( + openRowyRunModal()} + icon={} + /> + ); + const query: any = isCollectionGroup() ? db.collectionGroup(tableState?.tablePath!) : db.collection(tableState?.tablePath!); diff --git a/src/components/TableHeader/Webhooks/index.tsx b/src/components/TableHeader/Webhooks/index.tsx index 03b2cf5d..50cdc0d5 100644 --- a/src/components/TableHeader/Webhooks/index.tsx +++ b/src/components/TableHeader/Webhooks/index.tsx @@ -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 ( + openRowyRunModal("Webhooks", "1.2.0")} + icon={} + /> + ); const edited = !_isEqual(currentWebhooks, localWebhooksObjects); diff --git a/src/components/TableSettings/DeleteMenu.tsx b/src/components/TableSettings/DeleteMenu.tsx index 4179275d..1bc2624a 100644 --- a/src/components/TableSettings/DeleteMenu.tsx +++ b/src/components/TableSettings/DeleteMenu.tsx @@ -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: ( <> - This will only delete the {name} configuration data. + This will only delete the Rowy configuration data. You will not lose any data in your Firestore collection{" "} diff --git a/src/components/TableSettings/form.tsx b/src/components/TableSettings/form.tsx index d27229fe..8db56e25 100644 --- a/src/components/TableSettings/form.tsx +++ b/src/components/TableSettings/form.tsx @@ -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: ( -
- Primary collection - - Connect this table to the single collection matching the - collection name entered below - -
+ + Connect this table to the single collection matching + the collection name entered below + + } + style={{ maxWidth: 470 }} + /> ), value: "primaryCollection", }, { label: ( -
- Collection group - - Connect this table to all collections and subcollections{" "} - matching the collection name entered below - -
+ + Connect this table to{" "} + all collections and subcollections 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) => {option.label}, - 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) => ( + {option.label} + ), + freeText: true, + required: true, + assistiveText: ( <> - - You can change which Firestore collection to display. Data in the - new collection must be compatible with the existing columns. + {mode === TableSettingsDialogModes.update ? ( + <> + + 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." + )}{" "} + + Your collections + + - ) : ( - "Choose which Firestore collection to display." - )}{" "} - - Your collections - - - - ), - AddButtonProps: { - children: "Create collection or use custom path…", - }, - AddDialogProps: { - title: "Create collection or use custom path", - textFieldLabel: ( - <> - Collection name - - If this collection does not exist, it won’t be created until you - add a row to the table - - - ), - }, - 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 + + If this collection does not exist, it won’t be created until + you add a row to the table + + + ), + }, + 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 ? ( + <> + + 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." + )}{" "} + + Your collections + + + + ), + 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 { diff --git a/src/components/TableSettings/index.tsx b/src/components/TableSettings/index.tsx index bad0b512..8f11b2f8 100644 --- a/src/components/TableSettings/index.tsx +++ b/src/components/TableSettings/index.tsx @@ -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: { diff --git a/src/components/fields/Action/ActionFab.tsx b/src/components/fields/Action/ActionFab.tsx index eb921ea7..31f12533 100644 --- a/src/components/fields/Action/ActionFab.tsx +++ b/src/components/fields/Action/ActionFab.tsx @@ -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 = diff --git a/src/components/fields/Action/Settings.tsx b/src/components/fields/Action/Settings.tsx index d3356c4e..637b70a9 100644 --- a/src/components/fields/Action/Settings.tsx +++ b/src/components/fields/Action/Settings.tsx @@ -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 }) => { Write JavaScript code below that will be executed by Rowy Run.{" "} - - Requires Rowy Run setup - - + {!settings?.rowyRunUrl && ( + + Requires Rowy Run setup → + + )} } + disabled={!settings?.rowyRunUrl} /> { Callable A{" "} - callable function - {" "} + {" "} you’ve deployed on your Firestore or Google Cloud project @@ -339,25 +345,25 @@ const Settings = ({ config, onChange }) => { <> Write the name of the callable function you’ve deployed to your project.{" "} - View your callable functions - +
Your callable function must be compatible with Rowy Action columns.{" "} - View requirements - + } /> diff --git a/src/components/fields/ConnectTable/Settings.tsx b/src/components/fields/ConnectTable/Settings.tsx index 5cbdd58d..c220f109 100644 --- a/src/components/fields/ConnectTable/Settings.tsx +++ b/src/components/fields/ConnectTable/Settings.tsx @@ -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, diff --git a/src/components/fields/ConnectTable/index.tsx b/src/components/fields/ConnectTable/index.tsx index ae903d60..4a4c3d5a 100644 --- a/src/components/fields/ConnectTable/index.tsx +++ b/src/components/fields/ConnectTable/index.tsx @@ -30,7 +30,7 @@ export const config: IFieldConfig = { initialValue: [], icon: , 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; diff --git a/src/components/fields/Derivative/Settings.tsx b/src/components/fields/Derivative/Settings.tsx index 436c3d8e..888c6ef9 100644 --- a/src/components/fields/Derivative/Settings.tsx +++ b/src/components/fields/Derivative/Settings.tsx @@ -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); diff --git a/src/components/fields/Derivative/index.tsx b/src/components/fields/Derivative/index.tsx index 55644011..27eac022 100644 --- a/src/components/fields/Derivative/index.tsx +++ b/src/components/fields/Derivative/index.tsx @@ -14,14 +14,14 @@ export const config: IFieldConfig = { initialValue: "", initializable: true, icon: , - requireConfiguration: true, description: - "Value derived from the rest of the row’s values. Displayed using any other field type. Requires Cloud Function set up.", + "Value derived from the rest of the row’s values. Displayed using any other field type. Requires Rowy Run set up.", TableCell: withBasicCell(BasicCell), TableEditor: NullEditor as any, SideDrawerField: BasicCell as any, contextMenuActions: ContextMenuActions, settings: Settings, settingsValidator, + requireConfiguration: true, }; export default config; diff --git a/src/config/firestoreRules.ts b/src/config/firestoreRules.ts index e23047bd..8164abdf 100644 --- a/src/config/firestoreRules.ts +++ b/src/config/firestoreRules.ts @@ -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; } diff --git a/src/config/storageRules.ts b/src/config/storageRules.ts new file mode 100644 index 00000000..5abdd083 --- /dev/null +++ b/src/config/storageRules.ts @@ -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; + } +`; diff --git a/src/contexts/ProjectContext.tsx b/src/contexts/ProjectContext.tsx index 1729a3e6..a37cd671 100644 --- a/src/contexts/ProjectContext.tsx +++ b/src/contexts/ProjectContext.tsx @@ -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: ( - - ), - }); - return { success: false, error: "rowyRun is not setup" }; + console.log("Rowy Run is not set up", args); } }; diff --git a/src/hooks/useDocumentTitle.ts b/src/hooks/useDocumentTitle.ts index f81da18c..2b207c8a 100644 --- a/src/hooks/useDocumentTitle.ts +++ b/src/hooks/useDocumentTitle.ts @@ -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(" • "); diff --git a/src/hooks/useUpdateCheck.ts b/src/hooks/useUpdateCheck.ts index 1da13e04..59e4992e 100644 --- a/src/hooks/useUpdateCheck.ts +++ b/src/hooks/useUpdateCheck.ts @@ -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); diff --git a/src/pages/Auth/ImpersonatorAuth.tsx b/src/pages/Auth/ImpersonatorAuth.tsx index fc5a1d67..c1a014af 100644 --- a/src/pages/Auth/ImpersonatorAuth.tsx +++ b/src/pages/Auth/ImpersonatorAuth.tsx @@ -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.
- Make sure the {name} Run service account has the{" "} + Make sure the Rowy Run service account has the{" "} Service Account Token Creator IAM role. diff --git a/src/pages/Auth/SetupGuide.tsx b/src/pages/Auth/SetupGuide.tsx index 6de420df..5aa46cca 100644 --- a/src/pages/Auth/SetupGuide.tsx +++ b/src/pages/Auth/SetupGuide.tsx @@ -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. } diff --git a/src/pages/Deploy.tsx b/src/pages/Deploy.tsx index 2f890b90..eca61a32 100644 --- a/src/pages/Deploy.tsx +++ b/src/pages/Deploy.tsx @@ -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.

@@ -148,7 +147,7 @@ export default function DeployPage() { - By setting up {name}, you agree to our{" "} + By setting up Rowy, you agree to our{" "} ; @@ -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 }, ]; diff --git a/src/pages/Setup.tsx b/src/pages/Setup.tsx index a5a19754..d8401efc 100644 --- a/src/pages/Setup.tsx +++ b/src/pages/Setup.tsx @@ -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; - setCompletion: React.Dispatch>>; - 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 = {}; - - 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>({ 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: , - actions: completion.welcome ? ( - - Get started - - ) : ( - -
- - Get started - -
-
- ), - }, - { - id: "rowyRun", - shortTitle: `${name} Run`, - title: `Set up ${name} Run`, - body: , - }, - { - id: "projectOwner", - shortTitle: `Project owner`, - title: `Set up project owner`, - body: , - }, - { - id: "rules", - shortTitle: `Rules`, - title: `Set up Firestore Rules`, - body: , - }, - completion.migrate !== undefined - ? { - id: "migrate", - shortTitle: `Migrate`, - title: `Migrate to ${name} (optional)`, - body: , - } - : ({} as ISetupStep), - { - id: "finish", - layout: "centered" as "centered", - shortTitle: `Finish`, - title: `You’re all set up!`, - body: , - actions: ( - - ), - }, - ].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 ( - - - - 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 ? ( - - {listedSteps.map(({ id, shortTitle }, i) => ( - - setStepId(id)} - disabled={i > 0 && !completion[listedSteps[i - 1]?.id]} - sx={{ py: 2, my: -2, borderRadius: 1 }} - > - {shortTitle} - - - ))} - - ) : ( - setStepId(steps[stepIndex - 1].id)} - > - - - } - nextButton={ - setStepId(steps[stepIndex + 1].id)} - > - - - } - position="static" - sx={{ - background: "none", - p: 0, - "& .MuiMobileStepper-dot": { mx: 0.5 }, - }} - /> - )} - - {step.layout === "centered" ? ( - - - {stepId === "welcome" && ( - - - - - - )} - - - - - {step.title} - - - - - - - - {step.body} - - - - - - ) : ( - <> - - - - {step.title} - - - - - - - - {step.body} - - - - - )} - -
{ - e.preventDefault(); - try { - handleContinue(); - } catch (e: any) { - throw new Error(e.message); - } - return false; - }} - > - - {step.actions ?? ( - - Continue - - )} - -
-
-
+ ); } diff --git a/src/theme/components.tsx b/src/theme/components.tsx index ab7a0f74..7694ba8a 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -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, diff --git a/yarn.lock b/yarn.lock index 48f05a39..39182452 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1654,10 +1654,10 @@ resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== -"@date-io/core@^2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.11.0.tgz#28580cda1c8228ab2c7ed6aee673ef0495f913e6" - integrity sha512-DvPBnNoeuLaoSJZaxgpu54qzRhRKjSYVyQjhznTFrllKuDpm0sDFjHo6lvNLCM/cfMx2gb2PM2zY2kc9C8nmuw== +"@date-io/core@^2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.13.1.tgz#f041765aff5c55fbc7e37fdd75fc1792733426d6" + integrity sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg== "@date-io/date-fns@1.x": version "1.3.13" @@ -1666,33 +1666,33 @@ dependencies: "@date-io/core" "^1.3.13" -"@date-io/date-fns@^2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.11.0.tgz#142fbf954eda7ad66514af7a2802d78c4ea40053" - integrity sha512-mPQ71plBeFrArvBSHtjWMHXA89IUbZ6kuo2dsjlRC/1uNOybo91spIb+wTu03NxKTl8ut07s0jJ9svF71afpRg== +"@date-io/date-fns@^2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.13.1.tgz#19d8a245dab61c03c95ba492d679d98d2b0b4af5" + integrity sha512-8fmfwjiLMpFLD+t4NBwDx0eblWnNcgt4NgfT/uiiQTGI81fnPu9tpBMYdAcuWxaV7LLpXgzLBx1SYWAMDVUDQQ== dependencies: - "@date-io/core" "^2.11.0" + "@date-io/core" "^2.13.1" -"@date-io/dayjs@^2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.11.0.tgz#41f4b4b9629612e6012accffd848875d1aeffb74" - integrity sha512-w67vRK56NZJIKhJM/CrNbfnIcuMvR3ApfxzNZiCZ5w29sxgBDeKuX4M+P7A9r5HXOMGcsOcpgaoTDINNGkdpGQ== +"@date-io/dayjs@^2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.13.1.tgz#98461d22ee98179b9f2dca3b36f1b618704ae593" + integrity sha512-5bL4WWWmlI4uGZVScANhHJV7Mjp93ec2gNeUHDqqLaMZhp51S0NgD25oqj/k0LqBn1cdU2MvzNpk/ObMmVv5cQ== dependencies: - "@date-io/core" "^2.11.0" + "@date-io/core" "^2.13.1" -"@date-io/luxon@^2.11.1": - version "2.11.1" - resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.11.1.tgz#31a72f7b5e163c74e8a3b29d8f16c4c30de6ed43" - integrity sha512-JUXo01kdPQxLORxqdENrgdUhooKgDUggsNRSdi2BcUhASIY2KGwwWXu8ikVHHGkw+DUF4FOEKGfkQd0RHSvX6g== +"@date-io/luxon@^2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.13.1.tgz#3701b3cabfffda5102af302979aa6e58acfda91a" + integrity sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg== dependencies: - "@date-io/core" "^2.11.0" + "@date-io/core" "^2.13.1" -"@date-io/moment@^2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.11.0.tgz#850f8dd090d401845b39276d034dbabe20224ef5" - integrity sha512-QSL+83qezQ9Ty0dtFgAkk6eC0GMl/lgYfDajeVUDB3zVA2A038hzczRLBg29ifnBGhQMPABxuOafgWwhDjlarg== +"@date-io/moment@^2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.13.1.tgz#122a51e4bdedf71ff3babb264427737dc022c1e6" + integrity sha512-XX1X/Tlvl3TdqQy2j0ZUtEJV6Rl8tOyc5WOS3ki52He28Uzme4Ro/JuPWTMBDH63weSWIZDlbR7zBgp3ZA2y1A== dependencies: - "@date-io/core" "^2.11.0" + "@date-io/core" "^2.13.1" "@emotion/babel-plugin@^11.3.0": version "11.3.0" @@ -2429,55 +2429,55 @@ "@monaco-editor/loader" "^1.2.0" prop-types "^15.7.2" -"@mui/base@5.0.0-alpha.68": - version "5.0.0-alpha.68" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.68.tgz#d93d77e662bc8dce47c9415fc6cbcac6658efab7" - integrity sha512-q+3gX6EHuM/AyOn8fkoANQxSzIHBeuNsrGgb7SPP0y7NuM+4ZHG/b9882+OfHcilaSqPDWUQoLbphcBpw/m/RA== +"@mui/base@5.0.0-alpha.69": + version "5.0.0-alpha.69" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.69.tgz#8511198d760de0795870f5ec63e53db73ba801ec" + integrity sha512-IxUUj/lkilCTNBIybQxyQGW/zpxFp490G0QBQJgRp9TJkW2PWSTLvAH7gcH0YHd0L2TAf1TRgfdemoRseMzqQA== dependencies: "@babel/runtime" "^7.17.0" "@emotion/is-prop-valid" "^1.1.1" - "@mui/utils" "^5.4.1" + "@mui/utils" "^5.4.2" "@popperjs/core" "^2.4.4" clsx "^1.1.1" prop-types "^15.7.2" react-is "^17.0.2" -"@mui/icons-material@^5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.4.1.tgz#20901e9a09154355b7a832180a90717938c675c4" - integrity sha512-koiq9q2GfjXRUWcC5fEi1b+EA4vfJHgIaAdBHlkOrBx2cnmmazQcyib501eodPfaZGx9BikrhivODaNQYQq8hA== +"@mui/icons-material@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.4.2.tgz#b2fd2c6c81d2d275e17ce40bd50c63cb197d324b" + integrity sha512-7c+G3jBT+e+pN0a9DJ0Bd8Kr1Vy6os5Q1yd2aXcwuhlRI3uzJBLJ8sX6FSWoh5DSEBchb7Bsk1uHz6U0YN9l+Q== dependencies: "@babel/runtime" "^7.17.0" -"@mui/lab@^5.0.0-alpha.68": - version "5.0.0-alpha.68" - resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.68.tgz#a5034a8f749f3f4f0a1b8613515bed4054ade3e6" - integrity sha512-wvszkLsgXgl3kMPVpHNm9pRYld9/2r0MYRlJUEh2GWwjBPE3dDTOIF2IHgZ3WqRBnJMitzUVt7v5Lu9/grjrIQ== +"@mui/lab@^5.0.0-alpha.69": + version "5.0.0-alpha.69" + resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.69.tgz#c1130e6ed5edc579c5d88a762e79935b01181cb1" + integrity sha512-VrTcmXTS9UlTsp40nIZ/R/HBHOvxP2lvgSY9zLSn5XPhMQEMD1H0wTJ68mEuCm18cnl1sbcUOCMfWx9io/u5zg== dependencies: "@babel/runtime" "^7.17.0" - "@date-io/date-fns" "^2.11.0" - "@date-io/dayjs" "^2.11.0" - "@date-io/luxon" "^2.11.1" - "@date-io/moment" "^2.11.0" - "@mui/base" "5.0.0-alpha.68" - "@mui/system" "^5.4.1" - "@mui/utils" "^5.4.1" + "@date-io/date-fns" "^2.13.1" + "@date-io/dayjs" "^2.13.1" + "@date-io/luxon" "^2.13.1" + "@date-io/moment" "^2.13.1" + "@mui/base" "5.0.0-alpha.69" + "@mui/system" "^5.4.2" + "@mui/utils" "^5.4.2" clsx "^1.1.1" prop-types "^15.7.2" react-is "^17.0.2" react-transition-group "^4.4.2" rifm "^0.12.1" -"@mui/material@^5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.4.1.tgz#05d3f726771c413dc430163d7c508edfcee04807" - integrity sha512-SxAT43UAjFTBBpJrN+oGrv40xP1uCa5Z49NfHt3m93xYeFzbxKOk0V9IKU7zlUjbsaVQ0i+o24yF5GULZmynlA== +"@mui/material@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.4.2.tgz#04ea6632d7ca600a2ae528f6f140ef0af9c01434" + integrity sha512-jmeLWEO6AA6g7HErhI3MXVGaMZtqDZjDwcHCg24WY954wO38Xn0zJ53VfpFc44ZTJLV9Ejd7ci9fLlG/HmJCeg== dependencies: "@babel/runtime" "^7.17.0" - "@mui/base" "5.0.0-alpha.68" - "@mui/system" "^5.4.1" - "@mui/types" "^7.1.1" - "@mui/utils" "^5.4.1" + "@mui/base" "5.0.0-alpha.69" + "@mui/system" "^5.4.2" + "@mui/types" "^7.1.2" + "@mui/utils" "^5.4.2" "@types/react-transition-group" "^4.4.4" clsx "^1.1.1" csstype "^3.0.10" @@ -2486,34 +2486,34 @@ react-is "^17.0.2" react-transition-group "^4.4.2" -"@mui/private-theming@^5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.4.1.tgz#5fa6490f35e78781239f1944ae80a7006c5a7648" - integrity sha512-Xbc4MXFZxv0A3hoc4TSDBhzjhstppKfc+gQcTMqqBZQP7KjnmxF+wO7rEPQuYRBihjCqQBdrHIGMLsKWrhkZkQ== +"@mui/private-theming@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.4.2.tgz#f0a05f908456a2f7b87ccb6fc3b6e1faae9d89e6" + integrity sha512-mlPDYYko4wIcwXjCPEmOWbNTT4DZ6h9YHdnRtQPnWM28+TRUHEo7SbydnnmVDQLRXUfaH4Y6XtEHIfBNPE/SLg== dependencies: "@babel/runtime" "^7.17.0" - "@mui/utils" "^5.4.1" + "@mui/utils" "^5.4.2" prop-types "^15.7.2" -"@mui/styled-engine@^5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.4.1.tgz#1427738e71c087f7005547e17d4a59de75597850" - integrity sha512-CFLNJkopRoAuShkgUZOTBVxdTlKu4w6L4kOwPi4r3QB2XXS6O5kyLHSsg9huUbtOYk5Dv5UZyUSc5pw4J7ezdg== +"@mui/styled-engine@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.4.2.tgz#e04903e06bd49fd10072a44ff38e13f5481bb64d" + integrity sha512-tz9p3aRtzXHKAg7x3BgP0hVQEoGKaxNCFxsJ+d/iqEHYvywWFSs6oxqYAvDHIRpvMlUZyPNoTrkcNnbdMmH/ng== dependencies: "@babel/runtime" "^7.17.0" "@emotion/cache" "^11.7.1" prop-types "^15.7.2" -"@mui/styles@^5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.4.1.tgz#994171da902267184fffa19896ee5bbb07d4d783" - integrity sha512-ekw2NBC06re0H9SvCA1XgtFcghB8AQdGPXD3mjIz5ik+X+LvR+f2TeoCpJpkKp7UQdcNn6uuYi6BO6irTiQhdw== +"@mui/styles@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.4.2.tgz#e0dadfc5de8255605f23c2f909f3669f0911bb88" + integrity sha512-BX75fNHmRF51yove9dBkH28gpSFjClOPDEnUwLTghPYN913OsqViS/iuCd61dxzygtEEmmeYuWfQjxu/F6vF5g== dependencies: "@babel/runtime" "^7.17.0" "@emotion/hash" "^0.8.0" - "@mui/private-theming" "^5.4.1" - "@mui/types" "^7.1.1" - "@mui/utils" "^5.4.1" + "@mui/private-theming" "^5.4.2" + "@mui/types" "^7.1.2" + "@mui/utils" "^5.4.2" clsx "^1.1.1" csstype "^3.0.10" hoist-non-react-statics "^3.3.2" @@ -2527,29 +2527,29 @@ jss-plugin-vendor-prefixer "^10.8.2" prop-types "^15.7.2" -"@mui/system@^5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.4.1.tgz#cf253369fbf1d960c792f0ec068fa28af81be3d4" - integrity sha512-07JBYf9iQdxIHZU8cFOLoxBnkQDUPLb7UBhNxo4998yEqpWFJ00WKgEVYBKvPl0X+MRU/20wqFz6yGIuCx4AeA== +"@mui/system@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.4.2.tgz#8166e406ba4628950bd79cec8159de25d5aef162" + integrity sha512-QegBVu6fxUNov1X9bWc1MZUTeV3A5g9PIpli7d0kzkGfq6JzrJWuPlhSPZ+6hlWmWky+bbAXhU65Qz8atWxDGw== dependencies: "@babel/runtime" "^7.17.0" - "@mui/private-theming" "^5.4.1" - "@mui/styled-engine" "^5.4.1" - "@mui/types" "^7.1.1" - "@mui/utils" "^5.4.1" + "@mui/private-theming" "^5.4.2" + "@mui/styled-engine" "^5.4.2" + "@mui/types" "^7.1.2" + "@mui/utils" "^5.4.2" clsx "^1.1.1" csstype "^3.0.10" prop-types "^15.7.2" -"@mui/types@^7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.1.tgz#9cf159dc60a101ee336e6ec74193a4f5f97f6160" - integrity sha512-33hbHFLCwenTpS+T4m4Cz7cQ/ng5g+IgtINkw1uDBVvi1oM83VNt/IGzWIQNPK8H2pr0WIfkmboD501bVdYsPw== +"@mui/types@^7.1.2": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.2.tgz#4f3678ae77a7a3efab73b6e040469cc6df2144ac" + integrity sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ== -"@mui/utils@^5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.4.1.tgz#feb365ce9a4426587510f0943fd6d6e1889e06e6" - integrity sha512-5HzM+ZjlQqbSp7UTOvLlhAjkWB+o9Z4NzO0W+yhZ1KnxITr+zr/MBzYmmQ3kyvhui8pyhgRDoTcVgwb+02ZUZA== +"@mui/utils@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.4.2.tgz#3edda8f80de235418fff0424ee66e2a49793ec01" + integrity sha512-646dBCC57MXTo/Gf3AnZSHRHznaTETQq5x7AWp5FRQ4jPeyT4WSs18cpJVwkV01cAHKh06pNQTIufIALIWCL5g== dependencies: "@babel/runtime" "^7.17.0" "@types/prop-types" "^15.7.4" @@ -17352,9 +17352,9 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.4.3, url-parse@^1.5.1: - version "1.5.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a" - integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA== + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0"