mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
Merge pull request #2726 from Infisical/scott/paste-secrets
Feat: Paste Secrets for Upload
This commit is contained in:
@@ -11,7 +11,7 @@ import { SecretType } from "@app/hooks/api/types";
|
||||
import Button from "../basic/buttons/Button";
|
||||
import Error from "../basic/Error";
|
||||
import { createNotification } from "../notifications";
|
||||
import { parseDotEnv } from "../utilities/parseDotEnv";
|
||||
import { parseDotEnv } from "../utilities/parseSecrets";
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
|
||||
interface DropZoneProps {
|
||||
|
||||
@@ -6,7 +6,7 @@ const LINE =
|
||||
* @param {ArrayBuffer} src - source buffer
|
||||
* @returns {String} text - text of buffer
|
||||
*/
|
||||
export function parseDotEnv(src: ArrayBuffer) {
|
||||
export function parseDotEnv(src: ArrayBuffer | string) {
|
||||
const object: {
|
||||
[key: string]: { value: string; comments: string[] };
|
||||
} = {};
|
||||
@@ -65,3 +65,15 @@ export function parseDotEnv(src: ArrayBuffer) {
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
export const parseJson = (src: ArrayBuffer | string) => {
|
||||
const file = src.toString();
|
||||
const formatedData: Record<string, string> = JSON.parse(file);
|
||||
const env: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(formatedData).forEach((key) => {
|
||||
if (typeof formatedData[key] === "string") {
|
||||
env[key] = { value: formatedData[key], comments: [] };
|
||||
}
|
||||
});
|
||||
return env;
|
||||
};
|
||||
@@ -83,6 +83,7 @@ export type FormControlProps = {
|
||||
className?: string;
|
||||
icon?: ReactNode;
|
||||
tooltipText?: ReactElement | string;
|
||||
tooltipClassName?: string;
|
||||
};
|
||||
|
||||
export const FormControl = ({
|
||||
@@ -96,7 +97,8 @@ export const FormControl = ({
|
||||
isError,
|
||||
icon,
|
||||
className,
|
||||
tooltipText
|
||||
tooltipText,
|
||||
tooltipClassName
|
||||
}: FormControlProps): JSX.Element => {
|
||||
return (
|
||||
<div className={twMerge("mb-4", className)}>
|
||||
@@ -108,6 +110,7 @@ export const FormControl = ({
|
||||
id={id}
|
||||
icon={icon}
|
||||
tooltipText={tooltipText}
|
||||
tooltipClassName={tooltipClassName}
|
||||
/>
|
||||
) : (
|
||||
label
|
||||
|
||||
@@ -5,6 +5,7 @@ import axios from "axios";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import {
|
||||
DashboardProjectSecretsByKeys,
|
||||
DashboardProjectSecretsDetails,
|
||||
DashboardProjectSecretsDetailsResponse,
|
||||
DashboardProjectSecretsOverview,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
DashboardSecretsOrderBy,
|
||||
TDashboardProjectSecretsQuickSearch,
|
||||
TDashboardProjectSecretsQuickSearchResponse,
|
||||
TGetDashboardProjectSecretsByKeys,
|
||||
TGetDashboardProjectSecretsDetailsDTO,
|
||||
TGetDashboardProjectSecretsOverviewDTO,
|
||||
TGetDashboardProjectSecretsQuickSearchDTO
|
||||
@@ -101,6 +103,23 @@ export const fetchProjectSecretsDetails = async ({
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchDashboardProjectSecretsByKeys = async ({
|
||||
keys,
|
||||
...params
|
||||
}: TGetDashboardProjectSecretsByKeys) => {
|
||||
const { data } = await apiRequest.get<DashboardProjectSecretsByKeys>(
|
||||
"/api/v1/dashboard/secrets-by-keys",
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
keys: encodeURIComponent(keys.join(","))
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetProjectSecretsOverview = (
|
||||
{
|
||||
projectId,
|
||||
|
||||
@@ -29,6 +29,10 @@ export type DashboardProjectSecretsDetailsResponse = {
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
export type DashboardProjectSecretsByKeys = {
|
||||
secrets: SecretV3Raw[];
|
||||
};
|
||||
|
||||
export type DashboardProjectSecretsOverview = Omit<
|
||||
DashboardProjectSecretsOverviewResponse,
|
||||
"secrets"
|
||||
@@ -89,3 +93,10 @@ export type TGetDashboardProjectSecretsQuickSearchDTO = {
|
||||
search: string;
|
||||
environments: string[];
|
||||
};
|
||||
|
||||
export type TGetDashboardProjectSecretsByKeys = {
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
keys: string[];
|
||||
};
|
||||
|
||||
@@ -552,7 +552,6 @@ const SecretMainPageContent = () => {
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<SecretDropzone
|
||||
secrets={secrets}
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faClone,
|
||||
faFileImport,
|
||||
faKey,
|
||||
faSearch,
|
||||
faSquareCheck,
|
||||
@@ -151,6 +152,7 @@ export const CopySecretsFromBoard = ({
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||
onClick={() => onToggle(true)}
|
||||
isDisabled={!isAllowed}
|
||||
variant="star"
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faInfoCircle, faPaste } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
isSmaller?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
value: z.string().trim()
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
const PasteEnvForm = ({ onParsedEnv }: Pick<Props, "onParsedEnv">) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { isDirty, errors },
|
||||
setError,
|
||||
setFocus
|
||||
} = useForm<TForm>({ defaultValues: { value: "" }, resolver: zodResolver(formSchema) });
|
||||
|
||||
const onSubmit = ({ value }: TForm) => {
|
||||
let env: Record<string, { value: string; comments: string[] }>;
|
||||
try {
|
||||
env = parseJson(value);
|
||||
} catch (e) {
|
||||
// not json, parse as env
|
||||
env = parseDotEnv(value);
|
||||
}
|
||||
|
||||
if (!Object.keys(env).length) {
|
||||
setError("value", {
|
||||
message: "No secrets found. Please make sure the provided format is valid."
|
||||
});
|
||||
setFocus("value");
|
||||
return;
|
||||
}
|
||||
|
||||
onParsedEnv(env);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormControl
|
||||
label="Secret Values"
|
||||
isError={Boolean(errors.value)}
|
||||
errorText={errors.value?.message}
|
||||
icon={<FontAwesomeIcon size="sm" className="text-mineshaft-400" icon={faInfoCircle} />}
|
||||
tooltipClassName="max-w-lg px-2 whitespace-pre-line"
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>Example Formats:</p>
|
||||
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
|
||||
{/* eslint-disable-next-line react/jsx-no-comment-textnodes */}
|
||||
<p className="text-mineshaft-400">// .json</p>
|
||||
{JSON.stringify(
|
||||
{
|
||||
APP_NAME: "example-service",
|
||||
APP_VERSION: "1.2.3",
|
||||
NODE_ENV: "production"
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
|
||||
<p className="text-mineshaft-400"># .env</p>
|
||||
<p>APP_NAME="example-service"</p>
|
||||
<p>APP_VERSION="1.2.3"</p>
|
||||
<p>NODE_ENV="production"</p>
|
||||
</pre>
|
||||
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
|
||||
<p className="text-mineshaft-400"># .yml</p>
|
||||
<p>APP_NAME: example-service</p>
|
||||
<p>APP_VERSION: 1.2.3</p>
|
||||
<p>NODE_ENV: production</p>
|
||||
</pre>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TextArea
|
||||
{...register("value")}
|
||||
placeholder="Paste secrets in .json, .yml or .env format..."
|
||||
className="h-[60vh] !resize-none"
|
||||
/>
|
||||
</FormControl>
|
||||
<Button isDisabled={!isDirty} type="submit">
|
||||
Import Secrets
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const PasteSecretEnvModal = ({
|
||||
isSmaller,
|
||||
isOpen,
|
||||
onParsedEnv,
|
||||
onToggle,
|
||||
environment,
|
||||
secretPath
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalTrigger asChild>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPaste} />}
|
||||
onClick={() => onToggle(true)}
|
||||
isDisabled={!isAllowed}
|
||||
variant="star"
|
||||
size={isSmaller ? "xs" : "sm"}
|
||||
>
|
||||
Paste Secrets
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Past Secret Values"
|
||||
subTitle="Paste values in .env, .json or .yml format"
|
||||
>
|
||||
<PasteEnvForm
|
||||
onParsedEnv={(value) => {
|
||||
onToggle(false);
|
||||
onParsedEnv(value);
|
||||
}}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeEvent, DragEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -9,30 +9,22 @@ import { twMerge } from "tailwind-merge";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
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 { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets";
|
||||
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 { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import {
|
||||
dashboardKeys,
|
||||
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, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
|
||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
||||
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
|
||||
|
||||
const parseJson = (src: ArrayBuffer) => {
|
||||
const file = src.toString();
|
||||
const formatedData: Record<string, string> = JSON.parse(file);
|
||||
const env: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(formatedData).forEach((key) => {
|
||||
if (typeof formatedData[key] === "string") {
|
||||
env[key] = { value: formatedData[key], comments: [] };
|
||||
}
|
||||
});
|
||||
return env;
|
||||
};
|
||||
import { PasteSecretEnvModal } from "./PasteSecretEnvModal";
|
||||
|
||||
type TParsedEnv = Record<string, { value: string; comments: string[] }>;
|
||||
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv };
|
||||
@@ -43,7 +35,6 @@ type Props = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets?: SecretV3RawSanitized[];
|
||||
isProtectedBranch?: boolean;
|
||||
};
|
||||
|
||||
@@ -53,7 +44,6 @@ export const SecretDropzone = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secrets = [],
|
||||
isProtectedBranch = false
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
@@ -62,7 +52,8 @@ export const SecretDropzone = ({
|
||||
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"importSecEnv",
|
||||
"overlapKeyWarning"
|
||||
"confirmUpload",
|
||||
"pasteSecEnv"
|
||||
] as const);
|
||||
const queryClient = useQueryClient();
|
||||
const { openPopUp } = usePopUpAction();
|
||||
@@ -86,20 +77,10 @@ export const SecretDropzone = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleParsedEnv = (env: TParsedEnv) => {
|
||||
const secretsGroupedByKey = secrets?.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.key]: true }),
|
||||
{}
|
||||
);
|
||||
const overlappedSecrets = Object.keys(env)
|
||||
.filter((secKey) => secretsGroupedByKey?.[secKey])
|
||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||
const handleParsedEnv = async (env: TParsedEnv) => {
|
||||
const envSecretKeys = Object.keys(env);
|
||||
|
||||
const nonOverlappedSecrets = Object.keys(env)
|
||||
.filter((secKey) => !secretsGroupedByKey?.[secKey])
|
||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||
|
||||
if (!Object.keys(overlappedSecrets).length && !Object.keys(nonOverlappedSecrets).length) {
|
||||
if (!envSecretKeys.length) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to find secrets"
|
||||
@@ -107,10 +88,42 @@ export const SecretDropzone = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("overlapKeyWarning", {
|
||||
update: overlappedSecrets,
|
||||
create: nonOverlappedSecrets
|
||||
});
|
||||
try {
|
||||
setIsLoading.on();
|
||||
const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({
|
||||
secretPath,
|
||||
environment,
|
||||
projectId: workspaceId,
|
||||
keys: envSecretKeys
|
||||
});
|
||||
|
||||
const secretsGroupedByKey = existingSecrets.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.secretKey]: true }),
|
||||
{}
|
||||
);
|
||||
|
||||
const updateSecrets = Object.keys(env)
|
||||
.filter((secKey) => secretsGroupedByKey[secKey])
|
||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||
|
||||
const createSecrets = Object.keys(env)
|
||||
.filter((secKey) => !secretsGroupedByKey[secKey])
|
||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||
|
||||
handlePopUpOpen("confirmUpload", {
|
||||
update: updateSecrets,
|
||||
create: createSecrets
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
createNotification({
|
||||
text: "Failed to check for secret conflicts",
|
||||
type: "error"
|
||||
});
|
||||
handlePopUpClose("confirmUpload");
|
||||
} finally {
|
||||
setIsLoading.off();
|
||||
}
|
||||
};
|
||||
|
||||
const parseFile = (file?: File, isJson?: boolean) => {
|
||||
@@ -160,7 +173,7 @@ export const SecretDropzone = ({
|
||||
};
|
||||
|
||||
const handleSaveSecrets = async () => {
|
||||
const { update, create } = popUp?.overlapKeyWarning?.data as TSecOverwriteOpt;
|
||||
const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt;
|
||||
try {
|
||||
if (Object.keys(create || {}).length) {
|
||||
await createSecretBatch({
|
||||
@@ -195,7 +208,7 @@ export const SecretDropzone = ({
|
||||
dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
|
||||
);
|
||||
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
|
||||
handlePopUpClose("overlapKeyWarning");
|
||||
handlePopUpClose("confirmUpload");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: isProtectedBranch
|
||||
@@ -211,10 +224,16 @@ export const SecretDropzone = ({
|
||||
}
|
||||
};
|
||||
|
||||
const isUploadedDuplicateSecretsEmpty = !Object.keys(
|
||||
(popUp.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {}
|
||||
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 (
|
||||
<div>
|
||||
<div
|
||||
@@ -278,7 +297,15 @@ export const SecretDropzone = ({
|
||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-8">
|
||||
<div className="flex flex-col items-center justify-center gap-4 lg:flex-row">
|
||||
<PasteSecretEnvModal
|
||||
isOpen={popUp.pasteSecEnv.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("pasteSecEnv", isOpen)}
|
||||
onParsedEnv={handleParsedEnv}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
isSmaller={isSmaller}
|
||||
/>
|
||||
<CopySecretsFromBoard
|
||||
isOpen={popUp.importSecEnv.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
|
||||
@@ -301,11 +328,12 @@ export const SecretDropzone = ({
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
|
||||
variant="star"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add a new secret
|
||||
Add a New Secret
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -315,25 +343,25 @@ export const SecretDropzone = ({
|
||||
)}
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={popUp?.overlapKeyWarning?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle("overlapKeyWarning", open)}
|
||||
isOpen={popUp?.confirmUpload?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle("confirmUpload", open)}
|
||||
>
|
||||
<ModalContent
|
||||
title={isUploadedDuplicateSecretsEmpty ? "Confirmation" : "Duplicate Secrets!!"}
|
||||
title="Confirm Secret Upload"
|
||||
footerContent={[
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
colorSchema={isUploadedDuplicateSecretsEmpty ? "primary" : "danger"}
|
||||
colorSchema={isNonConflictingUpload ? "primary" : "danger"}
|
||||
key="overwrite-btn"
|
||||
onClick={handleSaveSecrets}
|
||||
>
|
||||
{isUploadedDuplicateSecretsEmpty ? "Upload" : "Overwrite"}
|
||||
{isNonConflictingUpload ? "Upload" : "Overwrite"}
|
||||
</Button>,
|
||||
<Button
|
||||
key="keep-old-btn"
|
||||
className="mr-4"
|
||||
onClick={() => handlePopUpClose("overlapKeyWarning")}
|
||||
className="ml-4"
|
||||
onClick={() => handlePopUpClose("confirmUpload")}
|
||||
variant="outline_bg"
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
@@ -341,17 +369,27 @@ export const SecretDropzone = ({
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{isUploadedDuplicateSecretsEmpty ? (
|
||||
<div>Upload secrets from this file</div>
|
||||
{isNonConflictingUpload ? (
|
||||
<div>
|
||||
Are you sure you want to import {createSecretCount} secret
|
||||
{createSecretCount > 1 ? "s" : ""} to this environment?
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-2 text-gray-300">
|
||||
<div>Your file contains following duplicate secrets</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{Object.keys((popUp?.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {})
|
||||
<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>Are you sure you want to overwrite these secrets and create other ones?</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>
|
||||
|
||||
Reference in New Issue
Block a user