feat: add segment click handling in SecretInput and InfisicalSecretInput components

This commit is contained in:
Victor Santos
2025-11-04 14:40:33 -03:00
parent 188eff144f
commit 1e857c04b7
4 changed files with 143 additions and 12 deletions
@@ -3,6 +3,7 @@ import { faFolder, faKey, faLayerGroup, faSearch } from "@fortawesome/free-solid
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { ROUTE_PATHS } from "@app/const/routes";
import { useProject } from "@app/context"; import { useProject } from "@app/context";
import { useDebounce, useToggle } from "@app/hooks"; import { useDebounce, useToggle } from "@app/hooks";
import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api"; import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api";
@@ -307,6 +308,41 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
} }
}, []); }, []);
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 ( return (
<Popover.Root open={isPopupOpen} onOpenChange={handlePopUpOpen}> <Popover.Root open={isPopupOpen} onOpenChange={handlePopUpOpen}>
<Popover.Trigger asChild> <Popover.Trigger asChild>
@@ -329,6 +365,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
}} }}
onChange={(e) => onChange?.(e.target.value)} onChange={(e) => onChange?.(e.target.value)}
containerClassName={containerClassName} containerClassName={containerClassName}
onClickSegment={handleClickSegment}
/> />
</Popover.Trigger> </Popover.Trigger>
<Popover.Content <Popover.Content
@@ -1,18 +1,22 @@
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
import { forwardRef, TextareaHTMLAttributes } from "react"; import { forwardRef, TextareaHTMLAttributes, useEffect, useState } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem"; import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g; const REGEX = /(\${([a-zA-Z0-9-_. ]+)})/g;
const syntaxHighlight = ( const syntaxHighlight = (
content?: string | null, content?: string | null,
isVisible?: boolean, isVisible?: boolean,
isImport?: boolean, isImport?: boolean,
isLoadingValue?: boolean, isLoadingValue?: boolean,
isErrorLoadingValue?: boolean isErrorLoadingValue?: boolean,
onHoverPart?: (part: string) => void,
hoveredPart?: string,
isCmdOrCtrlPressed?: boolean,
onClickSegment?: (segment: string, allSegments: string[]) => void
) => { ) => {
if (isLoadingValue) return HIDDEN_SECRET_VALUE; if (isLoadingValue) return HIDDEN_SECRET_VALUE;
if (isErrorLoadingValue) if (isErrorLoadingValue)
@@ -27,10 +31,51 @@ const syntaxHighlight = (
const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}"); const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
if (isInterpolationSyntax) { if (isInterpolationSyntax) {
skipNext = true; skipNext = true;
const part = el;
const innerContent = el.slice(2, -1); // Remove ${ and }
const parts = innerContent.split(".");
return ( return (
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}> <span className="ph-no-capture relative z-10 text-yellow" key={`secret-value-${i + 1}`}>
&#36;&#123; &#36;&#123;
<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span> {parts.map((segment, segmentIndex) => {
const segmentKey = `${part}-segment-${segmentIndex}`;
const isHovered = hoveredPart === segmentKey;
const shouldShowHoverStyle = isHovered && isCmdOrCtrlPressed;
return (
<span key={segmentKey}>
<span
role="button"
tabIndex={isCmdOrCtrlPressed ? 0 : -1}
className={`ph-no-capture text-yellow-200/80 ${
isCmdOrCtrlPressed ? "pointer-events-auto" : "pointer-events-none"
} ${shouldShowHoverStyle ? "cursor-pointer underline decoration-yellow-400" : ""}`}
onMouseEnter={() => 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}
</span>
{segmentIndex < parts.length - 1 && (
<span className="ph-no-capture pointer-events-none text-yellow-200/80">.</span>
)}
</span>
);
})}
&#125; &#125;
</span> </span>
); );
@@ -60,6 +105,7 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
canEditButNotView?: boolean; canEditButNotView?: boolean;
isLoadingValue?: boolean; isLoadingValue?: boolean;
isErrorLoadingValue?: 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"; const commonClassName = "font-mono text-sm caret-white border-none outline-hidden w-full break-all";
@@ -79,11 +125,36 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
canEditButNotView, canEditButNotView,
isLoadingValue, isLoadingValue,
isErrorLoadingValue, isErrorLoadingValue,
onClickSegment,
...props ...props
}, },
ref ref
) => { ) => {
const [isSecretFocused, setIsSecretFocused] = useToggle(); const [isSecretFocused, setIsSecretFocused] = useToggle();
const [hoveredPart, setHoveredPart] = useState<string | undefined>();
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 ( return (
<div <div
@@ -91,7 +162,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
style={{ maxHeight: `${21 * 7}px` }} style={{ maxHeight: `${21 * 7}px` }}
> >
<div className="relative overflow-hidden"> <div className="relative overflow-hidden">
<pre aria-hidden className="m-0"> <pre aria-hidden className="pointer-events-none relative z-10 m-0">
<code className={`inline-block w-full ${commonClassName}`}> <code className={`inline-block w-full ${commonClassName}`}>
<span style={{ whiteSpace: "break-spaces" }}> <span style={{ whiteSpace: "break-spaces" }}>
{syntaxHighlight( {syntaxHighlight(
@@ -99,7 +170,13 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
isVisible || (isSecretFocused && !valueAlwaysHidden), isVisible || (isSecretFocused && !valueAlwaysHidden),
isImport, isImport,
isLoadingValue, isLoadingValue,
isErrorLoadingValue isErrorLoadingValue,
(part) => {
setHoveredPart(part);
},
hoveredPart,
isCmdOrCtrlPressed,
onClickSegment
)} )}
</span> </span>
</code> </code>
@@ -128,6 +205,9 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
onBlur?.(evt); onBlur?.(evt);
setIsSecretFocused.off(); setIsSecretFocused.off();
}} }}
onMouseLeave={() => {
setHoveredPart(undefined);
}}
value={value || ""} value={value || ""}
{...props} {...props}
readOnly={isReadOnly || isLoadingValue || isErrorLoadingValue} readOnly={isReadOnly || isLoadingValue || isErrorLoadingValue}
@@ -534,7 +534,7 @@ const Page = () => {
}, [secretPath]); }, [secretPath]);
useEffect(() => { useEffect(() => {
if (!routerQueryParams.search && !routerQueryParams.tags) return; if (!routerQueryParams.search && !routerQueryParams.tags && !routerQueryParams.filterBy) return;
const queryTags = routerQueryParams.tags const queryTags = routerQueryParams.tags
? (routerQueryParams.tags as string).split(",").filter((tag) => Boolean(tag.trim())) ? (routerQueryParams.tags as string).split(",").filter((tag) => Boolean(tag.trim()))
@@ -544,21 +544,34 @@ const Page = () => {
updatedTags[tag] = true; updatedTags[tag] = true;
}); });
const filterBy = routerQueryParams.filterBy
? (routerQueryParams.filterBy as string).split(",").filter(Boolean)
: [];
const includeFilter: Record<RowType, boolean> = {
[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) => ({ setFilter((prev) => ({
...prev, ...prev,
...defaultFilterState, ...defaultFilterState,
searchFilter: (routerQueryParams.search as string) ?? "", searchFilter: (routerQueryParams.search as string) ?? "",
tags: updatedTags tags: updatedTags,
include: includeFilter
})); }));
setDebouncedSearchFilter(routerQueryParams.search as string); setDebouncedSearchFilter(routerQueryParams.search as string);
// this is a temp workaround until we fully transition state to query params, // this is a temp workaround until we fully transition state to query params,
navigate({ navigate({
search: (state) => { search: (state) => {
const { search, tags: qTags, ...query } = state; const { search, tags: qTags, filterBy: qFilterBy, ...query } = state;
return query; return query;
} }
}); });
}, [routerQueryParams.search, routerQueryParams.tags]); }, [routerQueryParams.search, routerQueryParams.tags, routerQueryParams.filterBy]);
const selectedSecrets = useSelectedSecrets(); const selectedSecrets = useSelectedSecrets();
const selectedSecretActions = useSelectedSecretActions(); const selectedSecretActions = useSelectedSecretActions();
@@ -11,6 +11,7 @@ const SecretDashboardPageQueryParamsSchema = z.object({
secretPath: z.string().catch("/"), secretPath: z.string().catch("/"),
search: z.string().catch(""), search: z.string().catch(""),
tags: z.string().catch(""), tags: z.string().catch(""),
filterBy: z.string().catch(""),
connectionId: z.string().optional(), connectionId: z.string().optional(),
connectionName: z.string().optional() connectionName: z.string().optional()
}); });
@@ -20,7 +21,7 @@ export const Route = createFileRoute(
component: SecretDashboardPage, component: SecretDashboardPage,
validateSearch: zodValidator(SecretDashboardPageQueryParamsSchema), validateSearch: zodValidator(SecretDashboardPageQueryParamsSchema),
search: { search: {
middlewares: [stripSearchParams({ secretPath: "/", search: "", tags: "" })] middlewares: [stripSearchParams({ secretPath: "/", search: "", tags: "", filterBy: "" })]
}, },
beforeLoad: ({ context, params, search }) => { beforeLoad: ({ context, params, search }) => {
const secretPathSegments = search.secretPath.split("/").filter(Boolean); const secretPathSegments = search.secretPath.split("/").filter(Boolean);