import { ChangeEvent, DragEvent } from "react"; import { useTranslation } from "react-i18next"; import { subject } from "@casl/ability"; import { faUpload } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQueryClient } from "@tanstack/react-query"; import { twMerge } from "tailwind-merge"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { ProjectPermissionCan } from "@app/components/permissions"; // TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality import { parseDotEnv } from "@app/components/utilities/parseDotEnv"; import { Button, Modal, ModalContent } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api"; import { secretKeys } from "@app/hooks/api/secrets/queries"; import { DecryptedSecret, UserWsKeyPair } from "@app/hooks/api/types"; import { CopySecretsFromBoard } from "./CopySecretsFromBoard"; const parseJson = (src: ArrayBuffer) => { const file = src.toString(); const formatedData: Record = JSON.parse(file); const env: Record = {}; Object.keys(formatedData).forEach((key) => { if (typeof formatedData[key] === "string") { env[key] = { value: formatedData[key], comments: [] }; } }); return env; }; type TParsedEnv = Record; type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv }; type Props = { isSmaller: boolean; environments?: { name: string; slug: string }[]; workspaceId: string; decryptFileKey: UserWsKeyPair; environment: string; secretPath: string; secrets?: DecryptedSecret[]; }; export const SecretDropzone = ({ isSmaller, environments = [], workspaceId, decryptFileKey, environment, secretPath, secrets = [] }: Props): JSX.Element => { const { t } = useTranslation(); const [isDragActive, setDragActive] = useToggle(); const [isLoading, setIsLoading] = useToggle(); const { createNotification } = useNotificationContext(); const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ "importSecEnv", "overlapKeyWarning" ] as const); const queryClient = useQueryClient(); const { mutateAsync: updateSecretBatch, isLoading: isUpdatingSecrets } = useUpdateSecretBatch({ options: { onSuccess: undefined } }); const { mutateAsync: createSecretBatch, isLoading: isCreatingSecrets } = useCreateSecretBatch({ options: { onSuccess: undefined } }); const isSubmitting = isCreatingSecrets || isUpdatingSecrets; const handleDrag = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") { setDragActive.on(); } else if (e.type === "dragleave") { setDragActive.off(); } }; const handleParsedEnv = (env: TParsedEnv) => { const secretsGroupedByKey = secrets?.reduce>( (prev, curr) => ({ ...prev, [curr.key]: true }), {} ); const overlappedSecrets = Object.keys(env) .filter((secKey) => secretsGroupedByKey?.[secKey]) .reduce((prev, curr) => ({ ...prev, [curr]: env[curr] }), {}); const nonOverlappedSecrets = Object.keys(env) .filter((secKey) => !secretsGroupedByKey?.[secKey]) .reduce((prev, curr) => ({ ...prev, [curr]: env[curr] }), {}); if (!Object.keys(overlappedSecrets).length && !Object.keys(nonOverlappedSecrets).length) { createNotification({ type: "error", text: "Failed to find secrets" }); return; } handlePopUpOpen("overlapKeyWarning", { update: overlappedSecrets, create: nonOverlappedSecrets }); }; const parseFile = (file?: File, isJson?: boolean) => { const reader = new FileReader(); if (!file) { createNotification({ text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.", type: "error", timeoutMs: 10000 }); return; } // const fileType = file.name.split('.')[1]; setIsLoading.on(); reader.onload = (event) => { if (!event?.target?.result) return; // parse function's argument looks like to be ArrayBuffer const env = isJson ? parseJson(event.target.result as ArrayBuffer) : parseDotEnv(event.target.result as ArrayBuffer); setIsLoading.off(); handleParsedEnv(env); }; // If something is wrong show an error try { reader.readAsText(file); } catch (error) { console.log(error); } }; const handleDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!e.dataTransfer) { return; } e.dataTransfer.dropEffect = "copy"; setDragActive.off(); parseFile(e.dataTransfer.files[0]); }; const handleFileUpload = (e: ChangeEvent) => { e.preventDefault(); parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json"); }; const handleSaveSecrets = async () => { const { update, create } = popUp?.overlapKeyWarning?.data as TSecOverwriteOpt; try { if (Object.keys(create || {}).length) { await createSecretBatch({ latestFileKey: decryptFileKey!, secretPath, workspaceId, environment, secrets: Object.entries(create).map(([secretName, secData]) => ({ type: "shared", secretComment: secData.comments.join("\n"), secretValue: secData.value, secretName })) }); } if (Object.keys(update || {}).length) { await updateSecretBatch({ latestFileKey: decryptFileKey!, secretPath, workspaceId, environment, secrets: Object.entries(update).map(([secretName, secData]) => ({ type: "shared", secretComment: secData.comments.join("\n"), secretValue: secData.value, secretName })) }); } queryClient.invalidateQueries( secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) ); handlePopUpClose("overlapKeyWarning"); createNotification({ type: "success", text: "Successfully uploaded secrets" }); } catch (err) { console.log(err); createNotification({ type: "error", text: "Failed to upload secrets" }); } }; const isUploadedDuplicateSecretsEmpty = !Object.keys( (popUp.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {} ).length; return (
{isLoading ? (
loading animation
) : (

{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}

{(isAllowed) => ( )}

OR

handlePopUpToggle("importSecEnv", isOpen)} onParsedEnv={handleParsedEnv} environment={environment} environments={environments} workspaceId={workspaceId} decryptFileKey={decryptFileKey} secretPath={secretPath} /> {!isSmaller && ( {(isAllowed) => ( )} )}
)}
handlePopUpToggle("overlapKeyWarning", open)} > {isUploadedDuplicateSecretsEmpty ? "Upload" : "Overwrite"} , ]} > {isUploadedDuplicateSecretsEmpty ? (
Upload secrets from this file
) : (
Your file contains following duplicate secrets
{Object.keys((popUp?.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {}) ?.map((key) => key) .join(", ")}
Are you sure you want to overwrite these secrets and create other ones?
)}
); };