/* eslint-disable react/jsx-no-useless-fragment */ import { memo, useEffect, useRef } from "react"; import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { faCheck, faCodeBranch, faComment, faCopy, faEllipsis, faInfoCircle, faPlus, faTags, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { cx } from "cva"; import { twMerge } from "tailwind-merge"; import { Button, Checkbox, FormControl, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, Input, Popover, PopoverContent, PopoverTrigger, Tag, TextArea, Tooltip } from "@app/components/v2"; import { useToggle } from "@app/hooks"; import { WsTag } from "@app/hooks/api/types"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; import { MaskedInput } from "./MaskedInput"; type Props = { index: number; // permission and external state's that decided to hide or show isReadOnly?: boolean; isAddOnly?: boolean; isRollbackMode?: boolean; isSecretValueHidden: boolean; searchTerm: string; // to record the ids of deleted ones onSecretDelete: (index: number, id?: string, overrideId?: string) => void; // sidebar control props onRowExpand: () => void; // tag props wsTags?: WsTag[]; onCreateTagOpen: () => void; }; const tagColors = [ { bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" }, { bg: "bg-[#cb1c8d]/40", text: "text-[#f2c6e3]/70" }, { bg: "bg-[#badc58]/40", text: "text-[#eef6d5]/70" }, { bg: "bg-[#ff5400]/40", text: "text-[#ffddcc]/70" }, { bg: "bg-[#3AB0FF]/40", text: "text-[#f0fffd]/70" }, { bg: "bg-[#6F1AB6]/40", text: "text-[#FFE5F1]/70" }, { bg: "bg-[#C40B13]/40", text: "text-[#FFDEDE]/70" }, { bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" } ]; export const SecretInputRow = memo( ({ index, isSecretValueHidden, onRowExpand, isReadOnly, isRollbackMode, isAddOnly, wsTags, onCreateTagOpen, onSecretDelete, searchTerm }: Props): JSX.Element => { const isKeySubDisabled = useRef(false); const { register, setValue, control } = useFormContext(); // comment management in a row const { fields: secretTags, remove, append } = useFieldArray({ control, name: `secrets.${index}.tags` }); // to get details on a secret const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` }); const idOverride = useWatch({ control, name: `secrets.${index}.idOverride` }); const secComment = useWatch({ control, name: `secrets.${index}.comment` }); const hasComment = Boolean(secComment); const secKey = useWatch({ control, name: `secrets.${index}.key`, disabled: isKeySubDisabled.current }); const secValue = useWatch({ control, name: `secrets.${index}.value`, disabled: isKeySubDisabled.current }); const secValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride`, disabled: isKeySubDisabled.current }) const secId = useWatch({ control, name: `secrets.${index}._id` }); const tags = useWatch({ control, name: `secrets.${index}.tags`, defaultValue: [] }) || []; const selectedTagIds = tags.reduce>( (prev, curr) => ({ ...prev, [curr.slug]: true }), {} ); const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); useEffect(() => { let timer: NodeJS.Timeout; if (isInviteLinkCopied) { timer = setTimeout(() => setInviteLinkCopied.off(), 2000); } return () => clearTimeout(timer); }, [isInviteLinkCopied]); const copyTokenToClipboard = () => { navigator.clipboard.writeText((secValueOverride || secValue) as string); setInviteLinkCopied.on(); }; // when secret is override by personal values const isOverridden = overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; const onSecretOverride = () => { if (isOverridden) { // when user created a new override but then removes if (overrideAction === SecretActionType.Created) setValue(`secrets.${index}.valueOverride`, ""); setValue(`secrets.${index}.overrideAction`, SecretActionType.Deleted, { shouldDirty: true }); } else { setValue(`secrets.${index}.valueOverride`, ""); setValue( `secrets.${index}.overrideAction`, idOverride ? SecretActionType.Modified : SecretActionType.Created, { shouldDirty: true } ); } }; const onSelectTag = (selectedTag: WsTag) => { const shouldAppend = !selectedTagIds[selectedTag.slug]; if (shouldAppend) { append(selectedTag); } else { const pos = tags.findIndex(({ slug }) => selectedTag.slug === slug); remove(pos); } }; const isCreatedSecret = !secId; const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly; // Why this instead of filter in parent // Because rhf field.map has default values so basically // keys are not updated there and index needs to kept so that we can monitor // values individually here if ( !( secKey?.toUpperCase().includes(searchTerm?.toUpperCase()) || tags ?.map((tag) => tag.name) .join(" ") ?.toUpperCase() .includes(searchTerm?.toUpperCase()) || secComment?.toUpperCase().includes(searchTerm?.toUpperCase()) ) ) { return <>; } return (
{index + 1}
(
{ isKeySubDisabled.current = true; }} variant="plain" isDisabled={isReadOnly || shouldBeBlockedInAddOnly || isRollbackMode} className="w-full focus:text-bunker-100 focus:ring-transparent" {...field} onBlur={() => { isKeySubDisabled.current = false; field.onBlur(); }} />
{error?.message}
)} />
{secretTags.map(({ id, slug }, i) => ( remove(i)} key={id} > {slug} ))}
{!(isReadOnly || isAddOnly || isRollbackMode) && (
Add tags to {secKey || "this secret"}
{wsTags?.map((wsTag) => ( ))}
)}
{!isAddOnly && (
)}