import { memo, useEffect } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { subject } from "@casl/ability"; import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; import { faCheck, faClose, faCodeBranch, faComment, faCopy, faEllipsis, faKey, faTag, faTags } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { AnimatePresence, motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, FormControl, IconButton, Input, Popover, PopoverContent, PopoverTrigger, SecretInput, Spinner, TextArea, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; import { useToggle } from "@app/hooks"; import { DecryptedSecret } from "@app/hooks/api/secrets/types"; import { WsTag } from "@app/hooks/api/types"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; type Props = { secret: DecryptedSecret; onSaveSecret: ( orgSec: DecryptedSecret, modSec: Omit & { tags: { _id: string }[] } ) => Promise; onDeleteSecret: (sec: DecryptedSecret) => void; onDetailViewSecret: (sec: DecryptedSecret) => void; isVisible?: boolean; isSelected?: boolean; onToggleSecretSelect: (id: string) => void; tags: WsTag[]; onCreateTag: () => void; environment: string; secretPath: string; }; export const SecretItem = memo( ({ secret, onSaveSecret, onDeleteSecret, onDetailViewSecret, isVisible, isSelected, tags = [], onCreateTag, onToggleSecretSelect, environment, secretPath }: Props) => { const { currentWorkspace } = useWorkspace(); const permission = useProjectPermission(); const isReadOnly = permission.can( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ) && permission.cannot( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); const { handleSubmit, control, register, watch, setValue, reset, getValues, formState: { isDirty, isSubmitting } } = useForm({ defaultValues: secret, values: secret, resolver: zodResolver(formSchema) }); const overrideAction = watch("overrideAction"); const hasComment = Boolean(watch("comment")); const selectedTags = watch("tags", []); const selectedTagsGroupById = selectedTags.reduce>( (prev, curr) => ({ ...prev, [curr._id]: true }), {} ); const { fields, append, remove } = useFieldArray({ control, name: "tags" }); const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); useEffect(() => { let timer: NodeJS.Timeout; if (isSecValueCopied) { timer = setTimeout(() => setIsSecValueCopied.off(), 2000); } return () => clearTimeout(timer); }, [isSecValueCopied]); const isOverriden = overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; const hasTagsApplied = Boolean(fields.length); const handleOverrideClick = () => { if (isOverriden) { // override need not be flagged delete if it was never saved in server // meaning a new unsaved personal secret but user toggled back later const isUnsavedOverride = !secret.idOverride; setValue( "overrideAction", isUnsavedOverride ? secret?.overrideAction : SecretActionType.Deleted, { shouldDirty: !isUnsavedOverride } ); setValue("valueOverride", secret?.valueOverride, { shouldDirty: !isUnsavedOverride }); } else { setValue("overrideAction", SecretActionType.Modified, { shouldDirty: true }); setValue("valueOverride", "", { shouldDirty: true }); } }; const handleFormSubmit = async (data: TFormSchema) => { await onSaveSecret(secret, { ...secret, ...data }); }; const handleTagSelect = (tag: WsTag) => { if (selectedTagsGroupById?.[tag._id]) { const tagPos = selectedTags.findIndex(({ _id }) => _id === tag._id); if (tagPos !== -1) { remove(tagPos); } } else { append(tag); } }; const copyTokenToClipboard = () => { const [overrideValue, value] = getValues(["value", "valueOverride"]); navigator.clipboard.writeText((overrideValue || value) as string); setIsSecValueCopied.on(); }; return (
onToggleSecretSelect(secret._id)} className={twMerge("group-hover:flex hidden ml-3", isSelected && "flex")} />
( )} />
{isOverriden ? ( ( )} /> ) : ( ( )} /> )}
{(isAllowed) => ( )} Apply tags to this secrets {tags.map((tag) => { const { _id: tagId, name, tagColor } = tag; const isTagSelected = selectedTagsGroupById?.[tagId]; return ( handleTagSelect(tag)} key={tagId} icon={isTagSelected && } iconPos="right" >
{name}
); })} {(isAllowed) => ( )} {(isAllowed) => ( )}