Merge pull request #2707 from akhilmhdh/feat/create-secret-tag

feat: added tag support on create secret
This commit is contained in:
Maidul Islam
2024-11-08 12:47:32 -05:00
committed by GitHub
6 changed files with 295 additions and 201 deletions
+4 -2
View File
@@ -31,7 +31,8 @@ export const useCreateSecretV3 = ({
secretKey, secretKey,
secretValue, secretValue,
secretComment, secretComment,
skipMultilineEncoding skipMultilineEncoding,
tagIds
}) => { }) => {
const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, { const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, {
secretPath, secretPath,
@@ -40,7 +41,8 @@ export const useCreateSecretV3 = ({
workspaceId, workspaceId,
secretValue, secretValue,
secretComment, secretComment,
skipMultilineEncoding skipMultilineEncoding,
tagIds
}); });
return data; return data;
}, },
+1
View File
@@ -132,6 +132,7 @@ export type TCreateSecretsV3DTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
type: SecretType; type: SecretType;
tagIds?: string[];
}; };
export type TUpdateSecretsV3DTO = { export type TUpdateSecretsV3DTO = {
@@ -9,7 +9,14 @@ import { twMerge } from "tailwind-merge";
import NavHeader from "@app/components/navigation/NavHeader"; import NavHeader from "@app/components/navigation/NavHeader";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { PermissionDeniedBanner } from "@app/components/permissions"; import { PermissionDeniedBanner } from "@app/components/permissions";
import { Checkbox, ContentLoader, Pagination, Tooltip } from "@app/components/v2"; import {
Checkbox,
ContentLoader,
Modal,
ModalContent,
Pagination,
Tooltip
} from "@app/components/v2";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -41,7 +48,10 @@ import { SecretDropzone } from "./components/SecretDropzone";
import { SecretListView, SecretNoAccessListView } from "./components/SecretListView"; import { SecretListView, SecretNoAccessListView } from "./components/SecretListView";
import { SnapshotView } from "./components/SnapshotView"; import { SnapshotView } from "./components/SnapshotView";
import { import {
PopUpNames,
StoreProvider, StoreProvider,
usePopUpAction,
usePopUpState,
useSelectedSecretActions, useSelectedSecretActions,
useSelectedSecrets useSelectedSecrets
} from "./SecretMainPage.store"; } from "./SecretMainPage.store";
@@ -123,6 +133,9 @@ const SecretMainPageContent = () => {
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter); const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map()); const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
const createSecretPopUp = usePopUpState(PopUpNames.CreateSecretForm);
const { togglePopUp } = usePopUpAction();
useEffect(() => { useEffect(() => {
if ( if (
!isWorkspaceLoading && !isWorkspaceLoading &&
@@ -520,13 +533,24 @@ const SecretMainPageContent = () => {
onChangePerPage={(newPerPage) => setPerPage(newPerPage)} onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/> />
)} )}
<CreateSecretForm <Modal
environment={environment} isOpen={createSecretPopUp.isOpen}
workspaceId={workspaceId} onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
secretPath={secretPath} >
autoCapitalize={currentWorkspace?.autoCapitalization} <ModalContent
isProtectedBranch={isProtectedBranch} title="Create Secret"
/> subTitle="Add a secret to this particular environment and folder"
bodyClassName="overflow-visible"
>
<CreateSecretForm
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
autoCapitalize={currentWorkspace?.autoCapitalization}
isProtectedBranch={isProtectedBranch}
/>
</ModalContent>
</Modal>
<SecretDropzone <SecretDropzone
secrets={secrets} secrets={secrets}
environment={environment} environment={environment}
@@ -1,20 +1,24 @@
import { ClipboardEvent } from "react"; import { ClipboardEvent } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { Button, FormControl, Input, MultiSelect } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { getKeyValue } from "@app/helpers/parseEnvVar"; import { getKeyValue } from "@app/helpers/parseEnvVar";
import { useCreateSecretV3 } from "@app/hooks/api"; import { useCreateSecretV3, useGetWsTags } from "@app/hooks/api";
import { SecretType } from "@app/hooks/api/types"; import { SecretType } from "@app/hooks/api/types";
import { PopUpNames, usePopUpAction, usePopUpState } from "../../SecretMainPage.store"; import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
const typeSchema = z.object({ const typeSchema = z.object({
key: z.string().trim().min(1, { message: "Secret key is required" }), key: z.string().trim().min(1, { message: "Secret key is required" }),
value: z.string().optional() value: z.string().optional(),
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
}); });
type TFormSchema = z.infer<typeof typeSchema>; type TFormSchema = z.infer<typeof typeSchema>;
@@ -43,12 +47,16 @@ export const CreateSecretForm = ({
setValue, setValue,
formState: { errors, isSubmitting } formState: { errors, isSubmitting }
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) }); } = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm); const { closePopUp } = usePopUpAction();
const { closePopUp, togglePopUp } = usePopUpAction();
const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { permission } = useProjectPermission();
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
canReadTags ? workspaceId : ""
);
const handleFormSubmit = async ({ key, value }: TFormSchema) => { const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => {
try { try {
await createSecretV3({ await createSecretV3({
environment, environment,
@@ -57,7 +65,8 @@ export const CreateSecretForm = ({
secretKey: key, secretKey: key,
secretValue: value || "", secretValue: value || "",
secretComment: "", secretComment: "",
type: SecretType.Shared type: SecretType.Shared,
tagIds: tags?.map((el) => el.value)
}); });
closePopUp(PopUpNames.CreateSecretForm); closePopUp(PopUpNames.CreateSecretForm);
reset(); reset();
@@ -88,67 +97,90 @@ export const CreateSecretForm = ({
}; };
return ( return (
<Modal <form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
isOpen={isOpen} <FormControl
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)} label="Key"
> isRequired
<ModalContent isError={Boolean(errors?.key)}
title="Create secret" errorText={errors?.key?.message}
subTitle="Add a secret to the particular environment and folder"
> >
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate> <Input
{...register("key")}
placeholder="Type your secret name"
onPaste={handlePaste}
autoCapitalization={autoCapitalize}
/>
</FormControl>
<Controller
control={control}
name="value"
render={({ field }) => (
<FormControl <FormControl
label="Key" label="Value"
isRequired isError={Boolean(errors?.value)}
isError={Boolean(errors?.key)} errorText={errors?.value?.message}
errorText={errors?.key?.message}
> >
<Input <InfisicalSecretInput
{...register("key")} {...field}
placeholder="Type your secret name" environment={environment}
onPaste={handlePaste} secretPath={secretPath}
autoCapitalization={autoCapitalize} containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/> />
</FormControl> </FormControl>
<Controller )}
control={control} />
name="value" <Controller
render={({ field }) => ( control={control}
<FormControl name="tags"
label="Value" render={({ field }) => (
isError={Boolean(errors?.value)} <FormControl
errorText={errors?.value?.message} label="Tags"
> isError={Boolean(errors?.value)}
<InfisicalSecretInput errorText={errors?.value?.message}
{...field} helperText={
environment={environment} !canReadTags ? (
secretPath={secretPath} <div className="flex items-center space-x-2">
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5" <FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" />
/> <span>You do not have permission to read tags.</span>
</FormControl> </div>
)} ) : (
/> ""
<div className="mt-7 flex items-center"> )
<Button }
isDisabled={isSubmitting} >
isLoading={isSubmitting} <MultiSelect
key="layout-create-project-submit" className="w-full"
className="mr-4" placeholder="Select tags to assign to secret..."
type="submit" isMulti
> name="tagIds"
Create Secret isDisabled={!canReadTags}
</Button> isLoading={isTagsLoading}
<Button options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
key="layout-cancel-create-project" value={field.value}
onClick={() => closePopUp(PopUpNames.CreateSecretForm)} onChange={field.onChange}
variant="plain" />
colorSchema="secondary" </FormControl>
> )}
Cancel />
</Button> <div className="mt-7 flex items-center">
</div> <Button
</form> isDisabled={isSubmitting}
</ModalContent> isLoading={isSubmitting}
</Modal> key="layout-create-project-submit"
className="mr-4"
type="submit"
>
Create Secret
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => closePopUp(PopUpNames.CreateSecretForm)}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
); );
}; };
@@ -1116,13 +1116,23 @@ export const SecretOverviewPage = () => {
)} )}
</div> </div>
</div> </div>
<CreateSecretForm <Modal
secretPath={secretPath}
isOpen={popUp.addSecretsInAllEnvs.isOpen} isOpen={popUp.addSecretsInAllEnvs.isOpen}
getSecretByKey={getSecretByKey} onOpenChange={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)} >
onClose={() => handlePopUpClose("addSecretsInAllEnvs")} <ModalContent
/> className="max-h-[80vh]"
bodyClassName="overflow-visible"
title="Create Secrets"
subTitle="Create a secret across multiple environments"
>
<CreateSecretForm
secretPath={secretPath}
getSecretByKey={getSecretByKey}
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
/>
</ModalContent>
</Modal>
<Modal <Modal
isOpen={popUp.addFolder.isOpen} isOpen={popUp.addFolder.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
@@ -1,7 +1,7 @@
import { ClipboardEvent } from "react"; import { ClipboardEvent } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faWarning } from "@fortawesome/free-solid-svg-icons"; import { faTriangleExclamation, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@@ -13,8 +13,7 @@ import {
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
Modal, MultiSelect,
ModalContent,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
@@ -25,14 +24,20 @@ import {
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { getKeyValue } from "@app/helpers/parseEnvVar"; import { getKeyValue } from "@app/helpers/parseEnvVar";
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api"; import {
useCreateFolder,
useCreateSecretV3,
useGetWsTags,
useUpdateSecretV3
} from "@app/hooks/api";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
const typeSchema = z const typeSchema = z
.object({ .object({
key: z.string().trim().min(1, "Key is required"), key: z.string().trim().min(1, "Key is required"),
value: z.string().optional(), value: z.string().optional(),
environments: z.record(z.boolean().optional()) environments: z.record(z.boolean().optional()),
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
}) })
.refine((data) => data.key !== undefined, { .refine((data) => data.key !== undefined, {
message: "Please enter secret name" message: "Please enter secret name"
@@ -44,18 +49,10 @@ type Props = {
secretPath?: string; secretPath?: string;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined; getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
// modal props // modal props
isOpen?: boolean;
onClose: () => void; onClose: () => void;
onTogglePopUp: (isOpen: boolean) => void;
}; };
export const CreateSecretForm = ({ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }: Props) => {
secretPath = "/",
isOpen,
getSecretByKey,
onClose,
onTogglePopUp
}: Props) => {
const { const {
register, register,
handleSubmit, handleSubmit,
@@ -69,14 +66,18 @@ export const CreateSecretForm = ({
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const environments = currentWorkspace?.environments || []; const environments = currentWorkspace?.environments || [];
const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: createFolder } = useCreateFolder();
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
canReadTags ? workspaceId : ""
);
const handleFormSubmit = async ({ key, value, environments: selectedEnv }: TFormSchema) => { const handleFormSubmit = async ({ key, value, environments: selectedEnv, tags }: TFormSchema) => {
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]); const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
const isEnvironmentsSelected = environmentsSelected.length; const isEnvironmentsSelected = environmentsSelected.length;
@@ -120,7 +121,8 @@ export const CreateSecretForm = ({
secretPath, secretPath,
secretKey: key, secretKey: key,
secretValue: value || "", secretValue: value || "",
type: SecretType.Shared type: SecretType.Shared,
tagIds: tags?.map((el) => el.value)
})), })),
environment environment
}; };
@@ -134,7 +136,8 @@ export const CreateSecretForm = ({
secretKey: key, secretKey: key,
secretValue: value || "", secretValue: value || "",
secretComment: "", secretComment: "",
type: SecretType.Shared type: SecretType.Shared,
tagIds: tags?.map((el) => el.value)
})), })),
environment environment
}; };
@@ -197,114 +200,136 @@ export const CreateSecretForm = ({
}; };
return ( return (
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}> <form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
<ModalContent <FormControl
className="max-h-[80vh] overflow-y-auto" label="Key"
title="Bulk Create & Update" isRequired
subTitle="Create & update a secret across many environments" isError={Boolean(errors?.key)}
errorText={errors?.key?.message}
> >
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate> <Input
{...register("key")}
placeholder="Type your secret name"
onPaste={handlePaste}
autoCapitalization={currentWorkspace?.autoCapitalization}
/>
</FormControl>
<Controller
control={control}
name="value"
render={({ field }) => (
<FormControl <FormControl
label="Key" label="Value"
isRequired isError={Boolean(errors?.value)}
isError={Boolean(errors?.key)} errorText={errors?.value?.message}
errorText={errors?.key?.message}
> >
<Input <InfisicalSecretInput
{...register("key")} {...field}
placeholder="Type your secret name" containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
onPaste={handlePaste}
autoCapitalization={currentWorkspace?.autoCapitalization}
/> />
</FormControl> </FormControl>
<Controller )}
control={control} />
name="value" <Controller
render={({ field }) => ( control={control}
<FormControl name="tags"
label="Value" render={({ field }) => (
isError={Boolean(errors?.value)} <FormControl
errorText={errors?.value?.message} label="Tags"
> isError={Boolean(errors?.value)}
<InfisicalSecretInput errorText={errors?.value?.message}
{...field} helperText={
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5" !canReadTags ? (
/> <div className="flex items-center space-x-2">
</FormControl> <FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" />
)} <span>You do not have permission to read tags.</span>
/> </div>
<FormLabel label="Environments" className="mb-2" /> ) : (
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2"> ""
{environments
.filter((environmentSlug) =>
permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: environmentSlug.slug,
secretPath,
secretName: "*",
secretTags: ["*"]
})
)
) )
.map((env) => { }
return ( >
<Controller <MultiSelect
name={`environments.${env.slug}`} className="w-full"
key={`secret-input-${env.slug}`} placeholder="Select tags to assign to secrets..."
control={control} isMulti
render={({ field }) => ( name="tagIds"
<Checkbox isDisabled={!canReadTags}
isChecked={field.value} isLoading={isTagsLoading}
onCheckedChange={field.onChange} options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
id={`secret-input-${env.slug}`} value={field.value}
className="!justify-start" onChange={field.onChange}
> />
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap"> </FormControl>
<span title={env.name} className="truncate"> )}
{env.name} />
</span> <FormLabel label="Environments" className="mb-2" />
<span> <div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
{getSecretByKey(env.slug, newSecretKey) && ( {environments
<Tooltip .filter((environmentSlug) =>
className="max-w-[150px]" permission.can(
content="Secret already exists, and it will be overwritten" ProjectPermissionActions.Create,
> subject(ProjectPermissionSub.Secrets, {
<FontAwesomeIcon environment: environmentSlug.slug,
icon={faWarning} secretPath,
className="ml-1 text-yellow-400" secretName: "*",
/> secretTags: ["*"]
</Tooltip> })
)} )
</span> )
</span> .map((env) => {
</Checkbox> return (
)} <Controller
/> name={`environments.${env.slug}`}
); key={`secret-input-${env.slug}`}
})} control={control}
</div> render={({ field }) => (
<div className="mt-7 flex items-center"> <Checkbox
<Button isChecked={field.value}
isDisabled={isSubmitting} onCheckedChange={field.onChange}
isLoading={isSubmitting} id={`secret-input-${env.slug}`}
key="layout-create-project-submit" className="!justify-start"
className="mr-4" >
type="submit" <span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
> <span title={env.name} className="truncate">
Create Secret {env.name}
</Button> </span>
<Button <span>
key="layout-cancel-create-project" {getSecretByKey(env.slug, newSecretKey) && (
onClick={onClose} <Tooltip
variant="plain" className="max-w-[150px]"
colorSchema="secondary" content="Secret already exists, and it will be overwritten"
> >
Cancel <FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
</Button> </Tooltip>
</div> )}
</form> </span>
</ModalContent> </span>
</Modal> </Checkbox>
)}
/>
);
})}
</div>
<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>
); );
}; };