mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 14:28:20 +00:00
Added Feature to Create Secrets and Folders in all envs from overview page
This commit is contained in:
@@ -81,7 +81,7 @@ export const CreateSecretImportForm = ({
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
const axiosError = err as AxiosError
|
const axiosError = err as AxiosError;
|
||||||
if (axiosError?.response?.status === 401) {
|
if (axiosError?.response?.status === 401) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "You do not have access to the selected environment/path",
|
text: "You do not have access to the selected environment/path",
|
||||||
|
|||||||
@@ -2,22 +2,31 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import {
|
import {
|
||||||
|
faAngleDown,
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
faFolderBlank,
|
faFolderBlank,
|
||||||
faMagnifyingGlass
|
faFolderPlus,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlus
|
||||||
} 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 { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -30,7 +39,13 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
|
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useOrganization,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
useCreateSecretV3,
|
useCreateSecretV3,
|
||||||
@@ -42,6 +57,8 @@ import {
|
|||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
||||||
|
import { CreateSecretInAllEnvsForm } from "./components/CreateSecretsSection/CreateSecretInAllEnvsForm";
|
||||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||||
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
|
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
|
||||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||||
@@ -110,6 +127,40 @@ export const SecretOverviewPage = () => {
|
|||||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||||
const { mutateAsync: createFolder } = useCreateFolder();
|
const { mutateAsync: createFolder } = useCreateFolder();
|
||||||
|
|
||||||
|
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||||
|
"addSecretsInAllEnvs",
|
||||||
|
"addFolder",
|
||||||
|
"misc"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const handleFolderCreate = async (folderName: string) => {
|
||||||
|
const promises = userAvailableEnvs.map((env) => {
|
||||||
|
const environment = env.slug;
|
||||||
|
return createFolder({
|
||||||
|
name: folderName,
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
projectId: workspaceId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
const isFoldersAdded = results.some((result) => result.status === "fulfilled");
|
||||||
|
|
||||||
|
if (isFoldersAdded) {
|
||||||
|
handlePopUpClose("addFolder");
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created folder"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create folder"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSecretCreate = async (env: string, key: string, value: string) => {
|
const handleSecretCreate = async (env: string, key: string, value: string) => {
|
||||||
try {
|
try {
|
||||||
// create folder if not existing
|
// create folder if not existing
|
||||||
@@ -269,6 +320,7 @@ export const SecretOverviewPage = () => {
|
|||||||
filteredFolderNames?.length === 0;
|
filteredFolderNames?.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
|
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
|
||||||
<div className="relative right-5 ml-4">
|
<div className="relative right-5 ml-4">
|
||||||
@@ -324,6 +376,7 @@ export const SecretOverviewPage = () => {
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||||
|
<div className="flex flex-row items-center justify-center space-x-2">
|
||||||
<div className="w-80">
|
<div className="w-80">
|
||||||
<Input
|
<Input
|
||||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
@@ -333,6 +386,63 @@ export const SecretOverviewPage = () => {
|
|||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
|
||||||
|
className="h-10 rounded-r-none"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Add Secret
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<DropdownMenu
|
||||||
|
open={popUp.misc.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="add-folder-or-import"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="rounded-l-none bg-mineshaft-600 p-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAngleDown} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<div className="flex flex-col space-y-1 p-1.5">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("addFolder");
|
||||||
|
handlePopUpClose("misc");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
variant="outline_bg"
|
||||||
|
className="h-10"
|
||||||
|
isFullWidth
|
||||||
|
>
|
||||||
|
Add Folder
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
||||||
@@ -474,5 +584,22 @@ export const SecretOverviewPage = () => {
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<CreateSecretInAllEnvsForm
|
||||||
|
secretPath={secretPath}
|
||||||
|
isOpen={popUp.addSecretsInAllEnvs.isOpen}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
|
||||||
|
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
|
||||||
|
decryptFileKey={latestFileKey!}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.addFolder.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent title="Create Folder">
|
||||||
|
<FolderForm onCreateFolder={handleFolderCreate} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { Button, FormControl, Input, Modal, ModalContent, SecretInput } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateSecretV3 } from "@app/hooks/api";
|
||||||
|
import { UserWsKeyPair } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
const typeSchema = z
|
||||||
|
.object({
|
||||||
|
key: z.string(),
|
||||||
|
values: z.record(z.string().optional())
|
||||||
|
})
|
||||||
|
.refine((data) => data.key !== undefined, {
|
||||||
|
message: "Please enter secret name"
|
||||||
|
});
|
||||||
|
|
||||||
|
type TFormSchema = z.infer<typeof typeSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspaceId: string;
|
||||||
|
secretPath?: string;
|
||||||
|
decryptFileKey: UserWsKeyPair;
|
||||||
|
|
||||||
|
// modal props
|
||||||
|
isOpen?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onTogglePopUp: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateSecretInAllEnvsForm = ({
|
||||||
|
workspaceId,
|
||||||
|
secretPath = "/",
|
||||||
|
decryptFileKey,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onTogglePopUp
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
setError,
|
||||||
|
formState: { isSubmitting, errors }
|
||||||
|
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const environments = currentWorkspace?.environments || [];
|
||||||
|
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
|
|
||||||
|
const handleFormSubmit = async ({ key, values }: TFormSchema) => {
|
||||||
|
if (!key) {
|
||||||
|
setError("key", { message: "Please enter secret name" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = environments.map((env) => {
|
||||||
|
const environment = env.slug;
|
||||||
|
const value = values[environment] || "";
|
||||||
|
|
||||||
|
return createSecretV3({
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
secretPath,
|
||||||
|
secretName: key,
|
||||||
|
secretValue: value,
|
||||||
|
secretComment: "",
|
||||||
|
type: "shared",
|
||||||
|
latestFileKey: decryptFileKey
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
const isSecretsAdded = results.some((result) => result.status === "fulfilled");
|
||||||
|
|
||||||
|
if (isSecretsAdded) {
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Secrets created successfully"
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
reset();
|
||||||
|
} else {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create secrets"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-h-[80vh] overflow-y-auto"
|
||||||
|
title="Add Secrets"
|
||||||
|
subTitle="Add secrets in all environments at once."
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
|
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
||||||
|
<Input
|
||||||
|
{...register("key")}
|
||||||
|
placeholder="Type your secret name"
|
||||||
|
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{environments.map((env) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
key={`secret-input-${env.slug}`}
|
||||||
|
control={control}
|
||||||
|
name={`values.${env.slug}`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
label={`${env.name} Value`}
|
||||||
|
isError={Boolean(errors?.values?.[env.slug])}
|
||||||
|
errorText={errors?.values?.[env.slug]?.message}
|
||||||
|
>
|
||||||
|
<SecretInput
|
||||||
|
{...field}
|
||||||
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="mt-7 flex items-center">
|
||||||
|
<Button
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
key="layout-create-project-submit"
|
||||||
|
className="mr-4"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Create Secret
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
key="layout-cancel-create-project"
|
||||||
|
onClick={onClose}
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user