diff --git a/src/App.tsx b/src/App.tsx index 653b6a88e..9612ff03b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,12 @@ const SetupPage = lazy(() => import("@src/pages/SetupPage" /* webpackChunkName: const Navigation = lazy(() => import("@src/layouts/Navigation" /* webpackChunkName: "Navigation" */)); // prettier-ignore const TableSettingsDialog = lazy(() => import("@src/components/TableSettingsDialog" /* webpackChunkName: "TableSettingsDialog" */)); +const ProjectSettingsDialog = lazy( + () => + import( + "@src/components/ProjectSettingsDialog" /* webpackChunkName: "ProjectSettingsDialog" */ + ) +); // prettier-ignore const TablesPage = lazy(() => import("@src/pages/TablesPage" /* webpackChunkName: "TablesPage" */)); @@ -99,6 +105,7 @@ export default function App() { + } diff --git a/src/atoms/projectScope/ui.ts b/src/atoms/projectScope/ui.ts index 48af64b02..01fc14996 100644 --- a/src/atoms/projectScope/ui.ts +++ b/src/atoms/projectScope/ui.ts @@ -131,6 +131,26 @@ export const tableSettingsDialogAtom = atom( } ); +export type ProjectSettingsDialogTab = + | "general" + | "rowy-run" + | "services" + | "secrets"; +export type ProjectSettingsDialogState = { + open: boolean; + tab: ProjectSettingsDialogTab; +}; +export const projectSettingsDialogAtom = atom( + { open: false, tab: "secrets" } as ProjectSettingsDialogState, + (_, set, update?: Partial) => { + set(projectSettingsDialogAtom, { + open: true, + tab: "secrets", + ...update, + }); + } +); + /** * Store the current ID of the table being edited in tableSettingsDialog * to derive tableSettingsDialogSchemaAtom diff --git a/src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx b/src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx new file mode 100644 index 000000000..49e9c1c8b --- /dev/null +++ b/src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx @@ -0,0 +1,282 @@ +import React from "react"; +import { useAtom } from "jotai"; +import { + projectScope, + projectSettingsDialogAtom, + ProjectSettingsDialogTab, + rowyRunAtom, + secretNamesAtom, + updateSecretNamesAtom, +} from "@src/atoms/projectScope"; +import Modal from "@src/components/Modal"; +import { Box, Button, Paper, Tab, Tooltip, Typography } from "@mui/material"; +import { TabContext, TabPanel, TabList } from "@mui/lab"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; +import EditIcon from "@mui/icons-material/Edit"; +import SecretDetailsModal from "./SecretDetailsModal"; +import { runRoutes } from "@src/constants/runRoutes"; + +export default function ProjectSettingsDialog() { + const [{ open, tab }, setProjectSettingsDialog] = useAtom( + projectSettingsDialogAtom, + projectScope + ); + const [secretNames] = useAtom(secretNamesAtom, projectScope); + const [secretDetailsModal, setSecretDetailsModal] = React.useState<{ + open: boolean; + loading?: boolean; + mode?: "add" | "edit" | "delete"; + secretName?: string; + error?: string; + }>({ + open: false, + }); + const [rowyRun] = useAtom(rowyRunAtom, projectScope); + const [updateSecretNames] = useAtom(updateSecretNamesAtom, projectScope); + + if (!open) return null; + + const handleClose = () => { + setProjectSettingsDialog({ open: false }); + }; + + const handleTabChange = ( + event: React.SyntheticEvent, + newTab: ProjectSettingsDialogTab + ) => { + setProjectSettingsDialog({ tab: newTab }); + }; + + console.log(secretDetailsModal); + + return ( + <> + + + + + + + + + + + + Secrets + + + + {secretNames.secretNames?.map((secretName) => ( + + + {secretName} + + + + + + + + + + + ))} + + + + + } + /> + { + setSecretDetailsModal({ ...secretDetailsModal, open: false }); + }} + handleAdd={async (newSecretName, secretValue) => { + setSecretDetailsModal({ + ...secretDetailsModal, + loading: true, + }); + try { + await rowyRun({ + route: runRoutes.addSecret, + body: { + name: newSecretName, + value: secretValue, + }, + }); + setSecretDetailsModal({ + ...secretDetailsModal, + open: false, + loading: false, + }); + // update secret name causes an unknown modal-related bug, to be fixed + // updateSecretNames?.(); + } catch (error: any) { + console.error(error); + setSecretDetailsModal({ + ...secretDetailsModal, + error: error.message, + }); + } + }} + handleEdit={async (secretName, secretValue) => { + setSecretDetailsModal({ + ...secretDetailsModal, + loading: true, + }); + try { + await rowyRun({ + route: runRoutes.editSecret, + body: { + name: secretName, + value: secretValue, + }, + }); + setSecretDetailsModal({ + ...secretDetailsModal, + open: false, + loading: false, + }); + // update secret name causes an unknown modal-related bug, to be fixed + // updateSecretNames?.(); + } catch (error: any) { + console.error(error); + setSecretDetailsModal({ + ...secretDetailsModal, + error: error.message, + }); + } + }} + handleDelete={async (secretName) => { + setSecretDetailsModal({ + ...secretDetailsModal, + loading: true, + }); + try { + await rowyRun({ + route: runRoutes.deleteSecret, + body: { + name: secretName, + }, + }); + console.log("Setting", { + ...secretDetailsModal, + open: false, + loading: false, + }); + setSecretDetailsModal({ + ...secretDetailsModal, + open: false, + loading: false, + }); + // update secret name causes an unknown modal-related bug, to be fixed + // updateSecretNames?.(); + } catch (error: any) { + console.error(error); + setSecretDetailsModal({ + ...secretDetailsModal, + error: error.message, + }); + } + }} + /> + + ); +} diff --git a/src/components/ProjectSettingsDialog/SecretDetailsModal.tsx b/src/components/ProjectSettingsDialog/SecretDetailsModal.tsx new file mode 100644 index 000000000..1e156429d --- /dev/null +++ b/src/components/ProjectSettingsDialog/SecretDetailsModal.tsx @@ -0,0 +1,157 @@ +import React, { useState } from "react"; +import Modal from "@src/components/Modal"; +import { Box, Button, TextField, Typography } from "@mui/material"; +import { capitalize } from "lodash-es"; +import LoadingButton from "@mui/lab/LoadingButton"; + +export interface ISecretDetailsModalProps { + open: boolean; + loading?: boolean; + mode?: "add" | "edit" | "delete"; + error?: string; + secretName?: string; + handleClose: () => void; + handleAdd: (secretName: string, secretValue: string) => void; + handleEdit: (secretName: string, secretValue: string) => void; + handleDelete: (secretName: string) => void; +} + +export default function SecretDetailsModal({ + open, + loading, + mode, + error, + secretName, + handleClose, + handleAdd, + handleEdit, + handleDelete, +}: ISecretDetailsModalProps) { + const [newSecretName, setNewSecretName] = useState(""); + const [secretValue, setSecretValue] = useState(""); + + return ( + + {mode === "add" && ( + + Secret Name + setNewSecretName(e.target.value)} + /> + + This will create a secret key on Google Cloud. + + + )} + {mode === "delete" ? ( + + Are you sure you want to delete this secret key {secretName}? + + ) : ( + + Secret Value + setSecretValue(e.target.value)} + /> + + Paste your secret key here. + + + )} + {error?.length && ( + + {error} + + )} + + + { + switch (mode) { + case "add": + handleAdd(newSecretName, secretValue); + break; + case "edit": + handleEdit(secretName ?? "", secretValue); + break; + case "delete": + handleDelete(secretName ?? ""); + break; + } + }} + > + {mode === "delete" ? "Delete" : "Save"} + + + + } + /> + ); +} diff --git a/src/components/ProjectSettingsDialog/index.ts b/src/components/ProjectSettingsDialog/index.ts new file mode 100644 index 000000000..e57386ddc --- /dev/null +++ b/src/components/ProjectSettingsDialog/index.ts @@ -0,0 +1,2 @@ +export * from "./ProjectSettingsDialog"; +export { default } from "./ProjectSettingsDialog"; diff --git a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx index 9c34383f6..0f146e7b0 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx @@ -7,6 +7,7 @@ import { projectScope, secretNamesAtom, updateSecretNamesAtom, + projectSettingsDialogAtom, } from "@src/atoms/projectScope"; import InputLabel from "@mui/material/InputLabel"; import MenuItem from "@mui/material/MenuItem"; @@ -56,6 +57,10 @@ export const webhookStripe = { Auth: (webhookObject: IWebhook, setWebhookObject: (w: IWebhook) => void) => { const [secretNames] = useAtom(secretNamesAtom, projectScope); const [updateSecretNames] = useAtom(updateSecretNamesAtom, projectScope); + const [{ open, tab }, setProjectSettingsDialog] = useAtom( + projectSettingsDialogAtom, + projectScope + ); return ( <> @@ -118,8 +123,9 @@ export const webhookStripe = { })} { - const secretManagerLink = `https://console.cloud.google.com/security/secret-manager/create`; - window?.open?.(secretManagerLink, "_blank")?.focus(); + setProjectSettingsDialog({ + open: true, + }); }} > Add a key in Secret Manager diff --git a/src/components/TableSettingsDialog/TableSettingsDialog.tsx b/src/components/TableSettingsDialog/TableSettingsDialog.tsx index 8424ce1ac..ecd94e4d4 100644 --- a/src/components/TableSettingsDialog/TableSettingsDialog.tsx +++ b/src/components/TableSettingsDialog/TableSettingsDialog.tsx @@ -411,7 +411,7 @@ export default function TableSettingsDialog() { }, /* * TODO: Figure out where to store this settings - + { id: "function", title: "Cloud Function", diff --git a/src/constants/runRoutes.ts b/src/constants/runRoutes.ts index b1504c3e0..96916b5f1 100644 --- a/src/constants/runRoutes.ts +++ b/src/constants/runRoutes.ts @@ -37,6 +37,9 @@ export const runRoutes = { setFirestoreRules: { path: "/setFirestoreRules", method: "POST" } as RunRoute, listCollections: { path: "/listCollections", method: "GET" } as RunRoute, listSecrets: { path: "/listSecrets", method: "GET" } as RunRoute, + addSecret: { path: "/addSecret", method: "POST" } as RunRoute, + editSecret: { path: "/editSecret", method: "POST" } as RunRoute, + deleteSecret: { path: "/deleteSecret", method: "POST" } as RunRoute, serviceAccountAccess: { path: "/serviceAccountAccess", method: "GET",