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 ( + ( + + + + )} + /> + ); + })} + +
+ + +
+ +
+
+ ); +};