mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: Add new tag when creating secret (#2791)
* feat: Add new tag when creating secret
This commit is contained in:
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
@@ -89,7 +89,7 @@
|
|||||||
"react-mailchimp-subscribe": "^2.1.3",
|
"react-mailchimp-subscribe": "^2.1.3",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-select": "^5.8.1",
|
"react-select": "^5.8.3",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"sanitize-html": "^2.12.1",
|
"sanitize-html": "^2.12.1",
|
||||||
@@ -21259,9 +21259,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-select": {
|
"node_modules/react-select": {
|
||||||
"version": "5.8.1",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz",
|
||||||
"integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==",
|
"integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.0",
|
"@babel/runtime": "^7.12.0",
|
||||||
|
|||||||
@@ -162,4 +162,4 @@
|
|||||||
"tailwindcss": "3.2",
|
"tailwindcss": "3.2",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { GroupBase } from "react-select";
|
||||||
|
import ReactSelectCreatable, { CreatableProps } from "react-select/creatable";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
|
||||||
|
|
||||||
|
export const CreatableSelect = <T,>({
|
||||||
|
isMulti,
|
||||||
|
closeMenuOnSelect,
|
||||||
|
...props
|
||||||
|
}: CreatableProps<T, boolean, GroupBase<T>>) => {
|
||||||
|
return (
|
||||||
|
<ReactSelectCreatable
|
||||||
|
isMulti={isMulti}
|
||||||
|
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
|
||||||
|
hideSelectedOptions={false}
|
||||||
|
unstyled
|
||||||
|
styles={{
|
||||||
|
input: (base) => ({
|
||||||
|
...base,
|
||||||
|
"input:focus": {
|
||||||
|
boxShadow: "none"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
multiValueLabel: (base) => ({
|
||||||
|
...base,
|
||||||
|
whiteSpace: "normal",
|
||||||
|
overflow: "visible"
|
||||||
|
}),
|
||||||
|
control: (base) => ({
|
||||||
|
...base,
|
||||||
|
transition: "none"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
|
||||||
|
classNames={{
|
||||||
|
container: () => "w-full font-inter",
|
||||||
|
control: ({ isFocused }) =>
|
||||||
|
twMerge(
|
||||||
|
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
|
||||||
|
"border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
|
||||||
|
),
|
||||||
|
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
|
||||||
|
input: () => "pl-1 py-0.5",
|
||||||
|
valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`,
|
||||||
|
singleValue: () => "leading-7 ml-1",
|
||||||
|
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
|
||||||
|
multiValueLabel: () => "leading-6 text-sm",
|
||||||
|
multiValueRemove: () => "hover:text-red text-bunker-400",
|
||||||
|
indicatorsContainer: () => "p-1 gap-1",
|
||||||
|
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
|
||||||
|
indicatorSeparator: () => "bg-bunker-400",
|
||||||
|
dropdownIndicator: () => "text-bunker-200 p-1",
|
||||||
|
menu: () =>
|
||||||
|
"mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
||||||
|
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
||||||
|
option: ({ isFocused, isSelected }) =>
|
||||||
|
twMerge(
|
||||||
|
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
||||||
|
isSelected && "text-mineshaft-200",
|
||||||
|
"hover:cursor-pointer text-xs px-3 py-2"
|
||||||
|
),
|
||||||
|
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
frontend/src/components/v2/CreatableSelect/index.tsx
Normal file
1
frontend/src/components/v2/CreatableSelect/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./CreatableSelect";
|
||||||
@@ -1,50 +1,7 @@
|
|||||||
import Select, {
|
import Select, { Props } from "react-select";
|
||||||
ClearIndicatorProps,
|
|
||||||
components,
|
|
||||||
DropdownIndicatorProps,
|
|
||||||
MultiValueRemoveProps,
|
|
||||||
OptionProps,
|
|
||||||
Props
|
|
||||||
} from "react-select";
|
|
||||||
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
|
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
|
||||||
return (
|
|
||||||
<components.DropdownIndicator {...props}>
|
|
||||||
<FontAwesomeIcon icon={faChevronDown} size="xs" />
|
|
||||||
</components.DropdownIndicator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
|
|
||||||
return (
|
|
||||||
<components.ClearIndicator {...props}>
|
|
||||||
<FontAwesomeIcon icon={faCircleXmark} />
|
|
||||||
</components.ClearIndicator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MultiValueRemove = (props: MultiValueRemoveProps) => {
|
|
||||||
return (
|
|
||||||
<components.MultiValueRemove {...props}>
|
|
||||||
<FontAwesomeIcon icon={faXmark} size="xs" />
|
|
||||||
</components.MultiValueRemove>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
|
|
||||||
return (
|
|
||||||
<components.Option isSelected={isSelected} {...props}>
|
|
||||||
{children}
|
|
||||||
{isSelected && (
|
|
||||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
|
||||||
)}
|
|
||||||
</components.Option>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: Props<T>) => (
|
export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: Props<T>) => (
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
45
frontend/src/components/v2/Select/components/index.tsx
Normal file
45
frontend/src/components/v2/Select/components/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
ClearIndicatorProps,
|
||||||
|
components,
|
||||||
|
DropdownIndicatorProps,
|
||||||
|
MultiValueRemoveProps,
|
||||||
|
OptionProps
|
||||||
|
} from "react-select";
|
||||||
|
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
export const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
|
||||||
|
return (
|
||||||
|
<components.DropdownIndicator {...props}>
|
||||||
|
<FontAwesomeIcon icon={faChevronDown} size="xs" />
|
||||||
|
</components.DropdownIndicator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
|
||||||
|
return (
|
||||||
|
<components.ClearIndicator {...props}>
|
||||||
|
<FontAwesomeIcon icon={faCircleXmark} />
|
||||||
|
</components.ClearIndicator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultiValueRemove = (props: MultiValueRemoveProps) => {
|
||||||
|
return (
|
||||||
|
<components.MultiValueRemove {...props}>
|
||||||
|
<FontAwesomeIcon icon={faXmark} size="xs" />
|
||||||
|
</components.MultiValueRemove>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
|
||||||
|
return (
|
||||||
|
<components.Option isSelected={isSelected} {...props}>
|
||||||
|
{children}
|
||||||
|
{isSelected && (
|
||||||
|
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||||
|
)}
|
||||||
|
</components.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,11 +6,12 @@ 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, FilterableSelect, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||||
import { useCreateSecretV3, useGetWsTags } from "@app/hooks/api";
|
import { useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
|
||||||
import { SecretType } from "@app/hooks/api/types";
|
import { SecretType } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
||||||
@@ -50,12 +51,32 @@ export const CreateSecretForm = ({
|
|||||||
const { closePopUp } = usePopUpAction();
|
const { closePopUp } = usePopUpAction();
|
||||||
|
|
||||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
|
const createWsTag = useCreateWsTag();
|
||||||
|
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||||
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
|
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
|
||||||
canReadTags ? workspaceId : ""
|
canReadTags ? workspaceId : ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const slugSchema = z.string().trim().toLowerCase().min(1);
|
||||||
|
const createNewTag = async (slug: string) => {
|
||||||
|
// TODO: Replace with slugSchema generic
|
||||||
|
try {
|
||||||
|
const parsedSlug = slugSchema.parse(slug);
|
||||||
|
await createWsTag.mutateAsync({
|
||||||
|
workspaceID: workspaceId,
|
||||||
|
tagSlug: parsedSlug,
|
||||||
|
tagColor: ""
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create new tag"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => {
|
const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await createSecretV3({
|
await createSecretV3({
|
||||||
@@ -148,16 +169,18 @@ export const CreateSecretForm = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FilterableSelect
|
<CreatableSelect
|
||||||
|
isMulti
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder="Select tags to assign to secret..."
|
placeholder="Select tags to assign to secret..."
|
||||||
isMulti
|
isValidNewOption={(v) => slugSchema.safeParse(v).success}
|
||||||
name="tagIds"
|
name="tagIds"
|
||||||
isDisabled={!canReadTags}
|
isDisabled={!canReadTags}
|
||||||
isLoading={isTagsLoading && canReadTags}
|
isLoading={isTagsLoading && canReadTags}
|
||||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
|
onCreateOption={createNewTag}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,15 +7,8 @@ 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 {
|
import { Button, Checkbox, FormControl, FormLabel, Input, Tooltip } from "@app/components/v2";
|
||||||
Button,
|
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
|
||||||
Checkbox,
|
|
||||||
FilterableSelect,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Input,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
@@ -27,6 +20,7 @@ import { getKeyValue } from "@app/helpers/parseEnvVar";
|
|||||||
import {
|
import {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
useCreateSecretV3,
|
useCreateSecretV3,
|
||||||
|
useCreateWsTag,
|
||||||
useGetWsTags,
|
useGetWsTags,
|
||||||
useUpdateSecretV3
|
useUpdateSecretV3
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
@@ -199,6 +193,25 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
|||||||
setValue("value", value);
|
setValue("value", value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createWsTag = useCreateWsTag();
|
||||||
|
const slugSchema = z.string().trim().toLowerCase().min(1);
|
||||||
|
const createNewTag = async (slug: string) => {
|
||||||
|
// TODO: Replace with slugSchema generic
|
||||||
|
try {
|
||||||
|
const parsedSlug = slugSchema.parse(slug);
|
||||||
|
await createWsTag.mutateAsync({
|
||||||
|
workspaceID: workspaceId,
|
||||||
|
tagSlug: parsedSlug,
|
||||||
|
tagColor: ""
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create new tag"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -249,16 +262,18 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FilterableSelect
|
<CreatableSelect
|
||||||
className="w-full"
|
|
||||||
placeholder="Select tags to assign to secrets..."
|
|
||||||
isMulti
|
isMulti
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Select tags to assign to secret..."
|
||||||
|
isValidNewOption={(v) => slugSchema.safeParse(v).success}
|
||||||
name="tagIds"
|
name="tagIds"
|
||||||
isDisabled={!canReadTags}
|
isDisabled={!canReadTags}
|
||||||
isLoading={isTagsLoading && canReadTags}
|
isLoading={isTagsLoading && canReadTags}
|
||||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
|
onCreateOption={createNewTag}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user