Use commit batch logic on SecretDropzone

This commit is contained in:
Carlos Monastyrski
2025-10-20 21:28:05 -03:00
parent 7224ff9ece
commit 5c6b7ed95c
2 changed files with 96 additions and 75 deletions
@@ -1109,7 +1109,6 @@ const Page = () => {
secretPath={secretPath} secretPath={secretPath}
isSmaller={isNotEmpty} isSmaller={isNotEmpty}
environments={currentProject?.environments} environments={currentProject?.environments}
isProtectedBranch={isProtectedBranch}
/> />
<PitDrawer <PitDrawer
secretSnaphots={snapshotList} secretSnaphots={snapshotList}
@@ -10,7 +10,6 @@ import {
faUpload faUpload
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -34,21 +33,27 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api"; import { PendingAction } from "@app/hooks/api/secretFolders/types";
import { import { fetchProjectSecrets, mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
dashboardKeys, import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
fetchDashboardProjectSecretsByKeys
} from "@app/hooks/api/dashboard/queries";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { SecretType } from "@app/hooks/api/types";
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store"; import {
PopUpNames,
usePopUpAction,
useBatchModeActions,
PendingSecretCreate,
PendingSecretUpdate,
BatchContext
} from "../../SecretMainPage.store";
import { CopySecretsFromBoard } from "./CopySecretsFromBoard"; import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
import { PasteSecretEnvModal } from "./PasteSecretEnvModal"; import { PasteSecretEnvModal } from "./PasteSecretEnvModal";
type TParsedEnv = Record<string, { value: string; comments: string[] }>; type TParsedEnv = Record<string, { value: string; comments: string[] }>;
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv }; type TSecOverwriteOpt = {
update: TParsedEnv;
create: TParsedEnv;
existingSecrets: SecretV3RawSanitized[];
};
type Props = { type Props = {
isSmaller: boolean; isSmaller: boolean;
@@ -56,7 +61,6 @@ type Props = {
projectId: string; projectId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
isProtectedBranch?: boolean;
}; };
type SecretMatrixMap = { type SecretMatrixMap = {
@@ -142,8 +146,7 @@ export const SecretDropzone = ({
environments = [], environments = [],
projectId, projectId,
environment, environment,
secretPath, secretPath
isProtectedBranch = false
}: Props): JSX.Element => { }: Props): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isDragActive, setDragActive] = useToggle(); const [isDragActive, setDragActive] = useToggle();
@@ -157,18 +160,12 @@ export const SecretDropzone = ({
}); });
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp(popupKeys); const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp(popupKeys);
const queryClient = useQueryClient();
const { openPopUp } = usePopUpAction(); 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 // hide copy secrets from board due to import folders feature
const shouldRenderCopySecrets = false; const shouldRenderCopySecrets = false;
const isSubmitting = isCreatingSecrets || isUpdatingSecrets; const [isSubmitting, setIsSubmitting] = useToggle();
const handleDrag = (e: DragEvent) => { const handleDrag = (e: DragEvent) => {
e.preventDefault(); e.preventDefault();
@@ -193,29 +190,38 @@ export const SecretDropzone = ({
try { try {
setIsLoading.on(); setIsLoading.on();
const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({ const { secrets: rawExistingSecrets } = await fetchProjectSecrets({
secretPath,
environment,
projectId, projectId,
keys: envSecretKeys environment,
secretPath,
viewSecretValue: true
}); });
const secretsGroupedByKey = existingSecrets.reduce<Record<string, boolean>>( const allExistingSecrets = mergePersonalSecrets(rawExistingSecrets);
(prev, curr) => ({ ...prev, [curr.secretKey]: true }),
const existingSecretsMap = allExistingSecrets.reduce<Record<string, SecretV3RawSanitized>>(
(prev, curr) => ({ ...prev, [curr.key]: curr }),
{} {}
); );
const updateSecrets = Object.keys(env) const updateSecrets: TParsedEnv = {};
.filter((secKey) => secretsGroupedByKey[secKey]) const createSecrets: TParsedEnv = {};
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {}); const relevantExistingSecrets: SecretV3RawSanitized[] = [];
const createSecrets = Object.keys(env) Object.entries(env).forEach(([secretKey, secretData]) => {
.filter((secKey) => !secretsGroupedByKey[secKey]) const existingSecret = existingSecretsMap[secretKey];
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {}); if (existingSecret) {
updateSecrets[secretKey] = secretData;
relevantExistingSecrets.push(existingSecret);
} else {
createSecrets[secretKey] = secretData;
}
});
handlePopUpOpen("confirmUpload", { handlePopUpOpen("confirmUpload", {
update: updateSecrets, update: updateSecrets,
create: createSecrets create: createSecrets,
existingSecrets: relevantExistingSecrets
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -329,56 +335,72 @@ export const SecretDropzone = ({
}; };
const handleSaveSecrets = async () => { const handleSaveSecrets = async () => {
const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt; const { update, create, existingSecrets } = popUp?.confirmUpload?.data as TSecOverwriteOpt;
try { try {
setIsSubmitting.on();
const context: BatchContext = {
projectId,
environment,
secretPath
};
const existingSecretsMap = existingSecrets.reduce<Record<string, SecretV3RawSanitized>>(
(prev, curr) => ({ ...prev, [curr.key]: curr }),
{}
);
if (Object.keys(create || {}).length) { if (Object.keys(create || {}).length) {
await createSecretBatch({ Object.entries(create).forEach(([secretKey, secData]) => {
secretPath, const createChange: PendingSecretCreate = {
projectId, id: secretKey,
environment, timestamp: Date.now(),
secrets: Object.entries(create).map(([secretKey, secData]) => ({ resourceType: "secret",
type: SecretType.Shared, type: PendingAction.Create,
secretComment: secData.comments.join("\n"), secretKey,
secretValue: secData.value, secretValue: secData.value,
secretKey secretComment: secData.comments.join("\n") || undefined,
})) tags: [],
secretMetadata: []
};
addPendingChange(createChange, context);
}); });
} }
if (Object.keys(update || {}).length) { if (Object.keys(update || {}).length) {
await updateSecretBatch({ Object.entries(update).forEach(([secretKey, secData]) => {
secretPath, const existingSecret = existingSecretsMap[secretKey];
projectId,
environment, if (!existingSecret) {
secrets: Object.entries(update).map(([secretKey, secData]) => ({ console.warn(`Existing secret not found for key: ${secretKey}`);
type: SecretType.Shared, return;
secretComment: secData.comments.join("\n"), }
const updateChange: PendingSecretUpdate = {
id: existingSecret.id,
timestamp: Date.now(),
resourceType: "secret",
type: PendingAction.Update,
secretKey,
secretValue: secData.value, 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"); handlePopUpClose("confirmUpload");
createNotification({
type: "success",
text: isProtectedBranch
? "Uploaded changes have been sent for review"
: "Successfully uploaded secrets"
});
} catch (err) { } catch (err) {
console.log(err); console.log(err);
createNotification({ } finally {
type: "error", setIsSubmitting.off();
text: "Failed to upload secrets"
});
} }
}; };
@@ -541,7 +563,7 @@ export const SecretDropzone = ({
? ` and import ${createSecretCount} new ? ` and import ${createSecretCount} new
one${createSecretCount > 1 ? "s" : ""}` one${createSecretCount > 1 ? "s" : ""}`
: ""} : ""}
? ? These will be applied when you commit your changes.
</div> </div>
</div> </div>
)} )}