From 1e857c04b72890d981221971a1a3cb055391db8c Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Tue, 4 Nov 2025 14:40:33 -0300 Subject: [PATCH] feat: add segment click handling in SecretInput and InfisicalSecretInput components --- .../InfisicalSecretInput.tsx | 37 ++++++++ .../components/v2/SecretInput/SecretInput.tsx | 94 +++++++++++++++++-- .../SecretDashboardPage.tsx | 21 ++++- .../SecretDashboardPage/route.tsx | 3 +- 4 files changed, 143 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index 9c4dc0b83..07d07dd4d 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -3,6 +3,7 @@ import { faFolder, faKey, faLayerGroup, faSearch } from "@fortawesome/free-solid import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; +import { ROUTE_PATHS } from "@app/const/routes"; import { useProject } from "@app/context"; import { useDebounce, useToggle } from "@app/hooks"; import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api"; @@ -307,6 +308,41 @@ export const InfisicalSecretInput = forwardRef( } }, []); + const handleClickSegment = useCallback( + (segment: string, allSegments: string[]) => { + // Single segment: search in current environment and path + if (allSegments.length === 1) { + const currentEnv = propEnvironment || ""; + const currentPath = propSecretPath || "/"; + const encodedPath = encodeURIComponent(currentPath); + const url = ROUTE_PATHS.SecretManager.SecretDashboardPage.path + .replace("$projectId", projectId) + .replace("$envSlug", currentEnv); + window.open( + `${url}?secretPath=${encodedPath}&search=${encodeURIComponent(segment)}&filterBy=secret`, + "_blank", + "noopener,noreferrer" + ); + return; + } + + // Multiple segments: first is env, last is secret, middle are folders + const environmentSlug = allSegments[0]; + const secretName = allSegments[allSegments.length - 1]; + const folderPath = allSegments.length > 2 ? `/${allSegments.slice(1, -1).join("/")}` : "/"; + const encodedPath = encodeURIComponent(folderPath); + const url = ROUTE_PATHS.SecretManager.SecretDashboardPage.path + .replace("$projectId", projectId) + .replace("$envSlug", environmentSlug); + window.open( + `${url}?secretPath=${encodedPath}&search=${encodeURIComponent(secretName)}&filterBy=secret`, + "_blank", + "noopener,noreferrer" + ); + }, + [projectId, propEnvironment, propSecretPath] + ); + return ( @@ -329,6 +365,7 @@ export const InfisicalSecretInput = forwardRef( }} onChange={(e) => onChange?.(e.target.value)} containerClassName={containerClassName} + onClickSegment={handleClickSegment} /> void, + hoveredPart?: string, + isCmdOrCtrlPressed?: boolean, + onClickSegment?: (segment: string, allSegments: string[]) => void ) => { if (isLoadingValue) return HIDDEN_SECRET_VALUE; if (isErrorLoadingValue) @@ -27,10 +31,51 @@ const syntaxHighlight = ( const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}"); if (isInterpolationSyntax) { skipNext = true; + const part = el; + const innerContent = el.slice(2, -1); // Remove ${ and } + const parts = innerContent.split("."); + return ( - + ${ - {el.slice(2, -1)} + {parts.map((segment, segmentIndex) => { + const segmentKey = `${part}-segment-${segmentIndex}`; + const isHovered = hoveredPart === segmentKey; + const shouldShowHoverStyle = isHovered && isCmdOrCtrlPressed; + + return ( + + onHoverPart?.(segmentKey)} + onMouseLeave={() => onHoverPart?.("")} + onClick={(e) => { + if (isCmdOrCtrlPressed) { + e.preventDefault(); + e.stopPropagation(); + onClickSegment?.(segment, parts); + } + }} + onKeyDown={(e) => { + if (isCmdOrCtrlPressed && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + e.stopPropagation(); + onClickSegment?.(segment, parts); + } + }} + > + {segment} + + {segmentIndex < parts.length - 1 && ( + . + )} + + ); + })} } ); @@ -60,6 +105,7 @@ type Props = TextareaHTMLAttributes & { canEditButNotView?: boolean; isLoadingValue?: boolean; isErrorLoadingValue?: boolean; + onClickSegment?: (segment: string, allSegments: string[]) => void; }; const commonClassName = "font-mono text-sm caret-white border-none outline-hidden w-full break-all"; @@ -79,11 +125,36 @@ export const SecretInput = forwardRef( canEditButNotView, isLoadingValue, isErrorLoadingValue, + onClickSegment, ...props }, ref ) => { const [isSecretFocused, setIsSecretFocused] = useToggle(); + const [hoveredPart, setHoveredPart] = useState(); + const [isCmdOrCtrlPressed, setIsCmdOrCtrlPressed] = useState(false); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.metaKey || e.ctrlKey) { + setIsCmdOrCtrlPressed(true); + } + }; + + const handleKeyUp = (e: KeyboardEvent) => { + if (!e.metaKey && !e.ctrlKey) { + setIsCmdOrCtrlPressed(false); + } + }; + + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }, []); return (
( style={{ maxHeight: `${21 * 7}px` }} >
-
+          
             
               
                 {syntaxHighlight(
@@ -99,7 +170,13 @@ export const SecretInput = forwardRef(
                   isVisible || (isSecretFocused && !valueAlwaysHidden),
                   isImport,
                   isLoadingValue,
-                  isErrorLoadingValue
+                  isErrorLoadingValue,
+                  (part) => {
+                    setHoveredPart(part);
+                  },
+                  hoveredPart,
+                  isCmdOrCtrlPressed,
+                  onClickSegment
                 )}
               
             
@@ -128,6 +205,9 @@ export const SecretInput = forwardRef(
               onBlur?.(evt);
               setIsSecretFocused.off();
             }}
+            onMouseLeave={() => {
+              setHoveredPart(undefined);
+            }}
             value={value || ""}
             {...props}
             readOnly={isReadOnly || isLoadingValue || isErrorLoadingValue}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
index a2ee814f8..74ad462e4 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
@@ -534,7 +534,7 @@ const Page = () => {
   }, [secretPath]);
 
   useEffect(() => {
-    if (!routerQueryParams.search && !routerQueryParams.tags) return;
+    if (!routerQueryParams.search && !routerQueryParams.tags && !routerQueryParams.filterBy) return;
 
     const queryTags = routerQueryParams.tags
       ? (routerQueryParams.tags as string).split(",").filter((tag) => Boolean(tag.trim()))
@@ -544,21 +544,34 @@ const Page = () => {
       updatedTags[tag] = true;
     });
 
+    const filterBy = routerQueryParams.filterBy
+      ? (routerQueryParams.filterBy as string).split(",").filter(Boolean)
+      : [];
+
+    const includeFilter: Record = {
+      [RowType.Folder]: filterBy.includes("folder"),
+      [RowType.Import]: filterBy.includes("import"),
+      [RowType.DynamicSecret]: filterBy.includes("dynamic"),
+      [RowType.Secret]: filterBy.includes("secret"),
+      [RowType.SecretRotation]: filterBy.includes("rotation")
+    };
+
     setFilter((prev) => ({
       ...prev,
       ...defaultFilterState,
       searchFilter: (routerQueryParams.search as string) ?? "",
-      tags: updatedTags
+      tags: updatedTags,
+      include: includeFilter
     }));
     setDebouncedSearchFilter(routerQueryParams.search as string);
     // this is a temp workaround until we fully transition state to query params,
     navigate({
       search: (state) => {
-        const { search, tags: qTags, ...query } = state;
+        const { search, tags: qTags, filterBy: qFilterBy, ...query } = state;
         return query;
       }
     });
-  }, [routerQueryParams.search, routerQueryParams.tags]);
+  }, [routerQueryParams.search, routerQueryParams.tags, routerQueryParams.filterBy]);
 
   const selectedSecrets = useSelectedSecrets();
   const selectedSecretActions = useSelectedSecretActions();
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx
index 0700b3322..da48a04f0 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx
@@ -11,6 +11,7 @@ const SecretDashboardPageQueryParamsSchema = z.object({
   secretPath: z.string().catch("/"),
   search: z.string().catch(""),
   tags: z.string().catch(""),
+  filterBy: z.string().catch(""),
   connectionId: z.string().optional(),
   connectionName: z.string().optional()
 });
@@ -20,7 +21,7 @@ export const Route = createFileRoute(
   component: SecretDashboardPage,
   validateSearch: zodValidator(SecretDashboardPageQueryParamsSchema),
   search: {
-    middlewares: [stripSearchParams({ secretPath: "/", search: "", tags: "" })]
+    middlewares: [stripSearchParams({ secretPath: "/", search: "", tags: "", filterBy: "" })]
   },
   beforeLoad: ({ context, params, search }) => {
     const secretPathSegments = search.secretPath.split("/").filter(Boolean);