mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #817 from akhilmhdh/feat/import-sec-dashboard
feat: added copy secret feature in dashboard
This commit is contained in:
@@ -51,10 +51,10 @@ const buttonVariants = cva(
|
||||
false: ""
|
||||
},
|
||||
size: {
|
||||
xs: ["text-xs", "py-1", "px-1"],
|
||||
sm: ["text-sm", "py-2", "px-2"],
|
||||
md: ["text-md", "py-2", "px-4"],
|
||||
lg: ["text-lg", "py-2", "px-8"]
|
||||
xs: ["text-xs", "py-1", "px-2"],
|
||||
sm: ["text-sm", "py-2", "px-4"],
|
||||
md: ["text-md", "py-2", "px-5"],
|
||||
lg: ["text-lg", "py-2", "px-6"]
|
||||
}
|
||||
},
|
||||
compoundVariants: [
|
||||
@@ -186,16 +186,17 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
className="absolute rounded-xl opacity-80"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
loadingToggleClass,
|
||||
leftIcon && "ml-2",
|
||||
size === "xs" ? "mr-1" : "mr-2"
|
||||
)}
|
||||
>
|
||||
{leftIcon}
|
||||
</div>
|
||||
{leftIcon && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
loadingToggleClass,
|
||||
size === "xs" ? "mr-1" : "mr-2"
|
||||
)}
|
||||
>
|
||||
{leftIcon}
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={twMerge(
|
||||
"transition-all",
|
||||
@@ -205,15 +206,16 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
loadingToggleClass,
|
||||
size === "xs" ? "ml-1" : "ml-2"
|
||||
)}
|
||||
>
|
||||
{rightIcon}
|
||||
</div>
|
||||
{rightIcon && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
loadingToggleClass
|
||||
)}
|
||||
>
|
||||
{rightIcon}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,13 +54,14 @@ export const useGetProjectSecrets = ({
|
||||
env,
|
||||
decryptFileKey,
|
||||
isPaused,
|
||||
folderId
|
||||
folderId,
|
||||
secretPath
|
||||
}: GetProjectSecretsDTO) =>
|
||||
useQuery({
|
||||
// wait for all values to be available
|
||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId || secretPath),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
||||
select: useCallback(
|
||||
(data: EncryptedSecret[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { useDebounce } from "./useDebounce";
|
||||
export { useLeaveConfirm } from "./useLeaveConfirm";
|
||||
export { usePersistentState } from "./usePersistentState";
|
||||
export { usePopUp } from "./usePopUp";
|
||||
|
||||
26
frontend/src/hooks/useDebounce.tsx
Normal file
26
frontend/src/hooks/useDebounce.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// Ref: https://usehooks.com/useDebounce/
|
||||
export const useDebounce = <T extends unknown>(value: T, delay = 500): T => {
|
||||
// State and setters for debounced value
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
},
|
||||
[value, delay] // Only re-call effect if value or delay changes
|
||||
);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
@@ -740,7 +740,7 @@ export const DashboardPage = () => {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<form autoComplete="off" className="h-full">
|
||||
<form autoComplete="off" className="h-full flex flex-col">
|
||||
{/* breadcrumb row */}
|
||||
<div className="relative right-6 -top-2 mb-2 ml-6">
|
||||
<NavHeader
|
||||
@@ -924,8 +924,8 @@ export const DashboardPage = () => {
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
isEmptyPage ? "flex flex-col items-center justify-center" : ""
|
||||
} no-scrollbar::-webkit-scrollbar mt-3 h-3/4 overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
||||
isEmptyPage ? "flex flex-col flex-grow items-center justify-center" : ""
|
||||
} no-scrollbar::-webkit-scrollbar mt-3 flex flex-col overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
||||
ref={secretContainer}
|
||||
>
|
||||
{!isEmptyPage && (
|
||||
@@ -935,7 +935,7 @@ export const DashboardPage = () => {
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
>
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar flex-grow">
|
||||
<table className="secret-table relative">
|
||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||
<tbody className="max-h-96 overflow-y-auto">
|
||||
@@ -1017,9 +1017,12 @@ export const DashboardPage = () => {
|
||||
</FormProvider>
|
||||
|
||||
<SecretDropzone
|
||||
workspaceId={workspaceId}
|
||||
isSmaller={!isEmptyPage}
|
||||
onParsedEnv={handleUploadedEnv}
|
||||
onAddNewSecret={onAppendSecret}
|
||||
environments={userAvailableEnvs}
|
||||
decryptFileKey={latestFileKey!}
|
||||
/>
|
||||
</div>
|
||||
{/* secrets table and drawers, modals */}
|
||||
|
||||
@@ -75,6 +75,13 @@ export type FormData = yup.InferType<typeof schema>;
|
||||
export type TSecretDetailsOpen = { index: number; id: string };
|
||||
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
|
||||
|
||||
// to convert multi line into single line ones by quoting them and changing to string \n
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
};
|
||||
|
||||
export const downloadSecret = (
|
||||
secrets: FormData["secrets"] = [],
|
||||
importedSecrets: { key: string; value?: string; comment?: string }[] = [],
|
||||
@@ -86,9 +93,11 @@ export const downloadSecret = (
|
||||
});
|
||||
const finalSecret = [...importedSecrets];
|
||||
secrets.forEach(({ key, value, valueOverride, overrideAction, comment }) => {
|
||||
const finalVal =
|
||||
overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value;
|
||||
const newValue = {
|
||||
key,
|
||||
value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value,
|
||||
value: formatMultiValueEnv(finalVal),
|
||||
comment
|
||||
};
|
||||
// can also be zero thus failing
|
||||
|
||||
@@ -1,26 +1,128 @@
|
||||
import { ChangeEvent, DragEvent } from "react";
|
||||
import { ChangeEvent, DragEvent, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faClone,
|
||||
faKey,
|
||||
faSearch,
|
||||
faSquareXmark,
|
||||
faUpload
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionalityj
|
||||
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
||||
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
Select,
|
||||
SelectItem,
|
||||
Skeleton,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetProjectSecrets } from "@app/hooks/api";
|
||||
import { UserWsKeyPair } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = yup.object({
|
||||
environment: yup.string().required().label("Environment").trim(),
|
||||
secretPath: yup
|
||||
.string()
|
||||
.required()
|
||||
.label("Secret Path")
|
||||
.trim()
|
||||
.transform((val) =>
|
||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||
),
|
||||
secrets: yup.lazy((val) => {
|
||||
const valSchema: Record<string, yup.StringSchema> = {};
|
||||
Object.keys(val).forEach((key) => {
|
||||
valSchema[key] = yup.string().trim();
|
||||
});
|
||||
return yup.object(valSchema);
|
||||
})
|
||||
});
|
||||
|
||||
type TFormSchema = yup.InferType<typeof formSchema>;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isSmaller: boolean;
|
||||
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
|
||||
onAddNewSecret?: () => void;
|
||||
environments?: { name: string; slug: string }[];
|
||||
workspaceId: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props): JSX.Element => {
|
||||
export const SecretDropzone = ({
|
||||
isSmaller,
|
||||
onParsedEnv,
|
||||
onAddNewSecret,
|
||||
environments = [],
|
||||
workspaceId,
|
||||
decryptFileKey
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [isDragActive, setDragActive] = useToggle();
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["importSecEnv"] as const);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
register,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isDirty }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: yupResolver(formSchema),
|
||||
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
|
||||
});
|
||||
|
||||
const secretPath = watch("secretPath");
|
||||
const selectedEnvSlug = watch("environment");
|
||||
const debouncedSecretPath = useDebounce(secretPath);
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
env: selectedEnvSlug,
|
||||
secretPath: debouncedSecretPath,
|
||||
isPaused: !(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)),
|
||||
decryptFileKey
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setValue("secrets", {});
|
||||
setSearchFilter("");
|
||||
}, [debouncedSecretPath]);
|
||||
|
||||
const handleDrag = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -32,7 +134,7 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
}
|
||||
};
|
||||
|
||||
const parseFile = (file?: File) => {
|
||||
const parseFile = (file?: File, isJson?: boolean) => {
|
||||
const reader = new FileReader();
|
||||
if (!file) {
|
||||
createNotification({
|
||||
@@ -47,7 +149,9 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
reader.onload = (event) => {
|
||||
if (!event?.target?.result) return;
|
||||
// parse function's argument looks like to be ArrayBuffer
|
||||
const env = parseDotEnv(event.target.result as ArrayBuffer);
|
||||
const env = isJson
|
||||
? parseJson(event.target.result as ArrayBuffer)
|
||||
: parseDotEnv(event.target.result as ArrayBuffer);
|
||||
setIsLoading.off();
|
||||
onParsedEnv(env);
|
||||
};
|
||||
@@ -74,7 +178,31 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
|
||||
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
parseFile(e.target?.files?.[0]);
|
||||
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
|
||||
};
|
||||
|
||||
const handleFormSubmit = (data: TFormSchema) => {
|
||||
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(data.secrets || {}).forEach((key) => {
|
||||
if (data.secrets[key]) {
|
||||
secretsToBePulled[key] = {
|
||||
value: (shouldIncludeValues && data.secrets[key]) || "",
|
||||
comments: [""]
|
||||
};
|
||||
}
|
||||
});
|
||||
onParsedEnv(secretsToBePulled);
|
||||
handlePopUpClose("importSecEnv");
|
||||
reset();
|
||||
};
|
||||
|
||||
const handleSecSelectAll = () => {
|
||||
if (secrets?.secrets) {
|
||||
setValue(
|
||||
"secrets",
|
||||
secrets?.secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -84,9 +212,9 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
className={twMerge(
|
||||
"relative mx-0.5 mb-4 mt-4 flex w-full max-w-[calc(100vw-292px)] cursor-pointer items-center justify-center space-x-2 rounded-md bg-mineshaft-900 py-8 px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
||||
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 py-4 text-sm px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
||||
isDragActive && "opacity-100",
|
||||
!isSmaller && "max-w-3xl flex-col space-y-4 py-20",
|
||||
!isSmaller && "w-full max-w-3xl flex-col space-y-4 py-20",
|
||||
isLoading && "bg-bunker-800"
|
||||
)}
|
||||
>
|
||||
@@ -95,35 +223,184 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
|
||||
</div>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
accept=".txt,.env,.yml,.yaml"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
{!isSmaller && (
|
||||
<>
|
||||
<div className="flex w-full flex-row items-center justify-center py-4">
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
</div>
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex items-center justify-cente flex-col space-y-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
|
||||
</div>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
accept=".txt,.env,.yml,.yaml,.json"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full flex-row items-center justify-center py-4",
|
||||
isSmaller && "py-1"
|
||||
)}
|
||||
>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
<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">
|
||||
<Modal
|
||||
isOpen={popUp.importSecEnv.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("importSecEnv", isOpen);
|
||||
reset();
|
||||
setSearchFilter("");
|
||||
}}
|
||||
>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="star" size={isSmaller ? "xs" : "sm"}>
|
||||
Copy Secrets From An Environment
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Copy Secret From An Environment"
|
||||
subTitle="Copy/paste secrets from other environments into this context"
|
||||
>
|
||||
<form>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Environment" isRequired className="w-1/3">
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
position="popper"
|
||||
>
|
||||
{environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormControl label="Secret Path" className="flex-grow" isRequired>
|
||||
<Input
|
||||
{...register("secretPath")}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="border-t border-mineshaft-600 pt-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>Secrets</div>
|
||||
<div className="w-1/2 flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Search for secret"
|
||||
value={searchFilter}
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||
onChange={(evt) => setSearchFilter(evt.target.value)}
|
||||
/>
|
||||
<Tooltip content="Select All">
|
||||
<IconButton
|
||||
ariaLabel="Select all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={handleSecSelectAll}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Unselect All">
|
||||
<IconButton
|
||||
ariaLabel="UnSelect all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{!isSecretsLoading && !secrets?.secrets?.length && (
|
||||
<EmptyState title="No secrets found" icon={faKey} />
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4 max-h-64 overflow-auto thin-scrollbar ">
|
||||
{isSecretsLoading &&
|
||||
Array.apply(0, Array(2)).map((_x, i) => (
|
||||
<Skeleton
|
||||
key={`secret-pull-loading-${i + 1}`}
|
||||
className="bg-mineshaft-700"
|
||||
/>
|
||||
))}
|
||||
|
||||
{secrets?.secrets
|
||||
?.filter(({ key }) =>
|
||||
key.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
)
|
||||
?.map(({ _id, key, value: secVal }) => (
|
||||
<Controller
|
||||
key={`pull-secret--${_id}`}
|
||||
control={control}
|
||||
name={`secrets.${key}`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
id={`pull-secret-${_id}`}
|
||||
isChecked={Boolean(value)}
|
||||
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
|
||||
>
|
||||
{key}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 mb-4">
|
||||
<Checkbox
|
||||
id="populate-include-value"
|
||||
isChecked={shouldIncludeValues}
|
||||
onCheckedChange={(isChecked) =>
|
||||
setShouldIncludeValues(isChecked as boolean)
|
||||
}
|
||||
>
|
||||
Include secret values
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faClone} />}
|
||||
type="submit"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
Paste Secrets
|
||||
</Button>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isSmaller && (
|
||||
<Button variant="star" onClick={onAddNewSecret}>
|
||||
Add a new secret
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}{" "}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user