mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 00:28:31 +00:00
Merge pull request #1403 from Infisical/daniel/improve-reminders
(Fix): Improve reminders
This commit is contained in:
+38
-19
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faClock } from "@fortawesome/free-solid-svg-icons";
|
import { faClock, faTrash } 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 { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@@ -9,20 +9,28 @@ import { z } from "zod";
|
|||||||
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
|
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
|
||||||
|
|
||||||
const ReminderFormSchema = z.object({
|
const ReminderFormSchema = z.object({
|
||||||
note: z.string().optional(),
|
note: z.string().optional().nullable(),
|
||||||
days: z
|
days: z
|
||||||
.number()
|
.number()
|
||||||
.min(1, { message: "Must be at least 1 day" })
|
.min(1, { message: "Must be at least 1 day" })
|
||||||
.max(365, { message: "Must be less than 365 days" })
|
.max(365, { message: "Must be less than 365 days" })
|
||||||
|
.nullable()
|
||||||
});
|
});
|
||||||
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
|
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
|
||||||
|
|
||||||
interface ReminderFormProps {
|
interface ReminderFormProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
repeatDays?: number | null;
|
||||||
|
note?: string | null;
|
||||||
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
|
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps) => {
|
export const CreateReminderForm = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
repeatDays,
|
||||||
|
note
|
||||||
|
}: ReminderFormProps) => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
@@ -31,32 +39,31 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TReminderFormSchema>({
|
} = useForm<TReminderFormSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
days: repeatDays || undefined,
|
||||||
|
note: note || ""
|
||||||
|
},
|
||||||
resolver: zodResolver(ReminderFormSchema)
|
resolver: zodResolver(ReminderFormSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFormSubmit = async (data: TReminderFormSchema) => {
|
const handleFormSubmit = async (data: TReminderFormSchema) => {
|
||||||
console.log(data);
|
|
||||||
onOpenChange(false, data);
|
onOpenChange(false, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
reset();
|
reset({
|
||||||
|
days: repeatDays || undefined,
|
||||||
|
note: note || ""
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Create secret reminder"
|
title={`${repeatDays ? "Update" : "Create"} reminder`}
|
||||||
// ? QUESTION: Should this specifically say its for secret rotation?
|
subTitle="Set up a reminder for when this secret should be rotated. Everyone with access to this project will be notified when the reminder is triggered."
|
||||||
// ? Or should we be call it something more generic?
|
|
||||||
subTitle={
|
|
||||||
<div>
|
|
||||||
Set up a reminder for when this secret should be rotated. Everyone with access to this
|
|
||||||
project will be notified when the reminder is triggered.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -68,7 +75,7 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
<>
|
<>
|
||||||
<FormControl
|
<FormControl
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
label="How many days between"
|
label="Reminder Interval (in days)"
|
||||||
isError={Boolean(fieldState.error)}
|
isError={Boolean(fieldState.error)}
|
||||||
errorText={fieldState.error?.message || ""}
|
errorText={fieldState.error?.message || ""}
|
||||||
>
|
>
|
||||||
@@ -76,6 +83,7 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
|
onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="31"
|
placeholder="31"
|
||||||
|
value={field.value || undefined}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div
|
<div
|
||||||
@@ -84,7 +92,8 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
field.value ? "opacity-60" : "opacity-0"
|
field.value ? "opacity-60" : "opacity-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Every {field.value > 1 ? `${field.value} days` : "day"}
|
A reminder will be sent every{" "}
|
||||||
|
{field.value && field.value > 1 ? `${field.value} days` : "day"}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -102,17 +111,27 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-7 flex items-center">
|
<div className="mt-7 flex items-center space-x-4">
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
key="layout-create-project-submit"
|
key="layout-create-project-submit"
|
||||||
className="mr-4"
|
className=""
|
||||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create reminder
|
{repeatDays ? "Update" : "Create"} reminder
|
||||||
</Button>
|
</Button>
|
||||||
|
{repeatDays && (
|
||||||
|
<Button
|
||||||
|
key="layout-cancel-create-project"
|
||||||
|
onClick={() => onOpenChange(false, { days: null, note: null })}
|
||||||
|
colorSchema="danger"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete reminder
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
key="layout-cancel-create-project"
|
key="layout-cancel-create-project"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faCircle,
|
faCircle,
|
||||||
faCircleDot,
|
faCircleDot,
|
||||||
|
faClock,
|
||||||
faPlus,
|
faPlus,
|
||||||
faTag
|
faTag
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -33,9 +34,11 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
import { useGetSecretVersion } from "@app/hooks/api";
|
import { useGetSecretVersion } from "@app/hooks/api";
|
||||||
import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
|
import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import { CreateReminderForm } from "./CreateReminderForm";
|
||||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -147,13 +150,34 @@ export const SecretDetailSidebar = ({
|
|||||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||||
|
|
||||||
|
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||||
|
const secretReminderNote = watch("reminderNote");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<CreateReminderForm
|
||||||
|
repeatDays={secretReminderRepeatDays}
|
||||||
|
note={secretReminderNote}
|
||||||
|
isOpen={createReminderFormOpen}
|
||||||
|
onOpenChange={(_, data) => {
|
||||||
|
setCreateReminderFormOpen.toggle();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setValue("reminderRepeatDays", data.days, { shouldDirty: true });
|
||||||
|
setValue("reminderNote", data.note, { shouldDirty: true });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Drawer
|
<Drawer
|
||||||
onOpenChange={(state) => {
|
onOpenChange={(state) => {
|
||||||
if (isOpen && isDirty) {
|
if (isOpen && isDirty) {
|
||||||
if (
|
if (
|
||||||
// eslint-disable-next-line no-alert
|
// eslint-disable-next-line no-alert
|
||||||
window.confirm("You have edited the secret. Are you sure you want to reset the change?")
|
window.confirm(
|
||||||
|
"You have edited the secret. Are you sure you want to reset the change?"
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
onToggle(false);
|
onToggle(false);
|
||||||
reset();
|
reset();
|
||||||
@@ -313,6 +337,39 @@ export const SecretDetailSidebar = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl label="Reminder">
|
||||||
|
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
||||||
|
<div className="mt-2 ml-1 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
|
||||||
|
<span className="text-sm text-bunker-300">
|
||||||
|
Reminder every {secretReminderRepeatDays}{" "}
|
||||||
|
{secretReminderRepeatDays > 1 ? "days" : "day"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="px-2 py-1"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => setCreateReminderFormOpen.on()}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-2 ml-1 flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
className="px-2 py-1"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||||
|
onClick={() => setCreateReminderFormOpen.on()}
|
||||||
|
>
|
||||||
|
Create Reminder
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
<FormControl label="Comments & Notes">
|
<FormControl label="Comments & Notes">
|
||||||
<TextArea
|
<TextArea
|
||||||
className="border border-mineshaft-600 text-sm"
|
className="border border-mineshaft-600 text-sm"
|
||||||
@@ -404,5 +461,6 @@ export const SecretDetailSidebar = ({
|
|||||||
</form>
|
</form>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,9 +111,11 @@ export const SecretItem = memo(
|
|||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||||
|
const secretReminderNote = watch("reminderNote");
|
||||||
|
|
||||||
const overrideAction = watch("overrideAction");
|
const overrideAction = watch("overrideAction");
|
||||||
const hasComment = Boolean(watch("comment"));
|
const hasComment = Boolean(watch("comment"));
|
||||||
const hasReminder = Boolean(watch("reminderRepeatDays"));
|
|
||||||
|
|
||||||
const selectedTags = watch("tags", []);
|
const selectedTags = watch("tags", []);
|
||||||
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
||||||
@@ -191,6 +193,8 @@ export const SecretItem = memo(
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateReminderForm
|
<CreateReminderForm
|
||||||
|
repeatDays={secretReminderRepeatDays}
|
||||||
|
note={secretReminderNote}
|
||||||
isOpen={createReminderFormOpen}
|
isOpen={createReminderFormOpen}
|
||||||
onOpenChange={(_, data) => {
|
onOpenChange={(_, data) => {
|
||||||
setCreateReminderFormOpen.toggle();
|
setCreateReminderFormOpen.toggle();
|
||||||
@@ -380,22 +384,24 @@ export const SecretItem = memo(
|
|||||||
<IconButton
|
<IconButton
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
||||||
hasReminder && "w-5 text-primary"
|
Boolean(secretReminderRepeatDays) && "w-5 text-primary"
|
||||||
)}
|
)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="md"
|
size="md"
|
||||||
ariaLabel="add-reminder"
|
ariaLabel="add-reminder"
|
||||||
>
|
>
|
||||||
<Tooltip content="Reminder">
|
<Tooltip
|
||||||
<FontAwesomeIcon
|
content={
|
||||||
onClick={() => {
|
secretReminderRepeatDays && secretReminderRepeatDays > 0
|
||||||
if (!hasReminder) {
|
? `Every ${secretReminderRepeatDays} day${
|
||||||
setCreateReminderFormOpen.on();
|
Number(secretReminderRepeatDays) > 1 ? "s" : ""
|
||||||
} else {
|
|
||||||
setValue("reminderRepeatDays", null, { shouldDirty: true });
|
|
||||||
setValue("reminderNote", null, { shouldDirty: true });
|
|
||||||
}
|
}
|
||||||
}}
|
`
|
||||||
|
: "Reminder"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
onClick={() => setCreateReminderFormOpen.on()}
|
||||||
icon={faClock}
|
icon={faClock}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
Reference in New Issue
Block a user