From 5c6b7ed95c875d2b25d0e5d11604b5b841ce1337 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 20 Oct 2025 21:28:05 -0300 Subject: [PATCH 1/3] Use commit batch logic on SecretDropzone --- .../SecretDashboardPage.tsx | 1 - .../SecretDropzone/SecretDropzone.tsx | 170 ++++++++++-------- 2 files changed, 96 insertions(+), 75 deletions(-) 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 = { @@ -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,12 @@ 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 [isSubmitting, setIsSubmitting] = useToggle(); const handleDrag = (e: DragEvent) => { e.preventDefault(); @@ -193,29 +190,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", { update: updateSecrets, - create: createSecrets + create: createSecrets, + existingSecrets: relevantExistingSecrets }); } catch (e) { console.error(e); @@ -329,56 +335,72 @@ export const SecretDropzone = ({ }; const handleSaveSecrets = async () => { - const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt; + const { update, create, existingSecrets } = popUp?.confirmUpload?.data as TSecOverwriteOpt; + try { + setIsSubmitting.on(); + + const context: BatchContext = { + projectId, + environment, + secretPath + }; + + const existingSecretsMap = existingSecrets.reduce>( + (prev, curr) => ({ ...prev, [curr.key]: curr }), + {} + ); + 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"), + Object.entries(create).forEach(([secretKey, secData]) => { + const createChange: PendingSecretCreate = { + id: secretKey, + timestamp: Date.now(), + resourceType: "secret", + type: PendingAction.Create, + secretKey, secretValue: secData.value, - secretKey - })) + secretComment: secData.comments.join("\n") || undefined, + tags: [], + secretMetadata: [] + }; + addPendingChange(createChange, context); }); } + 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"), + 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, - secretKey - })) + 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); }); } - 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" - }); + } finally { + setIsSubmitting.off(); } }; @@ -541,7 +563,7 @@ export const SecretDropzone = ({ ? ` and import ${createSecretCount} new one${createSecretCount > 1 ? "s" : ""}` : ""} - ? + ? These will be applied when you commit your changes. )} From 3a61ce07365fc6f5ec0dcda06c3ec6476c8b6dac Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 20 Oct 2025 21:44:39 -0300 Subject: [PATCH 2/3] Lint fix --- .../components/SecretDropzone/SecretDropzone.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx index 5d8ea59c3..ded34f9d4 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx @@ -38,12 +38,12 @@ import { fetchProjectSecrets, mergePersonalSecrets } from "@app/hooks/api/secret import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { - PopUpNames, - usePopUpAction, - useBatchModeActions, + BatchContext, PendingSecretCreate, PendingSecretUpdate, - BatchContext + PopUpNames, + useBatchModeActions, + usePopUpAction } from "../../SecretMainPage.store"; import { CopySecretsFromBoard } from "./CopySecretsFromBoard"; import { PasteSecretEnvModal } from "./PasteSecretEnvModal"; From 0df1a7ec14744bfed2ad4555eba445d364ad8fce Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 22 Oct 2025 00:06:11 -0300 Subject: [PATCH 3/3] Remove confirmation modal for SecretDropzone as changes are now applied on commit --- .../SecretDropzone/SecretDropzone.tsx | 213 +++++++----------- 1 file changed, 77 insertions(+), 136 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx index ded34f9d4..79ac12df5 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx @@ -69,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, @@ -165,7 +165,6 @@ export const SecretDropzone = ({ // hide copy secrets from board due to import folders feature const shouldRenderCopySecrets = false; - const [isSubmitting, setIsSubmitting] = useToggle(); const handleDrag = (e: DragEvent) => { e.preventDefault(); @@ -177,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); @@ -218,7 +292,7 @@ export const SecretDropzone = ({ } }); - handlePopUpOpen("confirmUpload", { + await handleSaveSecrets({ update: updateSecrets, create: createSecrets, existingSecrets: relevantExistingSecrets @@ -229,7 +303,6 @@ export const SecretDropzone = ({ text: "Failed to check for secret conflicts", type: "error" }); - handlePopUpClose("confirmUpload"); } finally { setIsLoading.off(); } @@ -334,86 +407,6 @@ export const SecretDropzone = ({ parseFile(e.target?.files?.[0]); }; - const handleSaveSecrets = async () => { - const { update, create, existingSecrets } = popUp?.confirmUpload?.data as TSecOverwriteOpt; - - try { - setIsSubmitting.on(); - - const context: BatchContext = { - projectId, - environment, - secretPath - }; - - const existingSecretsMap = existingSecrets.reduce>( - (prev, curr) => ({ ...prev, [curr.key]: curr }), - {} - ); - - 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); - }); - } - - handlePopUpClose("confirmUpload"); - } catch (err) { - console.log(err); - } finally { - setIsSubmitting.off(); - } - }; - - 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" : ""}` - : ""} - ? These will be applied when you commit your changes. -
-
- )} -
-
{/* Matrix Import Modal */}