Compare commits

..

5 Commits

Author SHA1 Message Date
Scott Wilson
48fb77be49 improvement: typed date format 2024-11-27 12:17:30 -08:00
Scott Wilson
ca55f19926 improvement: add placeholder 2024-11-26 15:10:05 -08:00
Scott Wilson
3794521c56 improvement: project select filterable on audit logs with minor UI revisions 2024-11-26 15:07:20 -08:00
McPizza
92ce05283b feat: Add new tag when creating secret (#2791)
* feat: Add new tag when creating secret
2024-11-26 21:10:14 +01:00
Maidul Islam
39d92ce6ff Merge pull request #2799 from Infisical/misc/finalize-env-default
misc: finalized env schema handling
2024-11-26 15:10:06 -05:00
12 changed files with 232 additions and 126 deletions

View File

@@ -89,7 +89,7 @@
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-select": "^5.8.1",
"react-select": "^5.8.3",
"react-table": "^7.8.0",
"react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",
@@ -21259,9 +21259,9 @@
}
},
"node_modules/react-select": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
"integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz",
"integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0",

View File

@@ -162,4 +162,4 @@
"tailwindcss": "3.2",
"typescript": "^4.9.3"
}
}
}

View File

@@ -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}
/>
);
};

View File

@@ -0,0 +1 @@
export * from "./CreatableSelect";

View File

@@ -14,6 +14,7 @@ export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
onChange: (date?: Date) => void;
popUpProps: PopoverProps;
popUpContentProps: PopoverContentProps;
dateFormat?: "PPP" | "PP" | "P"; // extend as needed
};
// Doc: https://react-day-picker.js.org/
@@ -22,6 +23,7 @@ export const DatePicker = ({
onChange,
popUpProps,
popUpContentProps,
dateFormat = "PPP",
...props
}: DatePickerProps) => {
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
@@ -53,7 +55,7 @@ export const DatePicker = ({
<Popover {...popUpProps}>
<PopoverTrigger asChild>
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faCalendar} />}>
{value ? format(value, "PPP") : "Pick a date and time"}
{value ? format(value, dateFormat) : "Pick a date and time"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-fit p-2" {...popUpContentProps}>

View File

@@ -1,50 +1,7 @@
import 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 Select, { Props } from "react-select";
import { twMerge } from "tailwind-merge";
const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
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>
);
};
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: Props<T>) => (
<Select
@@ -90,7 +47,7 @@ export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: P
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",
"mt-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md",
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
option: ({ isFocused, isSelected }) =>
twMerge(

View 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>
);
};

View File

@@ -12,6 +12,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FilterableSelect,
FormControl,
Select,
SelectItem
@@ -64,7 +65,7 @@ export const LogsFilter = ({
useEffect(() => {
if (workspacesInOrg.length) {
setValue("projectId", workspacesInOrg[0].id);
setValue("project", workspacesInOrg[0]);
}
}, [workspaces]);
@@ -111,11 +112,34 @@ export const LogsFilter = ({
return (
<div
className={twMerge(
"sticky top-20 z-10 flex items-center justify-between bg-bunker-800",
"sticky top-20 z-10 flex flex-wrap items-center justify-between bg-bunker-800",
className
)}
>
<div className="flex items-center space-x-2">
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
<Controller
control={control}
name="project"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Project"
errorText={error?.message}
isError={Boolean(error)}
className="mr-12 w-64"
>
<FilterableSelect
value={value}
onChange={onChange}
placeholder="Select a project..."
options={workspacesInOrg.map(({ name, id }) => ({ name, id }))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
)}
<div className="mt-1 flex items-center space-x-2">
<Controller
control={control}
name="eventType"
@@ -123,7 +147,7 @@ export const LogsFilter = ({
<FormControl label="Events">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
<div className="inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{selectedEventTypes?.length === 1
? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0])
?.label
@@ -235,37 +259,6 @@ export const LogsFilter = ({
</FormControl>
)}
/>
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
<Controller
control={control}
name="projectId"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
<FormControl
label="Project"
errorText={error?.message}
isError={Boolean(error)}
className="w-40"
>
<Select
value={value}
{...field}
onValueChange={(e) => onChange(e)}
className={twMerge(
"w-full border border-mineshaft-500 bg-mineshaft-700 ",
value === undefined && "text-mineshaft-400"
)}
>
{workspacesInOrg.map((project) => (
<SelectItem value={String(project.id || "")} key={project.id}>
{project.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
<Controller
name="startDate"
control={control}
@@ -275,6 +268,7 @@ export const LogsFilter = ({
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
@@ -294,6 +288,7 @@ export const LogsFilter = ({
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
@@ -304,27 +299,27 @@ export const LogsFilter = ({
);
}}
/>
<Button
isLoading={false}
colorSchema="primary"
variant="outline_bg"
className="mt-[0.45rem]"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
onClick={() =>
reset({
eventType: presets?.eventType || [],
actor: presets?.actorId,
userAgentType: undefined,
startDate: undefined,
endDate: undefined,
project: null
})
}
>
Clear filters
</Button>
</div>
<Button
isLoading={false}
colorSchema="primary"
variant="outline_bg"
className="mt-1.5"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
onClick={() =>
reset({
eventType: presets?.eventType || [],
actor: presets?.actorId,
userAgentType: undefined,
startDate: undefined,
endDate: undefined,
projectId: undefined
})
}
>
Clear filters
</Button>
</div>
);
};

View File

@@ -47,7 +47,7 @@ export const LogsSection = ({
const { control, reset, watch, setValue } = useForm<AuditLogFilterFormData>({
resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: {
projectId: undefined,
project: null,
actor: presets?.actorId,
eventType: presets?.eventType || [],
page: 1,
@@ -66,7 +66,7 @@ export const LogsSection = ({
const eventType = watch("eventType") as EventType[] | undefined;
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor");
const projectId = watch("projectId");
const projectId = watch("project")?.id;
const startDate = watch("startDate");
const endDate = watch("endDate");

View File

@@ -5,7 +5,7 @@ import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
export const auditLogFilterFormSchema = yup
.object({
eventMetadata: yup.object({}).optional(),
projectId: yup.string().optional(),
project: yup.object({ id: yup.string().required(), name: yup.string().required() }).nullable(),
eventType: yup.array(yup.string().oneOf(Object.values(EventType), "Invalid event type")),
actor: yup.string(),
userAgentType: yup.string().oneOf(Object.values(UserAgentType), "Invalid user agent type"),

View File

@@ -6,11 +6,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
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 { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
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 { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
@@ -50,12 +51,32 @@ export const CreateSecretForm = ({
const { closePopUp } = usePopUpAction();
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const createWsTag = useCreateWsTag();
const { permission } = useProjectPermission();
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
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) => {
try {
await createSecretV3({
@@ -148,16 +169,18 @@ export const CreateSecretForm = ({
)
}
>
<FilterableSelect
<CreatableSelect
isMulti
className="w-full"
placeholder="Select tags to assign to secret..."
isMulti
isValidNewOption={(v) => slugSchema.safeParse(v).success}
name="tagIds"
isDisabled={!canReadTags}
isLoading={isTagsLoading && canReadTags}
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
value={field.value}
onChange={field.onChange}
onCreateOption={createNewTag}
/>
</FormControl>
)}

View File

@@ -7,15 +7,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormControl,
FormLabel,
Input,
Tooltip
} from "@app/components/v2";
import { Button, Checkbox, FormControl, FormLabel, Input, Tooltip } from "@app/components/v2";
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import {
ProjectPermissionActions,
@@ -27,6 +20,7 @@ import { getKeyValue } from "@app/helpers/parseEnvVar";
import {
useCreateFolder,
useCreateSecretV3,
useCreateWsTag,
useGetWsTags,
useUpdateSecretV3
} from "@app/hooks/api";
@@ -199,6 +193,25 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
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 (
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
<FormControl
@@ -249,16 +262,18 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
)
}
>
<FilterableSelect
className="w-full"
placeholder="Select tags to assign to secrets..."
<CreatableSelect
isMulti
className="w-full"
placeholder="Select tags to assign to secret..."
isValidNewOption={(v) => slugSchema.safeParse(v).success}
name="tagIds"
isDisabled={!canReadTags}
isLoading={isTagsLoading && canReadTags}
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
value={field.value}
onChange={field.onChange}
onCreateOption={createNewTag}
/>
</FormControl>
)}