From 323c412f5ea6a976d6073f87ab48639b5ed9c025 Mon Sep 17 00:00:00 2001 From: Rhythm Bhiwani Date: Sat, 2 Mar 2024 06:41:32 +0530 Subject: [PATCH 1/5] Added Option to Rename Secrets from overview page in all environments --- .../SecretOverviewTableRow.tsx | 8 + .../SecretRenameRow.tsx | 178 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx index 08774535b..719b17d44 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -15,6 +15,7 @@ import { useToggle } from "@app/hooks"; import { DecryptedSecret } from "@app/hooks/api/secrets/types"; import { SecretEditRow } from "./SecretEditRow"; +import SecretRenameRow from "./SecretRenameRow"; type Props = { secretKey: string; @@ -105,6 +106,13 @@ export const SecretOverviewTableRow = ({ width: `calc(${expandableColWidth}px - 1rem)` }} > + + diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx new file mode 100644 index 000000000..57c71527c --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -0,0 +1,178 @@ +import { Controller, useForm } from "react-hook-form"; +import { subject } from "@casl/ability"; +import { faCheck, faClose } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AnimatePresence, motion } from "framer-motion"; +import { twMerge } from "tailwind-merge"; + +import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { useGetUserWsKey, useUpdateSecretV3 } from "@app/hooks/api"; +import { DecryptedSecret } from "@app/hooks/api/types"; +import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils"; + +type Props = { + secretKey: string; + secretPath: string; + environments: { name: string; slug: string }[]; + getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; +}; + +type Form = { key: string }; + +function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }: Props) { + const { currentWorkspace } = useWorkspace(); + const { permission } = useProjectPermission(); + + const secrets = environments.map((env) => getSecretByKey(env.slug, secretKey)); + + const isReadOnly = environments.some((env) => { + const environment = env.slug; + const isSecretInEnvReadOnly = + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) && + permission.cannot( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + if (isSecretInEnvReadOnly) { + return true; + } + return false; + }); + + const isOverriden = secrets.some( + (secret) => + secret?.overrideAction === SecretActionType.Created || + secret?.overrideAction === SecretActionType.Modified + ); + const workspaceId = currentWorkspace?.id || ""; + + const { data: decryptFileKey } = useGetUserWsKey(workspaceId); + + const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); + + const { + handleSubmit, + control, + reset, + formState: { isDirty, isSubmitting } + } = useForm({ + defaultValues: { key: secretKey }, + values: { key: secretKey } + }); + + const handleFormSubmit = async (data: Form) => { + const promises = secrets + .filter((secret) => !!secret) + .map((secret) => { + if (!secret) return null; + + return updateSecretV3({ + environment: secret?.env, + workspaceId, + secretPath, + secretName: secret.key, + secretId: secret.id, + secretValue: secret.value || "", + type: "shared", + latestFileKey: decryptFileKey!, + tags: secret.tags.map((tag) => tag.id), + secretComment: secret.comment, + secretReminderRepeatDays: secret.reminderRepeatDays, + secretReminderNote: secret.reminderNote, + skipMultilineEncoding: secret.skipMultilineEncoding, + newSecretName: data.key + }); + }); + + await Promise.all(promises); + }; + + return ( + +
+ + Key + + + ( + + )} + /> +
+ {(isReadOnly || isOverriden) && ( + Read Only + )} + + {isDirty && ( + + + + {isSubmitting ? ( + + ) : ( + + )} + + + + reset()} + isDisabled={isSubmitting} + > + + + + + )} + + + ); +} + +export default SecretRenameRow; From 4dbee7df063262d468a99cd682fd15b2a51c9c24 Mon Sep 17 00:00:00 2001 From: Rhythm Bhiwani Date: Sat, 2 Mar 2024 06:45:52 +0530 Subject: [PATCH 2/5] Added notification on success and failure renaming secret --- .../SecretOverviewTableRow/SecretRenameRow.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index 57c71527c..30f4198c4 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -5,6 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { AnimatePresence, motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, @@ -28,6 +29,7 @@ type Form = { key: string }; function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }: Props) { const { currentWorkspace } = useWorkspace(); const { permission } = useProjectPermission(); + const { createNotification } = useNotificationContext(); const secrets = environments.map((env) => getSecretByKey(env.slug, secretKey)); @@ -93,7 +95,19 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } }); }); - await Promise.all(promises); + await Promise.all(promises) + .then(() => { + createNotification({ + type: "success", + text: "Successfully renamed the secret" + }); + }) + .catch(() => { + createNotification({ + type: "error", + text: "Error renaming the secret" + }); + }); }; return ( From b6f3cf512e3c76175a5278da9b4373ce9f9e5e12 Mon Sep 17 00:00:00 2001 From: Rhythm Bhiwani Date: Sat, 2 Mar 2024 06:57:36 +0530 Subject: [PATCH 3/5] spacing made consistent --- .../components/SecretOverviewTableRow/SecretRenameRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index 30f4198c4..176200cee 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -116,7 +116,7 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } className="secret-table relative mb-2 flex w-full flex-row items-center justify-between overflow-hidden rounded-lg border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter" >
- + Key From 3fa529dcb0dbb692183a61f671bc1cbfc0e6d20f Mon Sep 17 00:00:00 2001 From: Rhythm Bhiwani Date: Sat, 2 Mar 2024 09:30:03 +0530 Subject: [PATCH 4/5] Added error message if name is empty --- .../components/SecretOverviewTableRow/SecretRenameRow.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index 176200cee..e6e8381db 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -72,6 +72,14 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } }); const handleFormSubmit = async (data: Form) => { + if (!data.key) { + createNotification({ + type: "error", + text: "Secret name cannot be empty" + }); + return; + } + const promises = secrets .filter((secret) => !!secret) .map((secret) => { From d8860e1ce3d2a88e2e737d3555005cf915ed5118 Mon Sep 17 00:00:00 2001 From: Rhythm Bhiwani Date: Sun, 3 Mar 2024 02:49:35 +0530 Subject: [PATCH 5/5] Disabled submit button when renaming all keys if key name is empty --- frontend/package-lock.json | 2 +- .../SecretRenameRow.tsx | 168 ++++++++++++------ 2 files changed, 114 insertions(+), 56 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 72607a272..2ec5b5578 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,5 +1,5 @@ { - "name": "npm-proj-1709146141702-0.772936286416932EMIzNi", + "name": "frontend", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index e6e8381db..a2813abee 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -1,9 +1,12 @@ +import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { subject } from "@casl/ability"; -import { faCheck, faClose } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faClose, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; import { AnimatePresence, motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; +import { z } from "zod"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2"; @@ -13,6 +16,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { useToggle } from "@app/hooks"; import { useGetUserWsKey, useUpdateSecretV3 } from "@app/hooks/api"; import { DecryptedSecret } from "@app/hooks/api/types"; import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils"; @@ -24,7 +28,11 @@ type Props = { getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; }; -type Form = { key: string }; +export const formSchema = z.object({ + key: z.string().trim().min(1, { message: "Secret key is required" }) +}); + +type TFormSchema = z.infer; function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }: Props) { const { currentWorkspace } = useWorkspace(); @@ -59,19 +67,32 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } const { data: decryptFileKey } = useGetUserWsKey(workspaceId); + const [isSecNameCopied, setIsSecNameCopied] = useToggle(false); + const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); const { handleSubmit, control, reset, - formState: { isDirty, isSubmitting } - } = useForm
({ + trigger, + getValues, + formState: { isDirty, isSubmitting, errors } + } = useForm({ defaultValues: { key: secretKey }, - values: { key: secretKey } + values: { key: secretKey }, + resolver: zodResolver(formSchema) }); - const handleFormSubmit = async (data: Form) => { + useEffect(() => { + let timer: NodeJS.Timeout; + if (isSecNameCopied) { + timer = setTimeout(() => setIsSecNameCopied.off(), 2000); + } + return () => clearTimeout(timer); + }, [isSecNameCopied]); + + const handleFormSubmit = async (data: TFormSchema) => { if (!data.key) { createNotification({ type: "error", @@ -118,6 +139,12 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } }); }; + const copyTokenToClipboard = () => { + const [key] = getValues(["key"]); + navigator.clipboard.writeText(key as string); + setIsSecNameCopied.on(); + }; + return ( ( + render={({ field, fieldState: { error } }) => ( trigger("key")} + isError={Boolean(error)} {...field} - className="w-full px-2 focus:text-bunker-100 focus:ring-transparent" + className="w-full px-2 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent" /> )} />
- {(isReadOnly || isOverriden) && ( + + {isReadOnly || isOverriden ? ( Read Only + ) : ( +
+ + {!isDirty ? ( + + + + + + + + ) : ( + + + + {isSubmitting ? ( + + ) : ( + + )} + + + + reset()} + isDisabled={isSubmitting} + > + + + + + )} + +
)} - - {isDirty && ( - - - - {isSubmitting ? ( - - ) : ( - - )} - - - - reset()} - isDisabled={isSubmitting} - > - - - - - )} - ); }