From 1c5dd0c35f7d300268f589db52d4f3655fd11796 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 12 Sep 2025 17:35:45 -0300 Subject: [PATCH] Improve UI no reference found message --- .../InfisicalSecretInput.tsx | 156 +++++++++--------- .../components/v2/SecretInput/SecretInput.tsx | 24 +-- 2 files changed, 81 insertions(+), 99 deletions(-) diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index c4b438e9a..dbeeb8747 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -1,5 +1,5 @@ import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react"; -import { faFolder, faKey, faLayerGroup } from "@fortawesome/free-solid-svg-icons"; +import { faFolder, faKey, faLayerGroup, faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; @@ -9,15 +9,6 @@ import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api"; import { SecretInput } from "../SecretInput"; -// Regex to find all secret references in the format ${reference} -const REFERENCE_REGEX = /\${([^}]+)}/g; - -// Extract unique references from a value -const extractReferences = (value: string): string[] => { - const matches = Array.from(value.matchAll(REFERENCE_REGEX)); - return [...new Set(matches.map((match) => match[1]))]; -}; - const getIndexOfUnclosedRefToTheLeft = (value: string, pos: number) => { // take substring up to pos in order to consider edits for closed references for (let i = pos; i >= 1; i -= 1) { @@ -92,7 +83,7 @@ export const InfisicalSecretInput = forwardRef( const [highlightedIndex, setHighlightedIndex] = useState(-1); - const inputRef = useRef(null); + const inputRef = useRef(null); const popoverContentRef = useRef(null); const [isFocused, setIsFocused] = useToggle(false); const currentCursorPosition = inputRef.current?.selectionStart || 0; @@ -148,8 +139,6 @@ export const InfisicalSecretInput = forwardRef( } }); - const allReferences = useMemo(() => extractReferences(value), [value]); - const suggestions = useMemo(() => { if (!isPopupOpen) return []; // reset highlight whenever recomputation happens @@ -186,51 +175,22 @@ export const InfisicalSecretInput = forwardRef( type: ReferenceType.SECRET }); }); - return suggestionsArr; - }, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]); - // Mark as invalid when editing and reference doesn't match any suggestion - const invalidReferences = useMemo(() => { - const invalid = new Set(); - - if (!isPopupOpen) { - return invalid; // No validation when not editing - } - - const suggestionsHaveLoaded = Boolean(secrets || folders || currentWorkspace?.environments); - - if (!suggestionsHaveLoaded) { - return invalid; - } - - // If we have an active suggestion context but no suggestions, it means the query returned empty - const suggestionsAreEmpty = suggestions.length === 0; - - allReferences.forEach((reference) => { - const matchesAnySuggestion = suggestions.some((suggestion) => { - if (!reference.includes(".")) { - return suggestion.slug === reference; - } - const parts = reference.split("."); - const finalPart = parts[parts.length - 1]; - return suggestion.slug === finalPart; + if (suggestionsArr.length === 0 && suggestionSource.predicate.trim()) { + suggestionsArr.push({ + label: "No matches found", + slug: "__no_match__", + type: ReferenceType.SECRET }); + } - // Mark as invalid only if: - // 1. We have suggestions loaded AND none match, OR - // 2. The query returned empty results - if (!matchesAnySuggestion && (suggestions.length > 0 || suggestionsAreEmpty)) { - invalid.add(reference); - } - }); - - return invalid; - }, [isPopupOpen, allReferences, suggestions, secrets, folders, currentWorkspace?.environments]); + return suggestionsArr; + }, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.predicate]); const handleSuggestionSelect = (selectIndex?: number) => { const selectedSuggestion = suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex]; - if (!selectedSuggestion) { + if (!selectedSuggestion || selectedSuggestion.slug === "__no_match__") { return; } @@ -275,21 +235,40 @@ export const InfisicalSecretInput = forwardRef( if (isPopupOpen) { if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) { setHighlightedIndex((prevIndex) => { - const pos = mod(prevIndex + 1, suggestions.length); - popoverContentRef.current?.children?.[pos]?.scrollIntoView({ + let nextIndex = mod(prevIndex + 1, suggestions.length); + // Skip "no match" messages + while ( + nextIndex < suggestions.length && + suggestions[nextIndex].slug === "__no_match__" + ) { + nextIndex = mod(nextIndex + 1, suggestions.length); + } + // If we only have no-match messages, don't highlight anything + if (suggestions[nextIndex]?.slug === "__no_match__") { + return -1; + } + popoverContentRef.current?.children?.[nextIndex]?.scrollIntoView({ block: "nearest", behavior: "smooth" }); - return pos; + return nextIndex; }); } else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) { setHighlightedIndex((prevIndex) => { - const pos = mod(prevIndex - 1, suggestions.length); - popoverContentRef.current?.children?.[pos]?.scrollIntoView({ + let prevIdx = mod(prevIndex - 1, suggestions.length); + // Skip "no match" messages + while (prevIdx >= 0 && suggestions[prevIdx].slug === "__no_match__") { + prevIdx = mod(prevIdx - 1, suggestions.length); + } + // If we only have no-match messages, don't highlight anything + if (suggestions[prevIdx]?.slug === "__no_match__") { + return -1; + } + popoverContentRef.current?.children?.[prevIdx]?.scrollIntoView({ block: "nearest", behavior: "smooth" }); - return pos; + return prevIdx; }); } else if (e.key === "Enter" && highlightedIndex >= 0) { e.preventDefault(); @@ -306,20 +285,18 @@ export const InfisicalSecretInput = forwardRef( }; // to handle multiple ref for single component - const handleRef = useCallback( - (el: HTMLTextAreaElement) => { - inputRef.current = el; - if (ref) { - if (typeof ref === "function") { - ref(el); - } else { - // eslint-disable-next-line no-param-reassign - ref.current = el; - } + const handleRef = useCallback((el: HTMLTextAreaElement) => { + // @ts-expect-error this is for multiple ref single component + inputRef.current = el; + if (ref) { + if (typeof ref === "function") { + ref(el); + } else { + // eslint-disable-next-line + ref.current = el; } - }, - [ref] - ); + } + }, []); return ( @@ -338,7 +315,6 @@ export const InfisicalSecretInput = forwardRef( }} onChange={(e) => onChange?.(e.target.value)} containerClassName={containerClassName} - invalidReferences={invalidReferences} /> ( {suggestions.map((item, i) => { let entryIcon; let subText; - if (item.type === ReferenceType.SECRET) { + const isNoMatchMessage = item.slug === "__no_match__"; + + if (isNoMatchMessage) { + entryIcon = ; + subText = "No results"; + } else if (item.type === ReferenceType.SECRET) { entryIcon = ; subText = "Secret"; } else if (item.type === ReferenceType.ENVIRONMENT) { @@ -367,10 +348,28 @@ export const InfisicalSecretInput = forwardRef( subText = "Folder"; } - return ( + return isNoMatchMessage ? (
+
+
+
{entryIcon}
+
+ {item.label} +
+ {subText} +
+
+
+
+
+ ) : ( + ); })} diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index e9f6d2625..f9b5b6f1b 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -7,12 +7,7 @@ import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPa const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g; -const syntaxHighlight = ( - content?: string | null, - isVisible?: boolean, - isImport?: boolean, - invalidReferences?: Set -) => { +const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => { if (isImport && !content) return "IMPORTED"; if (content === "") return "EMPTY"; if (!content) return "EMPTY"; @@ -23,18 +18,10 @@ const syntaxHighlight = ( const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}"); if (isInterpolationSyntax) { skipNext = true; - const referenceContent = el.slice(2, -1); - const isInvalid = invalidReferences?.has(referenceContent) ?? false; - return ( - + ${ - - {referenceContent} - + {el.slice(2, -1)} } ); @@ -62,7 +49,6 @@ type Props = TextareaHTMLAttributes & { isDisabled?: boolean; containerClassName?: string; canEditButNotView?: boolean; - invalidReferences?: Set; }; const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all"; @@ -80,7 +66,6 @@ export const SecretInput = forwardRef( isReadOnly, onFocus, canEditButNotView, - invalidReferences, ...props }, ref @@ -99,8 +84,7 @@ export const SecretInput = forwardRef( {syntaxHighlight( value, isVisible || (isSecretFocused && !valueAlwaysHidden), - isImport, - invalidReferences + isImport )}