diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index caef19326..f1a7ffba8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -1109,7 +1109,6 @@ const Page = () => { secretPath={secretPath} isSmaller={isNotEmpty} environments={currentProject?.environments} - isProtectedBranch={isProtectedBranch} /> ; -type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv }; +type TSecOverwriteOpt = { + update: TParsedEnv; + create: TParsedEnv; + existingSecrets: SecretV3RawSanitized[]; +}; type Props = { isSmaller: boolean; @@ -56,7 +61,6 @@ type Props = { projectId: string; environment: string; secretPath: string; - isProtectedBranch?: boolean; }; type SecretMatrixMap = { @@ -65,7 +69,7 @@ type SecretMatrixMap = { comment: number | null; }; -const popupKeys = ["importSecEnv", "confirmUpload", "pasteSecEnv", "importMatrixMap"] as const; +const popupKeys = ["importSecEnv", "pasteSecEnv", "importMatrixMap"] as const; const MatrixImportModalTableRow = ({ importSecretMatrixMap, @@ -142,8 +146,7 @@ export const SecretDropzone = ({ environments = [], projectId, environment, - secretPath, - isProtectedBranch = false + secretPath }: Props): JSX.Element => { const { t } = useTranslation(); const [isDragActive, setDragActive] = useToggle(); @@ -157,18 +160,11 @@ export const SecretDropzone = ({ }); const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp(popupKeys); - const queryClient = useQueryClient(); const { openPopUp } = usePopUpAction(); + const { addPendingChange } = useBatchModeActions(); - const { mutateAsync: updateSecretBatch, isPending: isUpdatingSecrets } = useUpdateSecretBatch({ - options: { onSuccess: undefined } - }); - const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({ - options: { onSuccess: undefined } - }); // hide copy secrets from board due to import folders feature const shouldRenderCopySecrets = false; - const isSubmitting = isCreatingSecrets || isUpdatingSecrets; const handleDrag = (e: DragEvent) => { e.preventDefault(); @@ -180,6 +176,81 @@ export const SecretDropzone = ({ } }; + const handleSaveSecrets = async (data: TSecOverwriteOpt) => { + const { update, create, existingSecrets } = data; + + try { + const context: BatchContext = { + projectId, + environment, + secretPath + }; + + const existingSecretsMap = existingSecrets.reduce>( + (prev, curr) => ({ ...prev, [curr.key]: curr }), + {} + ); + + const totalCount = Object.keys(create || {}).length + Object.keys(update || {}).length; + + if (Object.keys(create || {}).length) { + Object.entries(create).forEach(([secretKey, secData]) => { + const createChange: PendingSecretCreate = { + id: secretKey, + timestamp: Date.now(), + resourceType: "secret", + type: PendingAction.Create, + secretKey, + secretValue: secData.value, + secretComment: secData.comments.join("\n") || undefined, + tags: [], + secretMetadata: [] + }; + addPendingChange(createChange, context); + }); + } + + if (Object.keys(update || {}).length) { + Object.entries(update).forEach(([secretKey, secData]) => { + const existingSecret = existingSecretsMap[secretKey]; + + if (!existingSecret) { + console.warn(`Existing secret not found for key: ${secretKey}`); + return; + } + + const updateChange: PendingSecretUpdate = { + id: existingSecret.id, + timestamp: Date.now(), + resourceType: "secret", + type: PendingAction.Update, + secretKey, + secretValue: secData.value, + secretComment: secData.comments.join("\n") || undefined, + existingSecret, + originalValue: existingSecret.value || "", + originalComment: existingSecret.comment || "", + originalSkipMultilineEncoding: existingSecret.skipMultilineEncoding || false, + originalTags: existingSecret.tags || [], + originalSecretMetadata: existingSecret.secretMetadata || [] + }; + addPendingChange(updateChange, context); + }); + } + + createNotification({ + type: "success", + text: `Successfully imported ${totalCount} secret${totalCount > 1 ? "s" : ""}.` + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to import secrets" + }); + } + }; + const handleParsedEnv = async (env: TParsedEnv) => { const envSecretKeys = Object.keys(env); @@ -193,29 +264,38 @@ export const SecretDropzone = ({ try { setIsLoading.on(); - const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({ - secretPath, - environment, + const { secrets: rawExistingSecrets } = await fetchProjectSecrets({ projectId, - keys: envSecretKeys + environment, + secretPath, + viewSecretValue: true }); - const secretsGroupedByKey = existingSecrets.reduce>( - (prev, curr) => ({ ...prev, [curr.secretKey]: true }), + const allExistingSecrets = mergePersonalSecrets(rawExistingSecrets); + + const existingSecretsMap = allExistingSecrets.reduce>( + (prev, curr) => ({ ...prev, [curr.key]: curr }), {} ); - const updateSecrets = Object.keys(env) - .filter((secKey) => secretsGroupedByKey[secKey]) - .reduce((prev, curr) => ({ ...prev, [curr]: env[curr] }), {}); + const updateSecrets: TParsedEnv = {}; + const createSecrets: TParsedEnv = {}; + const relevantExistingSecrets: SecretV3RawSanitized[] = []; - const createSecrets = Object.keys(env) - .filter((secKey) => !secretsGroupedByKey[secKey]) - .reduce((prev, curr) => ({ ...prev, [curr]: env[curr] }), {}); + Object.entries(env).forEach(([secretKey, secretData]) => { + const existingSecret = existingSecretsMap[secretKey]; + if (existingSecret) { + updateSecrets[secretKey] = secretData; + relevantExistingSecrets.push(existingSecret); + } else { + createSecrets[secretKey] = secretData; + } + }); - handlePopUpOpen("confirmUpload", { + await handleSaveSecrets({ update: updateSecrets, - create: createSecrets + create: createSecrets, + existingSecrets: relevantExistingSecrets }); } catch (e) { console.error(e); @@ -223,7 +303,6 @@ export const SecretDropzone = ({ text: "Failed to check for secret conflicts", type: "error" }); - handlePopUpClose("confirmUpload"); } finally { setIsLoading.off(); } @@ -328,70 +407,6 @@ export const SecretDropzone = ({ parseFile(e.target?.files?.[0]); }; - const handleSaveSecrets = async () => { - const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt; - try { - if (Object.keys(create || {}).length) { - await createSecretBatch({ - secretPath, - projectId, - environment, - secrets: Object.entries(create).map(([secretKey, secData]) => ({ - type: SecretType.Shared, - secretComment: secData.comments.join("\n"), - secretValue: secData.value, - secretKey - })) - }); - } - if (Object.keys(update || {}).length) { - await updateSecretBatch({ - secretPath, - projectId, - environment, - secrets: Object.entries(update).map(([secretKey, secData]) => ({ - type: SecretType.Shared, - secretComment: secData.comments.join("\n"), - secretValue: secData.value, - secretKey - })) - }); - } - queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) - }); - queryClient.invalidateQueries({ - queryKey: secretApprovalRequestKeys.count({ projectId }) - }); - handlePopUpClose("confirmUpload"); - createNotification({ - type: "success", - text: isProtectedBranch - ? "Uploaded changes have been sent for review" - : "Successfully uploaded secrets" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to upload secrets" - }); - } - }; - - const createSecretCount = Object.keys( - (popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {} - ).length; - - const updateSecretCount = Object.keys( - (popUp.confirmUpload?.data as TSecOverwriteOpt)?.update || {} - ).length; - - const isNonConflictingUpload = !updateSecretCount; - return (
)}
- handlePopUpToggle("confirmUpload", open)} - > - - {isNonConflictingUpload ? "Upload" : "Overwrite"} - , - - ]} - > - {isNonConflictingUpload ? ( -
- Are you sure you want to import {createSecretCount} secret - {createSecretCount > 1 ? "s" : ""} to this environment? -
- ) : ( -
-
Your project already contains the following {updateSecretCount} secrets:
-
- {Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {}) - ?.map((key) => key) - .join(", ")} -
-
- Are you sure you want to overwrite these secrets - {createSecretCount > 0 - ? ` and import ${createSecretCount} new - one${createSecretCount > 1 ? "s" : ""}` - : ""} - ? -
-
- )} -
-
{/* Matrix Import Modal */}