/* eslint-disable react/jsx-no-useless-fragment */ import { memo, useEffect, useRef, useState } from "react"; import { Control, Controller, useFieldArray, UseFormRegister, UseFormSetValue, useWatch } from "react-hook-form"; import { subject } from "@casl/ability"; import { faCheck, faCodeBranch, faComment, faCopy, faEllipsis, faInfoCircle, faTags, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { cx } from "cva"; import { twMerge } from "tailwind-merge"; // TODO:(akhilmhdh): Refactor this import AddTagPopoverContent from "@app/components/AddTagPopoverContent/AddTagPopoverContent"; import { ProjectPermissionCan } from "@app/components/permissions"; import { FormControl, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, Input, Popover, PopoverContent, PopoverTrigger, SecretInput, Tag, TextArea, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; import { WsTag } from "@app/hooks/api/types"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; type Props = { index: number; environment: string; secretPath: string; // backend generated unique id secUniqId?: string; // 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, secretName: string, id?: string, overrideId?: string) => void; // sidebar control props onRowExpand: (secId: string | undefined, index: number) => void; // tag props wsTags?: WsTag[]; onCreateTagOpen: () => void; // rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering control: Control; register: UseFormRegister; setValue: UseFormSetValue; isKeyError?: boolean; keyError?: string; autoCapitalization?: boolean; }; export const SecretInputRow = memo( ({ index, secretPath, environment, isSecretValueHidden, onRowExpand, isReadOnly, isRollbackMode, isAddOnly, wsTags, onCreateTagOpen, onSecretDelete, searchTerm, control, register, setValue, isKeyError, keyError, secUniqId, autoCapitalization }: Props): JSX.Element => { const isKeySubDisabled = useRef(false); // comment management in a row const { fields: secretTags, remove, append } = useFieldArray({ control, name: `secrets.${index}.tags` }); // display the tags in alphabetical order secretTags.sort((a, b) => a?.name?.localeCompare(b?.name)); // to get details on a secret const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction`, exact: true }); const idOverride = useWatch({ control, name: `secrets.${index}.idOverride`, exact: true }); const secComment = useWatch({ control, name: `secrets.${index}.comment`, exact: true }); const hasComment = Boolean(secComment); const secKey = useWatch({ control, name: `secrets.${index}.key`, disabled: isKeySubDisabled.current, exact: true }); const secValue = useWatch({ control, name: `secrets.${index}.value`, disabled: isKeySubDisabled.current, exact: true }); const secValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride`, disabled: isKeySubDisabled.current, exact: true }); // when secret is override by personal values const isOverridden = overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; const [hoveredTag, setHoveredTag] = useState(null); const handleTagOnMouseEnter = (wsTag: WsTag) => { setHoveredTag(wsTag); }; const handleTagOnMouseLeave = () => { setHoveredTag(null); }; const checkIfTagIsVisible = (wsTag: WsTag) => wsTag._id === hoveredTag?._id; const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true }); const tags = useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || []; const selectedTagIds = tags.reduce>( (prev, curr) => ({ ...prev, [curr.slug]: true }), {} ); const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); useEffect(() => { let timer: NodeJS.Timeout; if (isSecValueCopied) { timer = setTimeout(() => setIsSecValueCopied.off(), 2000); } return () => clearTimeout(timer); }, [isSecValueCopied]); const copyTokenToClipboard = () => { navigator.clipboard.writeText((secValueOverride || secValue) as string); setIsSecValueCopied.on(); }; 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) { const { _id: id, name, slug, tagColor } = selectedTag; append({ _id: id, name, slug, tagColor }); } else { const pos = tags.findIndex(({ slug }: { slug: string }) => 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(); }} autoCapitalization={autoCapitalization} />
{keyError}
)} />
{isOverridden ? ( ( )} /> ) : ( ( )} /> )}
{secretTags.map(({ id, slug, tagColor }) => { return ( <>
remove(i)} key={id} className="cursor-pointer" >
{slug}
onSelectTag(wsTag)} handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} handleTagOnMouseLeave={() => handleTagOnMouseLeave()} checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} handleOnCreateTagOpen={() => onCreateTagOpen()} /> ); })}
{!(isReadOnly || isAddOnly || isRollbackMode) && (
{(isAllowed) => ( )}
onSelectTag(wsTag)} handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} handleTagOnMouseLeave={() => handleTagOnMouseLeave()} checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} handleOnCreateTagOpen={() => onCreateTagOpen()} />
)}
{!isAddOnly && (
{(isAllowed) => (
)}
)}
{(isAllowed) => ( )}