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/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..a2813abee --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -0,0 +1,258 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { subject } from "@casl/ability"; +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"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + 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"; + +type Props = { + secretKey: string; + secretPath: string; + environments: { name: string; slug: string }[]; + getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; +}; + +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(); + const { permission } = useProjectPermission(); + const { createNotification } = useNotificationContext(); + + 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 [isSecNameCopied, setIsSecNameCopied] = useToggle(false); + + const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); + + const { + handleSubmit, + control, + reset, + trigger, + getValues, + formState: { isDirty, isSubmitting, errors } + } = useForm({ + defaultValues: { key: secretKey }, + values: { key: secretKey }, + resolver: zodResolver(formSchema) + }); + + 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", + text: "Secret name cannot be empty" + }); + return; + } + + 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) + .then(() => { + createNotification({ + type: "success", + text: "Successfully renamed the secret" + }); + }) + .catch(() => { + createNotification({ + type: "error", + text: "Error renaming the secret" + }); + }); + }; + + const copyTokenToClipboard = () => { + const [key] = getValues(["key"]); + navigator.clipboard.writeText(key as string); + setIsSecNameCopied.on(); + }; + + return ( +
+
+ + Key + + + ( + trigger("key")} + isError={Boolean(error)} + {...field} + className="w-full px-2 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent" + /> + )} + /> +
+ + {isReadOnly || isOverriden ? ( + Read Only + ) : ( +
+ + {!isDirty ? ( + + + + + + + + ) : ( + + + + {isSubmitting ? ( + + ) : ( + + )} + + + + reset()} + isDisabled={isSubmitting} + > + + + + + )} + +
+ )} + + ); +} + +export default SecretRenameRow;