mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
implement secret manager popup
This commit is contained in:
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
285
src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx
Normal file
285
src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
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);
|
||||
|
||||
console.log("open", open);
|
||||
console.log("tab", tab);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
158
src/components/ProjectSettingsDialog/SecretDetailsModal.tsx
Normal file
158
src/components/ProjectSettingsDialog/SecretDetailsModal.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
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("");
|
||||
|
||||
console.log(open);
|
||||
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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
2
src/components/ProjectSettingsDialog/index.ts
Normal file
2
src/components/ProjectSettingsDialog/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ProjectSettingsDialog";
|
||||
export { default } from "./ProjectSettingsDialog";
|
||||
@@ -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
|
||||
|
||||
@@ -411,7 +411,7 @@ export default function TableSettingsDialog() {
|
||||
},
|
||||
/*
|
||||
* TODO: Figure out where to store this settings
|
||||
|
||||
|
||||
{
|
||||
id: "function",
|
||||
title: "Cloud Function",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user