diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index c88b26a23..b6888ba8c 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -1,86 +1,22 @@ -/* eslint-disable react/no-danger */ -import React, { forwardRef, TextareaHTMLAttributes, useEffect, useRef, useState } from "react"; +import { TextareaHTMLAttributes, useEffect, useRef, useState } from "react"; +import { faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as Popover from "@radix-ui/react-popover"; import { twMerge } from "tailwind-merge"; -import { useToggle } from "@app/hooks"; +import { useWorkspace } from "@app/context"; +import { useDebounce } from "@app/hooks"; +import { useGetFoldersByEnv, useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api"; -import SecretReferenceSelect, { ReferenceType } from "./SecretReferenceSelect"; +import { SecretInput } from "../SecretInput"; -const REGEX_SECRET_REFERENCE_FIND = /(\${([^}]*)})/g; -const REGEX_SECRET_REFERENCE_INVALID = /(?:\/|\\|\n|\.$|^\.)/; +const REGEX_UNCLOSED_SECRET_REFERENCE = /\${(?![^{}]*\})/g; -const isValidSecretReferenceValue = (str: string): boolean => { - try { - if (!str) return true; - let skipNext = false; - str.split(REGEX_SECRET_REFERENCE_FIND).flatMap((el) => { - if (skipNext) { - skipNext = false; - return []; - } - - const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}"); - if (!isInterpolationSyntax) return []; - - skipNext = true; - if (REGEX_SECRET_REFERENCE_INVALID.test(el.slice(2, -1))) - throw new Error("Invalid reference"); - - return el; - }); - return true; - } catch (e) { - return false; - } -}; - -const replaceContentWithDot = (str: string) => { - let finalStr = ""; - for (let i = 0; i < str.length; i += 1) { - const char = str.at(i); - finalStr += char === "\n" ? "\n" : "*"; - } - return finalStr; -}; - -const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => { - if (isImport) return "IMPORTED"; - if (content === "") return "EMPTY"; - if (!content) return "EMPTY"; - if (!isVisible) return replaceContentWithDot(content); - - let skipNext = false; - const formattedContent = content.split(REGEX_SECRET_REFERENCE_FIND).flatMap((el, i) => { - const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}"); - if (isInterpolationSyntax) { - skipNext = true; - return ( - - ${ - - {el.slice(2, -1)} - - } - - ); - } - if (skipNext) { - skipNext = false; - return []; - } - return el; - }); - - // akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough - // when break is added a line break works properly - return formattedContent.concat(
); -}; +export enum ReferenceType { + ENVIRONMENT = "environment", + FOLDER = "folder", + SECRET = "secret" +} type Props = TextareaHTMLAttributes & { value?: string | null; @@ -88,291 +24,296 @@ type Props = TextareaHTMLAttributes & { isVisible?: boolean; isReadOnly?: boolean; isDisabled?: boolean; - containerClassName?: string; - environment?: string; secretPath?: string; + environment?: string; + containerClassName?: string; }; -const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all"; +type ReferenceItem = { + name: string; + type: ReferenceType; + slug?: string; +}; -export const InfisicalSecretInput = forwardRef( - ( - { - value: propValue, - isVisible, - containerClassName, - onBlur, - isDisabled, - isImport, - isReadOnly, - onFocus, - secretPath: propSecretPath, - environment: propEnvironment, - onChange, - ...props - }, - ref - ) => { - const childRef = useRef(null); +export const InfisicalSecretInput = ({ + value: propValue, + isVisible, + containerClassName, + onBlur, + isDisabled, + isImport, + isReadOnly, + secretPath: propSecretPath, + environment: propEnvironment, + onChange, + ...props +}: Props) => { + const [inputValue, setInputValue] = useState(propValue ?? ""); + const [isSuggestionsOpen, setIsSuggestionsOpen] = useState(false); + const [currentCursorPosition, setCurrentCursorPosition] = useState(0); + const [currentReference, setCurrentReference] = useState(""); + const [secretPath, setSecretPath] = useState(propSecretPath || "/"); + const [environment, setEnvironment] = useState(propEnvironment); + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?.id || ""; + const { data: decryptFileKey } = useGetUserWsKey(workspaceId); + const { data: secrets } = useGetProjectSecrets({ + decryptFileKey: decryptFileKey!, + environment: environment || currentWorkspace?.environments?.[0].slug!, + secretPath, + workspaceId + }); + const { folderNames: folders } = useGetFoldersByEnv({ + path: secretPath, + environments: [environment || currentWorkspace?.environments?.[0].slug!], + projectId: workspaceId + }); - const [isSecretFocused, setIsSecretFocused] = useToggle(); - const [value, setValue] = useState(propValue || ""); - const [showReferencePopup, setShowReferencePopup] = useState(false); - const [referenceKey, setReferenceKey] = useState(); - const [lastCaretPos, setLastCaretPos] = useState(0); + const debouncedCurrentReference = useDebounce(currentReference, 100); - useEffect(() => { - setValue(propValue as string); - }, [propValue]); + const [listReference, setListReference] = useState([]); + const [highlightedIndex, setHighlightedIndex] = useState(-1); + const inputRef = useRef(null); - const isCaretInsideReference = (str: string, start: number) => { - if (str) { - const match = [...str.matchAll(REGEX_SECRET_REFERENCE_FIND)].find( - (entry) => - typeof entry?.index !== "undefined" && - entry.index <= start && - start < entry.index + entry[0].length - ); + useEffect(() => { + setInputValue(propValue ?? ""); + }, [propValue]); - return match || null; - } + useEffect(() => { + let currentEnvironment = propEnvironment; + let currentSecretPath = propSecretPath || "/"; - return null; - }; + if (!currentReference) { + setSecretPath(currentSecretPath); + setEnvironment(currentEnvironment); + return; + } - const setCaretPos = (caretPos: number) => { - if (childRef?.current) { - childRef.current.focus(); - const timeout = setTimeout(() => { - if (!childRef?.current) return; - childRef.current.selectionStart = caretPos; - childRef.current.selectionEnd = caretPos; - clearTimeout(timeout); - }, 200); - } - }; + const isNested = currentReference.includes("."); - const referencePopup = (text: string, pos: number) => { - const match = isCaretInsideReference(text, pos); - if (match && typeof match.index !== "undefined") { - setLastCaretPos(pos); - setReferenceKey(match?.[2]); - } - setShowReferencePopup(!!match); - }; + if (isNested) { + const [envSlug, ...folderPaths] = currentReference.split("."); + const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug); + currentEnvironment = isValidEnvSlug ? envSlug : undefined; - const handleReferencePopup = (element: HTMLTextAreaElement) => { - const { selectionStart, selectionEnd, value: text } = element; - if (selectionStart !== selectionEnd || selectionStart === 0) { - return; - } - referencePopup(text, selectionStart); - }; + // should be based on the last valid section (with .) + folderPaths.pop(); + currentSecretPath = `/${folderPaths?.join("/")}`; + } - const handleKeyUp = (event: React.KeyboardEvent) => { - if (event.key === "Escape") { - setShowReferencePopup(false); - return; - } + setSecretPath(currentSecretPath); + setEnvironment(currentEnvironment); + }, [debouncedCurrentReference]); - if (event.key === "{") { - // auto close the bracket - const currCaretPos = event.currentTarget.selectionEnd; - const isPrevDollar = value[currCaretPos - 2] === "$"; - if (!isPrevDollar) return; + useEffect(() => { + const currentListReference: ReferenceItem[] = []; + const isNested = currentReference?.includes("."); - const newValue = `${value.slice(0, currCaretPos)}}${value.slice(currCaretPos)}`; + if (!environment) { + currentWorkspace?.environments.forEach((env) => { + currentListReference.unshift({ + name: env.slug, + type: ReferenceType.ENVIRONMENT + }); + }); + } - setValue(newValue); - onChange?.({ target: { value: newValue } } as any); - setCaretPos(currCaretPos); + if (!environment || !currentReference) { + setListReference(currentListReference); + return; + } - if (event.currentTarget) { - const timeout = setTimeout(() => { - referencePopup(newValue, currCaretPos); - clearTimeout(timeout); - }, 200); + if (isNested) { + folders?.forEach((folder) => { + currentListReference.unshift({ name: folder, type: ReferenceType.FOLDER }); + }); + } else { + currentWorkspace?.environments.forEach((env) => { + currentListReference.unshift({ + name: env.slug, + type: ReferenceType.ENVIRONMENT + }); + }); + } - return; - } - } - if (!(event.key.startsWith("Arrow") || event.key === "Backspace" || event.key === "Delete")) { - handleReferencePopup(event.currentTarget); - } - }; + secrets?.forEach((secret) => { + currentListReference.unshift({ name: secret.key, type: ReferenceType.SECRET }); + }); - const handleKeyDown = (event: React.KeyboardEvent) => { - if ( - !( - event.key.startsWith("Arrow") || - ["Backspace", "Delete", "."].includes(event.key) || - (event.metaKey && event.key.toLowerCase() === "a") - ) - ) { - const match = isCaretInsideReference(value, event.currentTarget.selectionEnd); - if (match) event.preventDefault(); - } - }; - - const handleMouseClick = (event: React.MouseEvent) => { - handleReferencePopup(event.currentTarget); - }; - - const handleReferenceSelect = ({ - name, - type, - slug - }: { - name: string; - type: ReferenceType; - slug?: string; - }) => { - setShowReferencePopup(false); - - // forward ref for parent component - if (typeof ref === "function") { - ref(childRef.current); - } else if (ref && "current" in ref) { - const refCopy = ref; - refCopy.current = childRef.current; - } - - let newValue = value || ""; - const match = isCaretInsideReference(newValue, lastCaretPos); - const referenceStartIndex = match?.index || 0; - const referenceEndIndex = referenceStartIndex + (match?.[0]?.length || 0); - const [start, oldReference, end] = [ - value.slice(0, referenceStartIndex), - value.slice(referenceStartIndex, referenceEndIndex), - value.slice(referenceEndIndex) - ]; - - let oldReferenceStr = oldReference.slice(2, -1); - let currentPath = type === ReferenceType.ENVIRONMENT ? slug! : name; - currentPath = currentPath.replace(/\./g, "\\."); - - let replaceReference = ""; - let offset = 3; - switch (type) { - case ReferenceType.FOLDER: - replaceReference = `${oldReferenceStr}${currentPath}.`; - offset -= 1; - break; - case ReferenceType.SECRET: { - if (oldReferenceStr.indexOf(".") === -1) oldReferenceStr = ""; - replaceReference = `${oldReferenceStr}${currentPath}`; - break; - } - case ReferenceType.ENVIRONMENT: - replaceReference = `${currentPath}.`; - offset -= 1; - break; - default: - } - replaceReference = replaceReference.replace(/[//]/g, ""); - newValue = `${start}$\{${replaceReference}}${end}`; - setValue(newValue); - // TODO: there should be a better way to do - onChange?.({ target: { value: newValue } } as any); - setShowReferencePopup(type !== ReferenceType.SECRET); - const timeout = setTimeout(() => { - setIsSecretFocused.on(); - if (type !== ReferenceType.SECRET) setReferenceKey(replaceReference); - const caretPos = start.length + replaceReference.length + offset; - setCaretPos(caretPos); - clearTimeout(timeout); - }, 100); - }; - - const handleChange = (event: React.ChangeEvent) => { - setValue(event.target.value); - if (typeof onChange === "function") onChange(event); - }; - - const handleReferenceOpenChange = (currOpen: boolean) => { - if (!currOpen) setShowReferencePopup(false); - }; - - return ( -
-
-
-
-              
-                
-                  {syntaxHighlight(value, isVisible || isSecretFocused, isImport)}
-                
-              
-            
- -