mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #4804 from Infisical/feature/secret-reference-shortcut
feature: secret reference shortcut
This commit is contained in:
@@ -17,6 +17,10 @@ For example, if secret A references values from secrets B and C located in diffe
|
||||
|
||||
This is an important security consideration when planning your secret access strategy, especially when working with cross-environment or cross-folder references.
|
||||
|
||||
<Tip>
|
||||
You can hold the `Cmd` (Mac) or `Ctrl` (Windows/Linux) key and click the secret reference to be redirected to it.
|
||||
</Tip>
|
||||
|
||||
### Syntax
|
||||
|
||||
When defining a secret reference, interpolation syntax is used to define references to secrets in other environments and [folders](./folder).
|
||||
|
||||
@@ -2,10 +2,15 @@ import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useSt
|
||||
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";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { useProject } from "@app/context";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useProject, useProjectPermission } from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDebounce, useToggle } from "@app/hooks";
|
||||
import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
|
||||
import { SecretInput } from "../SecretInput";
|
||||
|
||||
@@ -80,6 +85,8 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
) => {
|
||||
const { currentProject } = useProject();
|
||||
const projectId = currentProject?.id || "";
|
||||
const navigate = useNavigate({ from: ROUTE_PATHS.SecretManager.SecretDashboardPage.path });
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const [debouncedValue] = useDebounce(value, 100);
|
||||
|
||||
@@ -307,6 +314,120 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClickSegment = useCallback(
|
||||
(segment: string, allSegments: string[]) => {
|
||||
if (!projectId) {
|
||||
createNotification({
|
||||
text: "Project ID is not set",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (allSegments.length === 0) {
|
||||
createNotification({
|
||||
text: "Invalid secret reference",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (allSegments.length === 1) {
|
||||
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
environment: propEnvironment ?? "*",
|
||||
secretPath: propSecretPath ?? "/",
|
||||
secretName: segment,
|
||||
secretTags: ["*"]
|
||||
}
|
||||
);
|
||||
|
||||
if (!canReadSecretValue) {
|
||||
createNotification({
|
||||
text: "You do not have permission to access this secret",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
search: segment,
|
||||
filterBy: "secret",
|
||||
tags: ""
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const environmentSlug = allSegments[0];
|
||||
const secretName = allSegments[allSegments.length - 1];
|
||||
let folderPath = "/";
|
||||
|
||||
if (allSegments.length > 2) {
|
||||
const pathSegments = allSegments.slice(1, -1);
|
||||
for (let i = 0; i < pathSegments.length; i += 1) {
|
||||
if (!pathSegments[i]) {
|
||||
createNotification({
|
||||
text: "Invalid secret reference",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const pathSegment = pathSegments[i];
|
||||
folderPath += `${pathSegment}`;
|
||||
if (pathSegment === segment) {
|
||||
folderPath += "/";
|
||||
break;
|
||||
}
|
||||
folderPath += "/";
|
||||
}
|
||||
}
|
||||
|
||||
// Only validate secret permission, users can always view environments and folders
|
||||
if (segment === secretName) {
|
||||
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
environment: environmentSlug,
|
||||
secretPath: folderPath,
|
||||
secretName,
|
||||
secretTags: ["*"]
|
||||
}
|
||||
);
|
||||
|
||||
if (!canReadSecretValue) {
|
||||
createNotification({
|
||||
text: "You do not have permission to access this secret",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
navigate({
|
||||
to: ROUTE_PATHS.SecretManager.SecretDashboardPage.path,
|
||||
params: {
|
||||
projectId,
|
||||
envSlug: environmentSlug
|
||||
},
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
secretPath: segment === environmentSlug ? "/" : folderPath,
|
||||
search: segment === secretName ? secretName : prev.search,
|
||||
filterBy: segment === secretName ? "secret" : prev.filterBy,
|
||||
tags: ""
|
||||
})
|
||||
});
|
||||
},
|
||||
[navigate, projectId, permission, propEnvironment, propSecretPath]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover.Root open={isPopupOpen} onOpenChange={handlePopUpOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
@@ -329,6 +450,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
}}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
containerClassName={containerClassName}
|
||||
onClickSegment={handleClickSegment}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import { forwardRef, TextareaHTMLAttributes } from "react";
|
||||
import { forwardRef, TextareaHTMLAttributes, useEffect, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useToggle } from "@app/hooks";
|
||||
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 = (
|
||||
content?: string | null,
|
||||
@@ -13,6 +13,10 @@ const syntaxHighlight = (
|
||||
isImport?: boolean,
|
||||
isLoadingValue?: boolean,
|
||||
isErrorLoadingValue?: boolean,
|
||||
onHoverPart?: (part: string) => void,
|
||||
hoveredPart?: string,
|
||||
isCmdOrCtrlPressed?: boolean,
|
||||
onClickSegment?: (segment: string, allSegments: string[]) => void,
|
||||
placeholder?: string
|
||||
) => {
|
||||
if (isLoadingValue) return HIDDEN_SECRET_VALUE;
|
||||
@@ -29,10 +33,57 @@ 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 (
|
||||
<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}`}>
|
||||
${
|
||||
<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?.("")}
|
||||
onMouseDown={(e) => {
|
||||
if (isCmdOrCtrlPressed) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isCmdOrCtrlPressed) {
|
||||
e.preventDefault();
|
||||
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>
|
||||
);
|
||||
})}
|
||||
}
|
||||
</span>
|
||||
);
|
||||
@@ -62,6 +113,7 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
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";
|
||||
@@ -81,12 +133,43 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
canEditButNotView,
|
||||
isLoadingValue,
|
||||
isErrorLoadingValue,
|
||||
onClickSegment,
|
||||
placeholder,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsCmdOrCtrlPressed(false);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
window.addEventListener("blur", handleBlur);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -94,7 +177,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
style={{ maxHeight: `${21 * 7}px` }}
|
||||
>
|
||||
<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}`}>
|
||||
<span
|
||||
className={twMerge(
|
||||
@@ -108,6 +191,12 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
isImport,
|
||||
isLoadingValue,
|
||||
isErrorLoadingValue,
|
||||
(part) => {
|
||||
setHoveredPart(part);
|
||||
},
|
||||
hoveredPart,
|
||||
isCmdOrCtrlPressed,
|
||||
onClickSegment,
|
||||
placeholder
|
||||
)}
|
||||
</span>
|
||||
@@ -138,6 +227,9 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
onBlur?.(evt);
|
||||
setIsSecretFocused.off();
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredPart(undefined);
|
||||
}}
|
||||
value={value || ""}
|
||||
{...props}
|
||||
readOnly={isReadOnly || isLoadingValue || isErrorLoadingValue}
|
||||
|
||||
@@ -249,6 +249,21 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
tooltipText={
|
||||
<div>
|
||||
You can add references to other secrets using the format{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">
|
||||
${"{"}secret_name{"}"}
|
||||
</code>
|
||||
<br />
|
||||
<br />
|
||||
You can go to the referenced secret by holding the{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">Cmd</code> (Mac) or{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">Ctrl</code>{" "}
|
||||
(Windows/Linux) key and clicking on the secret name.
|
||||
</div>
|
||||
}
|
||||
tooltipClassName="max-w-md"
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
|
||||
@@ -219,22 +219,43 @@ const Page = () => {
|
||||
ProjectPermissionSub.Commits
|
||||
);
|
||||
|
||||
const defaultFilterState = {
|
||||
tags: {},
|
||||
searchFilter: (routerQueryParams.search as string) || "",
|
||||
// these should always be on by default for the UI, they will be disabled for the query below based off permissions
|
||||
include: {
|
||||
[RowType.Folder]: false,
|
||||
[RowType.Import]: false,
|
||||
[RowType.DynamicSecret]: false,
|
||||
[RowType.Secret]: false,
|
||||
[RowType.SecretRotation]: false
|
||||
}
|
||||
};
|
||||
const getFilterStateFromQueryParams = useCallback(() => {
|
||||
const filterByArray = routerQueryParams.filterBy
|
||||
? (routerQueryParams.filterBy as string).split(",").filter(Boolean)
|
||||
: [];
|
||||
|
||||
const includeFilters = {
|
||||
[RowType.Folder]: filterByArray.includes("folder") || false,
|
||||
[RowType.Import]: filterByArray.includes("import") || false,
|
||||
[RowType.DynamicSecret]: filterByArray.includes("dynamic") || false,
|
||||
[RowType.Secret]: filterByArray.includes("secret") || false,
|
||||
[RowType.SecretRotation]: filterByArray.includes("rotation") || false
|
||||
};
|
||||
|
||||
const tags = routerQueryParams.tags
|
||||
? routerQueryParams.tags.split(",").reduce(
|
||||
(acc, tag) => {
|
||||
const trimmedTag = tag.trim();
|
||||
if (trimmedTag) {
|
||||
acc[trimmedTag] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, boolean>
|
||||
)
|
||||
: {};
|
||||
|
||||
return {
|
||||
tags,
|
||||
searchFilter: (routerQueryParams.search as string) || "",
|
||||
include: includeFilters
|
||||
};
|
||||
}, [routerQueryParams.search, routerQueryParams.tags, routerQueryParams.filterBy]);
|
||||
|
||||
const defaultFilterState = getFilterStateFromQueryParams();
|
||||
|
||||
const [filter, setFilter] = useState<Filter>(defaultFilterState);
|
||||
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
|
||||
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
|
||||
|
||||
const createSecretPopUp = usePopUpState(PopUpNames.CreateSecretForm);
|
||||
const { togglePopUp } = usePopUpAction();
|
||||
@@ -475,34 +496,96 @@ const Page = () => {
|
||||
);
|
||||
|
||||
const handleTagToggle = useCallback(
|
||||
(tagSlug: string) =>
|
||||
(tagSlug: string) => {
|
||||
setFilter((state) => {
|
||||
const isTagPresent = Boolean(state.tags?.[tagSlug]);
|
||||
const newTagFilter = { ...state.tags };
|
||||
if (isTagPresent) delete newTagFilter[tagSlug];
|
||||
else newTagFilter[tagSlug] = true;
|
||||
|
||||
// Update URL to match filter state
|
||||
const tagsList = Object.keys(newTagFilter).filter((tag) => newTagFilter[tag]);
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
tags: tagsList.length > 0 ? tagsList.join(",") : ""
|
||||
})
|
||||
});
|
||||
|
||||
return { ...state, tags: newTagFilter };
|
||||
}),
|
||||
[]
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const handleToggleRowType = useCallback(
|
||||
(rowType: RowType) =>
|
||||
(rowType: RowType) => {
|
||||
setFilter((state) => {
|
||||
const newInclude = {
|
||||
...state.include,
|
||||
[rowType]: !state.include[rowType]
|
||||
};
|
||||
|
||||
// Update URL to match filter state
|
||||
const filterByList: string[] = [];
|
||||
if (newInclude[RowType.Folder]) filterByList.push("folder");
|
||||
if (newInclude[RowType.Import]) filterByList.push("import");
|
||||
if (newInclude[RowType.DynamicSecret]) filterByList.push("dynamic");
|
||||
if (newInclude[RowType.Secret]) filterByList.push("secret");
|
||||
if (newInclude[RowType.SecretRotation]) filterByList.push("rotation");
|
||||
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
filterBy: filterByList.length > 0 ? filterByList.join(",") : ""
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
include: {
|
||||
...state.include,
|
||||
[rowType]: !state.include[rowType]
|
||||
}
|
||||
include: newInclude
|
||||
};
|
||||
}),
|
||||
[]
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const handleClearFilters = useCallback(() => {
|
||||
setFilter({
|
||||
searchFilter: "",
|
||||
tags: {},
|
||||
include: {
|
||||
[RowType.Folder]: false,
|
||||
[RowType.Import]: false,
|
||||
[RowType.DynamicSecret]: false,
|
||||
[RowType.Secret]: false,
|
||||
[RowType.SecretRotation]: false
|
||||
}
|
||||
});
|
||||
setDebouncedSearchFilter("");
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
search: "",
|
||||
tags: "",
|
||||
filterBy: ""
|
||||
})
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(searchFilter: string) => setFilter((state) => ({ ...state, searchFilter })),
|
||||
[]
|
||||
(searchFilter: string) => {
|
||||
setFilter((state) => ({ ...state, searchFilter }));
|
||||
|
||||
// Update URL to match filter state
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
search: searchFilter || ""
|
||||
})
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const handleToggleVisibility = useCallback(() => setIsVisible((state) => !state), []);
|
||||
@@ -527,38 +610,10 @@ const Page = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// restore filters for path if set
|
||||
const restore = filterHistory.get(secretPath);
|
||||
setFilter(restore ?? defaultFilterState);
|
||||
setDebouncedSearchFilter(restore?.searchFilter ?? "");
|
||||
}, [secretPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!routerQueryParams.search && !routerQueryParams.tags) return;
|
||||
|
||||
const queryTags = routerQueryParams.tags
|
||||
? (routerQueryParams.tags as string).split(",").filter((tag) => Boolean(tag.trim()))
|
||||
: [];
|
||||
const updatedTags: Record<string, boolean> = {};
|
||||
queryTags.forEach((tag) => {
|
||||
updatedTags[tag] = true;
|
||||
});
|
||||
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
...defaultFilterState,
|
||||
searchFilter: (routerQueryParams.search as string) ?? "",
|
||||
tags: updatedTags
|
||||
}));
|
||||
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;
|
||||
return query;
|
||||
}
|
||||
});
|
||||
}, [routerQueryParams.search, routerQueryParams.tags]);
|
||||
const filterState = getFilterStateFromQueryParams();
|
||||
setFilter(filterState);
|
||||
setDebouncedSearchFilter(filterState.searchFilter);
|
||||
}, [getFilterStateFromQueryParams]);
|
||||
|
||||
const selectedSecrets = useSelectedSecrets();
|
||||
const selectedSecretActions = useSelectedSecretActions();
|
||||
@@ -597,13 +652,6 @@ const Page = () => {
|
||||
}
|
||||
|
||||
const handleResetFilter = () => {
|
||||
// store for breadcrumb nav to restore previously used filters
|
||||
setFilterHistory((prev) => {
|
||||
const curr = new Map(prev);
|
||||
curr.set(secretPath, filter);
|
||||
return curr;
|
||||
});
|
||||
|
||||
setFilter(defaultFilterState);
|
||||
setDebouncedSearchFilter("");
|
||||
};
|
||||
@@ -838,19 +886,7 @@ const Page = () => {
|
||||
isPITEnabled={isPITEnabled}
|
||||
hasPathPolicies={hasPathPolicies}
|
||||
onRequestAccess={(params) => handlePopUpOpen("requestAccess", params)}
|
||||
onClearFilters={() =>
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
tags: {},
|
||||
include: {
|
||||
secret: false,
|
||||
import: false,
|
||||
dynamic: false,
|
||||
rotation: false,
|
||||
folder: false
|
||||
}
|
||||
}))
|
||||
}
|
||||
onClearFilters={handleClearFilters}
|
||||
/>
|
||||
<div
|
||||
ref={tableRef}
|
||||
|
||||
@@ -197,6 +197,21 @@ export const CreateSecretForm = ({
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Value"
|
||||
tooltipText={
|
||||
<div>
|
||||
You can add references to other secrets using the format{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">
|
||||
${"{"}secret_name{"}"}
|
||||
</code>
|
||||
<br />
|
||||
<br />
|
||||
You can go to the referenced secret by holding the{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">Cmd</code> (Mac) or{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">Ctrl</code>{" "}
|
||||
(Windows/Linux) key and clicking on the secret name.
|
||||
</div>
|
||||
}
|
||||
tooltipClassName="max-w-md"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user