Merge pull request #1322 from rowyio/feat/secret-management-popup

Add secret management popup
This commit is contained in:
Shams
2023-10-25 00:32:05 -07:00
committed by GitHub
8 changed files with 480 additions and 3 deletions

View File

@@ -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() {
<RequireAuth>
<Navigation>
<TableSettingsDialog />
<ProjectSettingsDialog />
</Navigation>
</RequireAuth>
}

View File

@@ -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<ProjectSettingsDialogState>) => {
set(projectSettingsDialogAtom, {
open: true,
tab: "secrets",
...update,
});
}
);
/**
* Store the current ID of the table being edited in tableSettingsDialog
* to derive tableSettingsDialogSchemaAtom

View File

@@ -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 (
<>
<Modal
onClose={handleClose}
open={open}
maxWidth="sm"
fullWidth
title={"Project settings"}
sx={{
".MuiDialogContent-root": {
display: "flex",
flexDirection: "column",
height: "100%",
},
}}
children={
<>
<TabContext value={tab}>
<Box
sx={{
borderBottom: 1,
borderColor: "divider",
}}
>
<TabList value={tab} onChange={handleTabChange}>
<Tab label="Secret keys" value={"secrets"} />
</TabList>
</Box>
<TabPanel
value={tab}
sx={{
overflowY: "scroll",
}}
>
<Paper elevation={1} variant={"outlined"}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: 3,
}}
>
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
Secrets
</Typography>
<Button
variant="contained"
color="primary"
onClick={() => {
setSecretDetailsModal({
open: true,
mode: "add",
});
}}
>
Add secret key
</Button>
</Box>
{secretNames.secretNames?.map((secretName) => (
<Box
key={secretName}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: 3,
borderTop: 1,
borderColor: "divider",
}}
>
<Typography variant="body2" color="text.secondary">
{secretName}
</Typography>
<Box>
<Tooltip title={"Edit"}>
<Button
variant="outlined"
color="primary"
style={{
minWidth: "40px",
paddingLeft: 0,
paddingRight: 0,
marginRight: "8px",
}}
onClick={() => {
setSecretDetailsModal({
open: true,
mode: "edit",
secretName,
});
}}
>
<EditIcon color={"secondary"} />
</Button>
</Tooltip>
<Tooltip title={"Delete"}>
<Button
variant="outlined"
color="primary"
style={{
minWidth: "40px",
paddingLeft: 0,
paddingRight: 0,
}}
onClick={() => {
console.log("setting", {
open: true,
mode: "delete",
secretName,
});
setSecretDetailsModal({
open: true,
mode: "delete",
secretName,
});
}}
>
<DeleteOutlineIcon color={"secondary"} />
</Button>
</Tooltip>
</Box>
</Box>
))}
</Paper>
</TabPanel>
</TabContext>
</>
}
/>
<SecretDetailsModal
open={secretDetailsModal.open}
mode={secretDetailsModal.mode}
error={secretDetailsModal.error}
loading={secretDetailsModal.loading}
secretName={secretDetailsModal.secretName}
handleClose={() => {
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,
});
}
}}
/>
</>
);
}

View File

@@ -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 (
<Modal
onClose={handleClose}
open={open}
maxWidth="xs"
fullWidth
title={`${capitalize(mode)} secret key`}
sx={{
".MuiDialogContent-root": {
display: "flex",
flexDirection: "column",
height: "100%",
},
}}
children={
<Box
sx={{
marginTop: 1,
}}
>
{mode === "add" && (
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
gap: 1,
}}
>
<Typography variant="subtitle2">Secret Name</Typography>
<TextField
fullWidth
variant="outlined"
value={newSecretName}
onChange={(e) => setNewSecretName(e.target.value)}
/>
<Typography
variant={"body2"}
color={"text.secondary"}
fontSize={"12px"}
>
This will create a secret key on Google Cloud.
</Typography>
</Box>
)}
{mode === "delete" ? (
<Typography>
Are you sure you want to delete this secret key {secretName}?
</Typography>
) : (
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
gap: 1,
marginTop: 2,
}}
>
<Typography variant="subtitle2">Secret Value</Typography>
<TextField
fullWidth
variant="outlined"
value={secretValue}
onChange={(e) => setSecretValue(e.target.value)}
/>
<Typography
variant={"body2"}
color={"text.secondary"}
fontSize={"12px"}
>
Paste your secret key here.
</Typography>
</Box>
)}
{error?.length && (
<Typography color={"error"} marginTop={2}>
{error}
</Typography>
)}
<Box
sx={{
display: "flex",
justifyContent: "flex-start",
gap: 1,
marginTop: 4,
}}
>
<Button
variant="outlined"
onClick={handleClose}
sx={{ textTransform: "none" }}
>
Cancel
</Button>
<LoadingButton
variant="contained"
color={"primary"}
loading={loading}
disabled={
(mode === "add" && (!newSecretName || !secretValue)) ||
(mode === "edit" && !secretValue)
}
onClick={() => {
switch (mode) {
case "add":
handleAdd(newSecretName, secretValue);
break;
case "edit":
handleEdit(secretName ?? "", secretValue);
break;
case "delete":
handleDelete(secretName ?? "");
break;
}
}}
>
{mode === "delete" ? "Delete" : "Save"}
</LoadingButton>
</Box>
</Box>
}
/>
);
}

View File

@@ -0,0 +1,2 @@
export * from "./ProjectSettingsDialog";
export { default } from "./ProjectSettingsDialog";

View File

@@ -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 = {
})}
<MenuItem
onClick={() => {
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

View File

@@ -411,7 +411,7 @@ export default function TableSettingsDialog() {
},
/*
* TODO: Figure out where to store this settings
{
id: "function",
title: "Cloud Function",

View File

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