diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 4a03533f9..bfb858892 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -1,15 +1,14 @@ /* eslint-disable react/no-danger */ -import React, { forwardRef, TextareaHTMLAttributes, useEffect, useRef, useState } from "react"; -import { faChevronRight, faFolder, faKey, faRecycle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { forwardRef, TextareaHTMLAttributes, useRef, useState } from "react"; import { twMerge } from "tailwind-merge"; -import { useWorkspace } from "@app/context"; -import { REGEX_SECRET_REFERENCE_FIND, REGEX_SECRET_REFERENCE_INVALID } from "@app/helpers/secret-reference"; +import { + REGEX_SECRET_REFERENCE_FIND, + REGEX_SECRET_REFERENCE_INVALID +} from "@app/helpers/secret-reference"; import { useToggle } from "@app/hooks"; -import { useGetUserWsKey } from "@app/hooks/api"; -import { useGetFoldersByEnv } from "@app/hooks/api/secretFolders/queries"; -import { useGetProjectSecrets } from "@app/hooks/api/secrets/queries"; + +import SecretReferenceSelect from "./SecretReferenceSelect"; const replaceContentWithDot = (str: string) => { let finalStr = ""; @@ -68,12 +67,6 @@ type Props = TextareaHTMLAttributes & { secretPath?: string; }; -type ReferenceType = { - name: string; - type: "folder" | "secret"; - slug?: string; -}; - const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all"; export const SecretInput = forwardRef( @@ -97,76 +90,11 @@ export const SecretInput = forwardRef( const [isSecretFocused, setIsSecretFocused] = useToggle(); const [value, setValue] = useState(propValue || ""); - const { currentWorkspace } = useWorkspace(); - const [listReference, setListReference] = useState([]); const [showReferencePopup, setShowReferencePopup] = useState(false); const [referenceKey, setReferenceKey] = useState(); const [lastCaretPos, setLastCaretPos] = useState(0); - const [secretPath, setSecretPath] = useState(propSecretPath || "/"); - const [environment, setEnvironment] = useState(propEnvironment); - 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 - }); - - useEffect(() => { - let currentEnvironment = propEnvironment; - let currentSecretPath = propSecretPath || "/"; - - if (!referenceKey) { - setSecretPath(currentSecretPath); - setEnvironment(currentEnvironment!); - return; - } - - const isNested = referenceKey.includes("."); - - if (isNested) { - const [envSlug, ...folderPaths] = referenceKey.split("."); - const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug); - currentEnvironment = isValidEnvSlug ? envSlug : undefined; - currentSecretPath = `/${folderPaths?.join("/")}` || "/"; - } - - setSecretPath(currentSecretPath); - setEnvironment(currentEnvironment); - }, [referenceKey]); - - useEffect(() => { - const currentListReference: ReferenceType[] = []; - const isNested = referenceKey?.includes("."); - - if (!environment) { - setListReference(currentListReference); - setShowReferencePopup(true); - return; - } - - if (isNested) { - folders?.forEach((folder) => { - currentListReference.unshift({ name: folder, type: "folder" }); - }); - } - - secrets?.forEach((secret) => { - currentListReference.unshift({ name: secret.key, type: "secret" }); - }); - - setListReference(currentListReference); - setShowReferencePopup(true); - }, [secrets, environment, referenceKey]); - - function findMatch(str: string, start: number) { + function isCaretInsideReference(str: string, start: number) { const matches = [...str.matchAll(REGEX_SECRET_REFERENCE_FIND)]; for (let i = 0; i < matches.length; i += 1) { const match = matches[i]; @@ -193,7 +121,7 @@ export const SecretInput = forwardRef( } function referencePopup(text: string, pos: number) { - const match = findMatch(text, pos); + const match = isCaretInsideReference(text, pos); if (match && typeof match.index !== "undefined") { setLastCaretPos(pos); setReferenceKey(match?.[2]); @@ -236,12 +164,22 @@ export const SecretInput = forwardRef( return; } } - // On Key up or down if the popup is open ignore it - if ((showReferencePopup && event.key === "ArrowUp") || event.key === "ArrowDown") { - event.preventDefault(); + if (!(event.key.startsWith("Arrow") || event.key === "Backspace" || event.key === "Delete")) { + handleReferencePopup(event.currentTarget); } + } - handleReferencePopup(event.currentTarget); + function 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(); + } } function handleMouseClick(event: React.MouseEvent) { @@ -268,7 +206,7 @@ export const SecretInput = forwardRef( } let newValue = value || ""; - const match = findMatch(newValue, lastCaretPos); + const match = isCaretInsideReference(newValue, lastCaretPos); const referenceStartIndex = match?.index || 0; const referenceEndIndex = referenceStartIndex + (match?.[0]?.length || 0); const [start, oldReference, end] = [ @@ -304,8 +242,13 @@ export const SecretInput = forwardRef( setValue(newValue); // TODO: there should be a better way to do onChange?.({ target: { value: newValue } } as any); - setCaretPos(start.length + replaceReference.length + offset); - if (type !== "secret") setReferenceKey(replaceReference); + setShowReferencePopup(type !== "secret"); + setTimeout(() => { + setIsSecretFocused.on(); + if (type !== "secret") setReferenceKey(replaceReference); + const caretPos = start.length + replaceReference.length + offset; + setCaretPos(caretPos); + }, 100); } function handleChange(event: React.ChangeEvent) { @@ -313,88 +256,17 @@ export const SecretInput = forwardRef( if (typeof onChange === "function") onChange(event); } + function handleReferenceOpenChange(currOpen: boolean) { + if (!currOpen) setShowReferencePopup(false); + } + return ( - // TODO: hide popup if the focus within the child component left
- {/* TODO(radix): Move to radix select component and scroll element */} - {showReferencePopup && isSecretFocused && ( -
-
- {listReference.map((e, i) => { - return ( - - ); - })} - -
- All Secrets -
- - {currentWorkspace?.environments.map((env, i) => ( - - ))} -
-
- )} -
@@ -415,6 +287,7 @@ export const SecretInput = forwardRef(
               )}
               onFocus={() => setIsSecretFocused.on()}
               onKeyUp={handleKeyUp}
+              onKeyDown={handleKeyDown}
               onClick={handleMouseClick}
               onChange={handleChange}
               disabled={isDisabled}
@@ -429,6 +302,20 @@ export const SecretInput = forwardRef(
             />
           
+ + handleReferenceOpenChange(isOpen)} + onSelect={(refValue) => handleReferenceSelect(refValue)} + onEscapeKeyDown={() => { + setTimeout(() => { + setCaretPos(lastCaretPos); + }, 200); + }} + />
); } diff --git a/frontend/src/components/v2/SecretInput/SecretReferenceSelect.tsx b/frontend/src/components/v2/SecretInput/SecretReferenceSelect.tsx new file mode 100644 index 000000000..da8cff98f --- /dev/null +++ b/frontend/src/components/v2/SecretInput/SecretReferenceSelect.tsx @@ -0,0 +1,211 @@ +import React, { useEffect, useState } from "react"; +import { + faCaretDown, + faCaretUp, + faChevronRight, + faFolder, + faKey, + faRecycle +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { twMerge } from "tailwind-merge"; + +import { useWorkspace } from "@app/context"; +import { useGetUserWsKey } from "@app/hooks/api"; +import { useGetFoldersByEnv } from "@app/hooks/api/secretFolders/queries"; +import { useGetProjectSecrets } from "@app/hooks/api/secrets/queries"; + +type ReferenceType = "environment" | "folder" | "secret"; + +type Props = { + open: boolean; + reference?: string; + secretPath?: string; + environment?: string; + handleOpenChange: (params: boolean) => void; + onEscapeKeyDown: () => void; + onSelect: (params: { type: ReferenceType; name: string; slug?: string }) => void; +}; + +type ReferenceItem = { + name: string; + type: "folder" | "secret"; + slug?: string; +}; + +export default function SecretReferenceSelect({ + open, + secretPath: propSecretPath, + environment: propEnvironment, + handleOpenChange, + reference, + onSelect, + onEscapeKeyDown +}: Props) { + const { currentWorkspace } = useWorkspace(); + const [listReference, setListReference] = useState([]); + const [secretPath, setSecretPath] = useState(propSecretPath || "/"); + const [environment, setEnvironment] = useState(propEnvironment); + 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 + }); + + useEffect(() => { + let currentEnvironment = propEnvironment; + let currentSecretPath = propSecretPath || "/"; + + if (!reference) { + setSecretPath(currentSecretPath); + setEnvironment(currentEnvironment!); + return; + } + + const isNested = reference.includes("."); + + if (isNested) { + const [envSlug, ...folderPaths] = reference.split("."); + const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug); + currentEnvironment = isValidEnvSlug ? envSlug : undefined; + currentSecretPath = `/${folderPaths?.join("/")}` || "/"; + } + + setSecretPath(currentSecretPath); + setEnvironment(currentEnvironment); + }, [reference]); + + useEffect(() => { + const currentListReference: ReferenceItem[] = []; + const isNested = reference?.includes("."); + + if (!environment) { + setListReference(currentListReference); + return; + } + + if (isNested) { + folders?.forEach((folder) => { + currentListReference.unshift({ name: folder, type: "folder" }); + }); + } + + secrets?.forEach((secret) => { + currentListReference.unshift({ name: secret.key, type: "secret" }); + }); + + setListReference(currentListReference); + }, [secrets, environment, reference]); + + return ( + onSelect(JSON.parse(str))} + > + + +
+ + + + + + +
+ +
+
+ + + {listReference.map((e, i) => { + return ( + + +
+
+
+ +
+
{e.name}
+
+ {e.type === "folder" && ( +
+ +
+ )} +
+
+
+ ); + })} + + {listReference.length !== 0 && ( + + )} +
+ + + + All Secrets + + + {currentWorkspace?.environments.map((env, i) => ( + + +
+
+
+ +
+
{env.name}
+
+
+ +
+
+
+
+ ))} +
+
+ +
+ +
+
+
+
+ + ); +}