From 22d5f97793cb0bd319bfbab82b67298e1d7bf6c6 Mon Sep 17 00:00:00 2001 From: Rhythm Bhiwani Date: Sat, 2 Mar 2024 11:14:56 +0530 Subject: [PATCH 1/2] Added Feature to Create Secrets and Folders in all envs from overview page --- .../ActionBar/CreateSecretImportForm.tsx | 2 +- .../SecretOverviewPage/SecretOverviewPage.tsx | 523 +++++++++++------- .../CreateSecretInAllEnvsForm.tsx | 156 ++++++ 3 files changed, 482 insertions(+), 199 deletions(-) create mode 100644 frontend/src/views/SecretOverviewPage/components/CreateSecretsSection/CreateSecretInAllEnvsForm.tsx diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateSecretImportForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateSecretImportForm.tsx index 8a97a799f..c178e7514 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateSecretImportForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateSecretImportForm.tsx @@ -81,7 +81,7 @@ export const CreateSecretImportForm = ({ }); } catch (err) { console.error(err); - const axiosError = err as AxiosError + const axiosError = err as AxiosError; if (axiosError?.response?.status === 401) { createNotification({ text: "You do not have access to the selected environment/path", diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index b3e0efb8a..ed4f490f8 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -2,22 +2,31 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; +import { subject } from "@casl/ability"; import { + faAngleDown, faArrowDown, faArrowUp, faFolderBlank, - faMagnifyingGlass + faFolderPlus, + faMagnifyingGlass, + faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import NavHeader from "@app/components/navigation/NavHeader"; -import { PermissionDeniedBanner } from "@app/components/permissions"; +import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions"; import { Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, EmptyState, IconButton, Input, + Modal, + ModalContent, Table, TableContainer, TableSkeleton, @@ -30,7 +39,13 @@ import { Tr } from "@app/components/v2"; import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert"; -import { useOrganization, useWorkspace } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useWorkspace +} from "@app/context"; +import { usePopUp } from "@app/hooks"; import { useCreateFolder, useCreateSecretV3, @@ -42,6 +57,8 @@ import { } from "@app/hooks/api"; import { ProjectVersion } from "@app/hooks/api/workspace/types"; +import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm"; +import { CreateSecretInAllEnvsForm } from "./components/CreateSecretsSection/CreateSecretInAllEnvsForm"; import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs"; import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection"; import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow"; @@ -110,6 +127,40 @@ export const SecretOverviewPage = () => { const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3(); const { mutateAsync: createFolder } = useCreateFolder(); + const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ + "addSecretsInAllEnvs", + "addFolder", + "misc" + ] as const); + + const handleFolderCreate = async (folderName: string) => { + const promises = userAvailableEnvs.map((env) => { + const environment = env.slug; + return createFolder({ + name: folderName, + path: secretPath, + environment, + projectId: workspaceId + }); + }); + + const results = await Promise.allSettled(promises); + const isFoldersAdded = results.some((result) => result.status === "fulfilled"); + + if (isFoldersAdded) { + handlePopUpClose("addFolder"); + createNotification({ + type: "success", + text: "Successfully created folder" + }); + } else { + createNotification({ + type: "error", + text: "Failed to create folder" + }); + } + }; + const handleSecretCreate = async (env: string, key: string, value: string) => { try { // create folder if not existing @@ -269,210 +320,286 @@ export const SecretOverviewPage = () => { filteredFolderNames?.length === 0; return ( -
- -
- -
-
-
-

Secrets Overview

-

- Inject your secrets using - - Infisical CLI - - , - - Infisical API - - , - - Infisical SDKs - - , and - - more - - . -

+ <> +
+ +
+
+
+
+

Secrets Overview

+

+ Inject your secrets using + + Infisical CLI + + , + + Infisical API + + , + + Infisical SDKs + + , and + + more + + . +

+
- {currentWorkspace?.version === ProjectVersion.V1 && ( - - )} + {currentWorkspace?.version === ProjectVersion.V1 && ( + + )} -
- -
- setSearchFilter(e.target.value)} - leftIcon={} - /> +
+ +
+
+ setSearchFilter(e.target.value)} + leftIcon={} + /> +
+
+ + {(isAllowed) => ( + + )} + + handlePopUpToggle("misc", isOpen)} + > + + + + + + +
+ + {(isAllowed) => ( + + )} + +
+
+
+
+
-
-
- - - - - - {userAvailableEnvs?.map(({ name, slug }, index) => { - const envSecKeyCount = getEnvSecretKeyCount(slug); - const missingKeyCount = secKeys.length - envSecKeyCount; - return ( -
-
- Name - setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))} - > - - -
-
-
-
+
+
-
+ handlePopUpToggle("addSecretsInAllEnvs", isOpen)} + onClose={() => handlePopUpClose("addSecretsInAllEnvs")} + decryptFileKey={latestFileKey!} + /> + handlePopUpToggle("addFolder", isOpen)} + > + + + + + ); }; diff --git a/frontend/src/views/SecretOverviewPage/components/CreateSecretsSection/CreateSecretInAllEnvsForm.tsx b/frontend/src/views/SecretOverviewPage/components/CreateSecretsSection/CreateSecretInAllEnvsForm.tsx new file mode 100644 index 000000000..bc716edff --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/CreateSecretsSection/CreateSecretInAllEnvsForm.tsx @@ -0,0 +1,156 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { Button, FormControl, Input, Modal, ModalContent, SecretInput } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateSecretV3 } from "@app/hooks/api"; +import { UserWsKeyPair } from "@app/hooks/api/types"; + +const typeSchema = z + .object({ + key: z.string(), + values: z.record(z.string().optional()) + }) + .refine((data) => data.key !== undefined, { + message: "Please enter secret name" + }); + +type TFormSchema = z.infer; + +type Props = { + workspaceId: string; + secretPath?: string; + decryptFileKey: UserWsKeyPair; + + // modal props + isOpen?: boolean; + onClose: () => void; + onTogglePopUp: (isOpen: boolean) => void; +}; + +export const CreateSecretInAllEnvsForm = ({ + workspaceId, + secretPath = "/", + decryptFileKey, + isOpen, + onClose, + onTogglePopUp +}: Props) => { + const { + register, + handleSubmit, + control, + reset, + setError, + formState: { isSubmitting, errors } + } = useForm({ resolver: zodResolver(typeSchema) }); + const { currentWorkspace } = useWorkspace(); + const environments = currentWorkspace?.environments || []; + + const { createNotification } = useNotificationContext(); + + const { mutateAsync: createSecretV3 } = useCreateSecretV3(); + + const handleFormSubmit = async ({ key, values }: TFormSchema) => { + if (!key) { + setError("key", { message: "Please enter secret name" }); + return; + } + + const promises = environments.map((env) => { + const environment = env.slug; + const value = values[environment] || ""; + + return createSecretV3({ + environment, + workspaceId, + secretPath, + secretName: key, + secretValue: value, + secretComment: "", + type: "shared", + latestFileKey: decryptFileKey + }); + }); + + const results = await Promise.allSettled(promises); + const isSecretsAdded = results.some((result) => result.status === "fulfilled"); + + if (isSecretsAdded) { + createNotification({ + type: "success", + text: "Secrets created successfully" + }); + onClose(); + reset(); + } else { + createNotification({ + type: "error", + text: "Failed to create secrets" + }); + } + }; + return ( + + +
+ + + + + {environments.map((env) => { + return ( + ( + + + + )} + /> + ); + })} + +
+ + +
+ +
+
+ ); +}; From fafd963a8a2cbcc585e0861df1c5083927a88545 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Mon, 4 Mar 2024 14:32:53 +0530 Subject: [PATCH 2/2] feat(ui): updated create secret form in overview to better ux --- .../src/components/v2/Checkbox/Checkbox.tsx | 4 +- .../SecretOverviewPage/SecretOverviewPage.tsx | 6 +- .../CreateSecretForm/CreateSecretForm.tsx | 222 ++++++++++++++++++ .../components/CreateSecretForm/index.tsx | 1 + .../CreateSecretInAllEnvsForm.tsx | 156 ------------ 5 files changed, 228 insertions(+), 161 deletions(-) create mode 100644 frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/CreateSecretForm/index.tsx delete mode 100644 frontend/src/views/SecretOverviewPage/components/CreateSecretsSection/CreateSecretInAllEnvsForm.tsx diff --git a/frontend/src/components/v2/Checkbox/Checkbox.tsx b/frontend/src/components/v2/Checkbox/Checkbox.tsx index 87e278119..dc0e1a491 100644 --- a/frontend/src/components/v2/Checkbox/Checkbox.tsx +++ b/frontend/src/components/v2/Checkbox/Checkbox.tsx @@ -30,7 +30,7 @@ export const Checkbox = ({
-
- handlePopUpToggle("addSecretsInAllEnvs", isOpen)} onClose={() => handlePopUpClose("addSecretsInAllEnvs")} decryptFileKey={latestFileKey!} diff --git a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx new file mode 100644 index 000000000..52eebed9e --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -0,0 +1,222 @@ +import { Controller, useForm } from "react-hook-form"; +import { faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + Checkbox, + FormControl, + FormLabel, + Input, + Modal, + ModalContent, + SecretInput, + Tooltip +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api"; +import { DecryptedSecret, UserWsKeyPair } from "@app/hooks/api/types"; + +const typeSchema = z + .object({ + key: z.string().min(1, "Key is required"), + value: z.string().optional(), + environments: z.record(z.boolean().optional()) + }) + .refine((data) => data.key !== undefined, { + message: "Please enter secret name" + }); + +type TFormSchema = z.infer; + +type Props = { + secretPath?: string; + decryptFileKey: UserWsKeyPair; + getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; + // modal props + isOpen?: boolean; + onClose: () => void; + onTogglePopUp: (isOpen: boolean) => void; +}; + +export const CreateSecretForm = ({ + secretPath = "/", + decryptFileKey, + isOpen, + getSecretByKey, + onClose, + onTogglePopUp +}: Props) => { + const { + register, + handleSubmit, + control, + reset, + watch, + formState: { isSubmitting, errors } + } = useForm({ resolver: zodResolver(typeSchema) }); + const newSecretKey = watch("key"); + + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?.id || ""; + const environments = currentWorkspace?.environments || []; + + const { createNotification } = useNotificationContext(); + + const { mutateAsync: createSecretV3 } = useCreateSecretV3(); + const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); + const { mutateAsync: createFolder } = useCreateFolder(); + + const handleFormSubmit = async ({ key, value, environments: selectedEnv }: TFormSchema) => { + const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]); + const isEnvironmentsSelected = environmentsSelected.length; + + if (!isEnvironmentsSelected) { + createNotification({ type: "error", text: "Select atleast one environment" }); + return; + } + + const promises = environmentsSelected.map(async (env) => { + const environment = env.slug; + // create folder if not existing + if (secretPath !== "/") { + // /hello/world -> [hello","world"] + const pathSegment = secretPath.split("/").filter(Boolean); + const parentPath = `/${pathSegment.slice(0, -1).join("/")}`; + const folderName = pathSegment.at(-1); + if (folderName && parentPath) { + await createFolder({ + projectId: workspaceId, + path: parentPath, + environment, + name: folderName + }); + } + } + + const isEdit = getSecretByKey(environment, key) !== undefined; + if (isEdit) { + return updateSecretV3({ + environment, + workspaceId, + secretPath, + secretName: key, + secretValue: value || "", + type: "shared", + latestFileKey: decryptFileKey + }); + } + + return createSecretV3({ + environment, + workspaceId, + secretPath, + secretName: key, + secretValue: value || "", + secretComment: "", + type: "shared", + latestFileKey: decryptFileKey + }); + }); + + const results = await Promise.allSettled(promises); + const isSecretsAdded = results.some((result) => result.status === "fulfilled"); + + if (isSecretsAdded) { + createNotification({ + type: "success", + text: "Secrets created successfully" + }); + onClose(); + reset(); + } else { + createNotification({ + type: "error", + text: "Failed to create secrets" + }); + } + }; + return ( + + +
+ + + + ( + + + + )} + /> + +
+ {environments.map((env) => { + return ( + ( + + {env.name} + {getSecretByKey(env.slug, newSecretKey) && ( + + + + )} + + )} + /> + ); + })} +
+
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/index.tsx b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/index.tsx new file mode 100644 index 000000000..ffbe98f57 --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/index.tsx @@ -0,0 +1 @@ +export { CreateSecretForm } from "./CreateSecretForm" diff --git a/frontend/src/views/SecretOverviewPage/components/CreateSecretsSection/CreateSecretInAllEnvsForm.tsx b/frontend/src/views/SecretOverviewPage/components/CreateSecretsSection/CreateSecretInAllEnvsForm.tsx deleted file mode 100644 index bc716edff..000000000 --- a/frontend/src/views/SecretOverviewPage/components/CreateSecretsSection/CreateSecretInAllEnvsForm.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { Controller, useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { Button, FormControl, Input, Modal, ModalContent, SecretInput } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { useCreateSecretV3 } from "@app/hooks/api"; -import { UserWsKeyPair } from "@app/hooks/api/types"; - -const typeSchema = z - .object({ - key: z.string(), - values: z.record(z.string().optional()) - }) - .refine((data) => data.key !== undefined, { - message: "Please enter secret name" - }); - -type TFormSchema = z.infer; - -type Props = { - workspaceId: string; - secretPath?: string; - decryptFileKey: UserWsKeyPair; - - // modal props - isOpen?: boolean; - onClose: () => void; - onTogglePopUp: (isOpen: boolean) => void; -}; - -export const CreateSecretInAllEnvsForm = ({ - workspaceId, - secretPath = "/", - decryptFileKey, - isOpen, - onClose, - onTogglePopUp -}: Props) => { - const { - register, - handleSubmit, - control, - reset, - setError, - formState: { isSubmitting, errors } - } = useForm({ resolver: zodResolver(typeSchema) }); - const { currentWorkspace } = useWorkspace(); - const environments = currentWorkspace?.environments || []; - - const { createNotification } = useNotificationContext(); - - const { mutateAsync: createSecretV3 } = useCreateSecretV3(); - - const handleFormSubmit = async ({ key, values }: TFormSchema) => { - if (!key) { - setError("key", { message: "Please enter secret name" }); - return; - } - - const promises = environments.map((env) => { - const environment = env.slug; - const value = values[environment] || ""; - - return createSecretV3({ - environment, - workspaceId, - secretPath, - secretName: key, - secretValue: value, - secretComment: "", - type: "shared", - latestFileKey: decryptFileKey - }); - }); - - const results = await Promise.allSettled(promises); - const isSecretsAdded = results.some((result) => result.status === "fulfilled"); - - if (isSecretsAdded) { - createNotification({ - type: "success", - text: "Secrets created successfully" - }); - onClose(); - reset(); - } else { - createNotification({ - type: "error", - text: "Failed to create secrets" - }); - } - }; - return ( - - -
- - - - - {environments.map((env) => { - return ( - ( - - - - )} - /> - ); - })} - -
- - -
- -
-
- ); -};