mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 16:27:46 +00:00
Merge pull request #4707 from Infisical/feat/ENG-3961
Improve SecretDropzone to add all imported values to pending changes
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
+117
-154
@@ -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 {
|
||||||
|
BatchContext,
|
||||||
|
PendingSecretCreate,
|
||||||
|
PendingSecretUpdate,
|
||||||
|
PopUpNames,
|
||||||
|
useBatchModeActions,
|
||||||
|
usePopUpAction
|
||||||
|
} 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 = {
|
||||||
@@ -65,7 +69,7 @@ type SecretMatrixMap = {
|
|||||||
comment: number | null;
|
comment: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const popupKeys = ["importSecEnv", "confirmUpload", "pasteSecEnv", "importMatrixMap"] as const;
|
const popupKeys = ["importSecEnv", "pasteSecEnv", "importMatrixMap"] as const;
|
||||||
|
|
||||||
const MatrixImportModalTableRow = ({
|
const MatrixImportModalTableRow = ({
|
||||||
importSecretMatrixMap,
|
importSecretMatrixMap,
|
||||||
@@ -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,11 @@ 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 handleDrag = (e: DragEvent) => {
|
const handleDrag = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
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<Record<string, SecretV3RawSanitized>>(
|
||||||
|
(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 handleParsedEnv = async (env: TParsedEnv) => {
|
||||||
const envSecretKeys = Object.keys(env);
|
const envSecretKeys = Object.keys(env);
|
||||||
|
|
||||||
@@ -193,29 +264,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", {
|
await handleSaveSecrets({
|
||||||
update: updateSecrets,
|
update: updateSecrets,
|
||||||
create: createSecrets
|
create: createSecrets,
|
||||||
|
existingSecrets: relevantExistingSecrets
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -223,7 +303,6 @@ export const SecretDropzone = ({
|
|||||||
text: "Failed to check for secret conflicts",
|
text: "Failed to check for secret conflicts",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
handlePopUpClose("confirmUpload");
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading.off();
|
setIsLoading.off();
|
||||||
}
|
}
|
||||||
@@ -328,70 +407,6 @@ export const SecretDropzone = ({
|
|||||||
parseFile(e.target?.files?.[0]);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -495,58 +510,6 @@ export const SecretDropzone = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
|
||||||
isOpen={popUp?.confirmUpload?.isOpen}
|
|
||||||
onOpenChange={(open) => handlePopUpToggle("confirmUpload", open)}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title="Confirm Secret Upload"
|
|
||||||
footerContent={[
|
|
||||||
<Button
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
colorSchema={isNonConflictingUpload ? "primary" : "danger"}
|
|
||||||
key="overwrite-btn"
|
|
||||||
onClick={handleSaveSecrets}
|
|
||||||
>
|
|
||||||
{isNonConflictingUpload ? "Upload" : "Overwrite"}
|
|
||||||
</Button>,
|
|
||||||
<Button
|
|
||||||
key="keep-old-btn"
|
|
||||||
className="ml-4"
|
|
||||||
onClick={() => handlePopUpClose("confirmUpload")}
|
|
||||||
variant="outline_bg"
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{isNonConflictingUpload ? (
|
|
||||||
<div>
|
|
||||||
Are you sure you want to import {createSecretCount} secret
|
|
||||||
{createSecretCount > 1 ? "s" : ""} to this environment?
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col text-gray-300">
|
|
||||||
<div>Your project already contains the following {updateSecretCount} secrets:</div>
|
|
||||||
<div className="mt-2 text-sm text-gray-400">
|
|
||||||
{Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {})
|
|
||||||
?.map((key) => key)
|
|
||||||
.join(", ")}
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
Are you sure you want to overwrite these secrets
|
|
||||||
{createSecretCount > 0
|
|
||||||
? ` and import ${createSecretCount} new
|
|
||||||
one${createSecretCount > 1 ? "s" : ""}`
|
|
||||||
: ""}
|
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Matrix Import Modal */}
|
{/* Matrix Import Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
Reference in New Issue
Block a user