improvement: use multi-select for environment selection on create secret

This commit is contained in:
Scott Wilson
2024-12-02 09:45:43 -08:00
parent 39f7354fec
commit d1b9c316d8
2 changed files with 51 additions and 89 deletions
@@ -1128,7 +1128,6 @@ export const SecretOverviewPage = () => {
> >
<CreateSecretForm <CreateSecretForm
secretPath={secretPath} secretPath={secretPath}
getSecretByKey={getSecretByKey}
onClose={() => handlePopUpClose("addSecretsInAllEnvs")} onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
/> />
</ModalContent> </ModalContent>
@@ -1,13 +1,13 @@
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 { faTriangleExclamation, faWarning } from "@fortawesome/free-solid-svg-icons"; import { faTriangleExclamation } 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";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, Checkbox, FormControl, FormLabel, Input, Tooltip } from "@app/components/v2"; import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import { CreatableSelect } from "@app/components/v2/CreatableSelect"; import { CreatableSelect } from "@app/components/v2/CreatableSelect";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { import {
@@ -17,20 +17,14 @@ import {
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { getKeyValue } from "@app/helpers/parseEnvVar"; import { getKeyValue } from "@app/helpers/parseEnvVar";
import { import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
useCreateFolder, import { SecretType } from "@app/hooks/api/types";
useCreateSecretV3,
useCreateWsTag,
useGetWsTags,
useUpdateSecretV3
} from "@app/hooks/api";
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.object({ name: z.string(), slug: z.string() }).array(),
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).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, {
@@ -41,22 +35,19 @@ type TFormSchema = z.infer<typeof typeSchema>;
type Props = { type Props = {
secretPath?: string; secretPath?: string;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
// modal props // modal props
onClose: () => void; onClose: () => void;
}; };
export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }: Props) => { export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
const { const {
register, register,
handleSubmit, handleSubmit,
control, control,
reset, reset,
watch,
setValue, setValue,
formState: { isSubmitting, errors } formState: { isSubmitting, errors }
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) }); } = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
const newSecretKey = watch("key");
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
@@ -65,22 +56,14 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
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( const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
canReadTags ? workspaceId : "" canReadTags ? workspaceId : ""
); );
const handleFormSubmit = async ({ key, value, environments: selectedEnv, tags }: TFormSchema) => { const handleFormSubmit = async ({ key, value, environments: selectedEnv, tags }: TFormSchema) => {
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]); const promises = selectedEnv.map(async (env) => {
const isEnvironmentsSelected = environmentsSelected.length;
if (!isEnvironmentsSelected) {
createNotification({ type: "error", text: "Select at least one environment" });
return;
}
const promises = environmentsSelected.map(async (env) => {
const environment = env.slug; const environment = env.slug;
// create folder if not existing // create folder if not existing
if (secretPath !== "/") { if (secretPath !== "/") {
@@ -106,21 +89,22 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
} }
} }
const isEdit = getSecretByKey(environment, key) !== undefined; // TODO: add back - need to fetch secrets by key to check for conflicts as this method broke with pagination
if (isEdit) { // const isEdit = getSecretByKey(environment, key) !== undefined;
return { // if (isEdit) {
...(await updateSecretV3({ // return {
environment, // ...(await updateSecretV3({
workspaceId, // environment,
secretPath, // workspaceId,
secretKey: key, // secretPath,
secretValue: value || "", // secretKey: key,
type: SecretType.Shared, // secretValue: value || "",
tagIds: tags?.map((el) => el.value) // type: SecretType.Shared,
})), // tagIds: tags?.map((el) => el.value)
environment // })),
}; // environment
} // };
// }
return { return {
...(await createSecretV3({ ...(await createSecretV3({
@@ -278,54 +262,33 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
</FormControl> </FormControl>
)} )}
/> />
<FormLabel label="Environments" className="mb-2" /> <Controller
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2"> control={control}
{environments render={({ field: { value, onChange }, fieldState: { error } }) => (
.filter((environmentSlug) => <FormControl label="Environments" isError={Boolean(error)} errorText={error?.message}>
permission.can( <FilterableSelect
ProjectPermissionActions.Create, isMulti
subject(ProjectPermissionSub.Secrets, { options={environments.filter((environment) =>
environment: environmentSlug.slug, permission.can(
secretPath, ProjectPermissionActions.Create,
secretName: "*", subject(ProjectPermissionSub.Secrets, {
secretTags: ["*"] environment: environment.slug,
}) secretPath,
) secretName: "*",
) secretTags: ["*"]
.map((env) => { })
return ( )
<Controller )}
name={`environments.${env.slug}`} value={value}
key={`secret-input-${env.slug}`} onChange={onChange}
control={control} placeholder="Select environments to create secret in..."
render={({ field }) => ( getOptionLabel={(option) => option.name}
<Checkbox getOptionValue={(option) => option.slug}
isChecked={field.value} />
onCheckedChange={field.onChange} </FormControl>
id={`secret-input-${env.slug}`} )}
className="!justify-start" name="environments"
> />
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
<span title={env.name} className="truncate">
{env.name}
</span>
<span>
{getSecretByKey(env.slug, newSecretKey) && (
<Tooltip
className="max-w-[150px]"
content="Secret already exists, and it will be overwritten"
>
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
</Tooltip>
)}
</span>
</span>
</Checkbox>
)}
/>
);
})}
</div>
<div className="mt-7 flex items-center"> <div className="mt-7 flex items-center">
<Button <Button
isDisabled={isSubmitting} isDisabled={isSubmitting}