mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 17:00:56 +00:00
Merge remote-tracking branch 'origin/main' into feat/pki-ENG-3666
This commit is contained in:
@@ -29,17 +29,54 @@ const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/;
|
||||
export const hasSecretReference = (value: string | undefined) =>
|
||||
value ? INTERPOLATION_SYNTAX_REG.test(value) : false;
|
||||
|
||||
const createNodeId = (node: TSecretReferenceTraceNode): string =>
|
||||
`${node.environment}:${node.secretPath}:${node.key}`;
|
||||
|
||||
const isCircularReference = (
|
||||
node: TSecretReferenceTraceNode,
|
||||
visitedPath: Set<string>
|
||||
): boolean => {
|
||||
const nodeId = createNodeId(node);
|
||||
return visitedPath.has(nodeId);
|
||||
};
|
||||
|
||||
const hasCircularReferences = (
|
||||
node: TSecretReferenceTraceNode,
|
||||
visitedPath: Set<string> = new Set()
|
||||
): boolean => {
|
||||
const nodeId = createNodeId(node);
|
||||
|
||||
if (visitedPath.has(nodeId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const newVisitedPath = new Set([...visitedPath, nodeId]);
|
||||
|
||||
return node.children.some((child) => hasCircularReferences(child, newVisitedPath));
|
||||
};
|
||||
|
||||
export const SecretReferenceNode = ({
|
||||
node,
|
||||
isRoot,
|
||||
secretKey
|
||||
secretKey,
|
||||
visitedPath = new Set()
|
||||
}: {
|
||||
node: TSecretReferenceTraceNode;
|
||||
isRoot?: boolean;
|
||||
secretKey?: string;
|
||||
visitedPath?: Set<string>;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasChildren = node.children.length > 0;
|
||||
|
||||
const nodeId = createNodeId(node);
|
||||
const isCircular = !isRoot && isCircularReference(node, visitedPath);
|
||||
|
||||
const newVisitedPath = isCircular ? visitedPath : new Set([...visitedPath, nodeId]);
|
||||
|
||||
const safeChildren = isCircular
|
||||
? []
|
||||
: node.children.filter((child) => !isCircularReference(child, newVisitedPath));
|
||||
const hasChildren = safeChildren.length > 0;
|
||||
|
||||
return (
|
||||
<li>
|
||||
@@ -77,8 +114,12 @@ export const SecretReferenceNode = ({
|
||||
<Collapsible.Content className={twMerge("mt-4", style.collapsibleContent)}>
|
||||
{hasChildren && (
|
||||
<ul>
|
||||
{node.children.map((el, index) => (
|
||||
<SecretReferenceNode node={el} key={`${el.key}-${index + 1}`} />
|
||||
{safeChildren.map((el, index) => (
|
||||
<SecretReferenceNode
|
||||
node={el}
|
||||
key={`${el.key}-${index + 1}`}
|
||||
visitedPath={newVisitedPath}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
@@ -102,6 +143,9 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
|
||||
const tree = data?.tree;
|
||||
const secretValue = data?.value;
|
||||
|
||||
// Check if the tree contains circular references
|
||||
const hasCirculars = tree ? hasCircularReferences(tree) : false;
|
||||
|
||||
useEffect(() => {
|
||||
if (error instanceof AxiosError) {
|
||||
const err = error?.response?.data as TApiErrors;
|
||||
@@ -130,9 +174,25 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
|
||||
);
|
||||
}
|
||||
|
||||
if (tree?.children?.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<span className="text-mineshaft-400">This secret does not contain references</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControl label="Expanded value">
|
||||
<FormControl
|
||||
label="Expanded value"
|
||||
tooltipText={
|
||||
hasCirculars
|
||||
? "This secret contains circular references. Value shown is resolved once, with circular paths truncated in the reference tree below."
|
||||
: undefined
|
||||
}
|
||||
tooltipClassName="max-w-md break-words"
|
||||
>
|
||||
<SecretInput
|
||||
key="value-overriden"
|
||||
isReadOnly
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -55,6 +55,8 @@ type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "val
|
||||
secretPath?: string;
|
||||
environment?: string;
|
||||
containerClassName?: string;
|
||||
isLoadingValue?: boolean;
|
||||
isErrorLoadingValue?: boolean;
|
||||
};
|
||||
|
||||
type ReferenceItem = {
|
||||
@@ -175,13 +177,29 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
type: ReferenceType.SECRET
|
||||
});
|
||||
});
|
||||
|
||||
if (suggestionsArr.length === 0 && suggestionSource.predicate.trim()) {
|
||||
suggestionsArr.push({
|
||||
label: "No matches found",
|
||||
slug: "__no_match__",
|
||||
type: ReferenceType.SECRET
|
||||
});
|
||||
}
|
||||
|
||||
return suggestionsArr;
|
||||
}, [secrets, folders, currentProject?.environments, isPopupOpen, suggestionSource.value]);
|
||||
}, [
|
||||
secrets,
|
||||
folders,
|
||||
currentProject?.environments,
|
||||
isPopupOpen,
|
||||
suggestionSource.value,
|
||||
suggestionSource.predicate
|
||||
]);
|
||||
|
||||
const handleSuggestionSelect = (selectIndex?: number) => {
|
||||
const selectedSuggestion =
|
||||
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
|
||||
if (!selectedSuggestion) {
|
||||
if (!selectedSuggestion || selectedSuggestion.slug === "__no_match__") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,21 +244,40 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
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();
|
||||
@@ -279,11 +316,16 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
ref={handleRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={value}
|
||||
onFocus={() => setIsFocused.on()}
|
||||
onFocus={(evt) => {
|
||||
if (props.onFocus) props.onFocus(evt);
|
||||
setIsFocused.on();
|
||||
}}
|
||||
onBlur={(evt) => {
|
||||
// should not on blur when its mouse down selecting a item from suggestion
|
||||
if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item"))
|
||||
setIsFocused.off();
|
||||
|
||||
if (props.onBlur) props.onBlur(evt);
|
||||
}}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
containerClassName={containerClassName}
|
||||
@@ -304,7 +346,12 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
{suggestions.map((item, i) => {
|
||||
let entryIcon;
|
||||
let subText;
|
||||
if (item.type === ReferenceType.SECRET) {
|
||||
const isNoMatchMessage = item.slug === "__no_match__";
|
||||
|
||||
if (isNoMatchMessage) {
|
||||
entryIcon = <FontAwesomeIcon icon={faSearch} className="text-gray-400" />;
|
||||
subText = "No results";
|
||||
} else if (item.type === ReferenceType.SECRET) {
|
||||
entryIcon = <FontAwesomeIcon icon={faKey} className="text-bunker-300" />;
|
||||
subText = "Secret";
|
||||
} else if (item.type === ReferenceType.ENVIRONMENT) {
|
||||
@@ -315,10 +362,28 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
subText = "Folder";
|
||||
}
|
||||
|
||||
return (
|
||||
return isNoMatchMessage ? (
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
role="status"
|
||||
aria-label="no-match-message"
|
||||
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
|
||||
key={`secret-reference-secret-${i + 1}`}
|
||||
>
|
||||
<div className="text-md relative flex w-full cursor-default select-none items-center justify-between px-2 py-2 opacity-75 outline-none transition-all">
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<div className="mt-1 flex items-center">{entryIcon}</div>
|
||||
<div className="text-md w-10/12 truncate text-left">
|
||||
<span className="text-gray-400">{item.label}</span>
|
||||
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
|
||||
{subText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSuggestionSelect(i);
|
||||
}}
|
||||
@@ -330,8 +395,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
handleSuggestionSelect(i);
|
||||
}}
|
||||
onMouseEnter={() => setHighlightedIndex(i)}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
|
||||
className="flex w-full items-center justify-between border-none border-mineshaft-600 bg-transparent p-0 text-left"
|
||||
key={`secret-reference-secret-${i + 1}`}
|
||||
>
|
||||
<div
|
||||
@@ -342,14 +406,14 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<div className="mt-1 flex items-center">{entryIcon}</div>
|
||||
<div className="text-md w-10/12 truncate text-left">
|
||||
{item.label}
|
||||
<span>{item.label}</span>
|
||||
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
|
||||
{subText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,16 @@ 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) => {
|
||||
const syntaxHighlight = (
|
||||
content?: string | null,
|
||||
isVisible?: boolean,
|
||||
isImport?: boolean,
|
||||
isLoadingValue?: boolean,
|
||||
isErrorLoadingValue?: boolean
|
||||
) => {
|
||||
if (isLoadingValue) return HIDDEN_SECRET_VALUE;
|
||||
if (isErrorLoadingValue)
|
||||
return <span className="ph-no-capture text-red/75">Error loading secret value.</span>;
|
||||
if (isImport && !content) return "IMPORTED";
|
||||
if (content === "") return "EMPTY";
|
||||
if (!content) return "EMPTY";
|
||||
@@ -20,7 +29,8 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
|
||||
skipNext = true;
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
|
||||
${<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
||||
${
|
||||
<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
@@ -48,6 +58,8 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
isDisabled?: boolean;
|
||||
containerClassName?: string;
|
||||
canEditButNotView?: boolean;
|
||||
isLoadingValue?: boolean;
|
||||
isErrorLoadingValue?: boolean;
|
||||
};
|
||||
|
||||
const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
|
||||
@@ -65,6 +77,8 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
isReadOnly,
|
||||
onFocus,
|
||||
canEditButNotView,
|
||||
isLoadingValue,
|
||||
isErrorLoadingValue,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -83,7 +97,9 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
{syntaxHighlight(
|
||||
value,
|
||||
isVisible || (isSecretFocused && !valueAlwaysHidden),
|
||||
isImport
|
||||
isImport,
|
||||
isLoadingValue,
|
||||
isErrorLoadingValue
|
||||
)}
|
||||
</span>
|
||||
</code>
|
||||
@@ -114,7 +130,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
}}
|
||||
value={value || ""}
|
||||
{...props}
|
||||
readOnly={isReadOnly}
|
||||
readOnly={isReadOnly || isLoadingValue || isErrorLoadingValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user