mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 17:27:40 +00:00
Search bar improvements and position show more on top of last folder of the row
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { MongoAbility, MongoQuery } from "@casl/ability";
|
import { MongoAbility, MongoQuery } from "@casl/ability";
|
||||||
import {
|
import {
|
||||||
|
faArrowsToDot,
|
||||||
|
faArrowsUpDownLeftRight,
|
||||||
faArrowUpRightFromSquare,
|
faArrowUpRightFromSquare,
|
||||||
faDownLeftAndUpRightToCenter,
|
faDownLeftAndUpRightToCenter,
|
||||||
faUpLong,
|
|
||||||
faUpRightAndDownLeftFromCenter,
|
faUpRightAndDownLeftFromCenter,
|
||||||
faWindowRestore
|
faWindowRestore
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -24,9 +25,9 @@ import {
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
|
||||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||||
|
|
||||||
|
import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput";
|
||||||
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
|
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
|
||||||
import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components";
|
import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components";
|
||||||
import { BasePermissionEdge } from "./edges";
|
import { BasePermissionEdge } from "./edges";
|
||||||
@@ -46,12 +47,13 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
const [selectedPath, setSelectedPath] = useState<string>("/");
|
const [selectedPath, setSelectedPath] = useState<string>("/");
|
||||||
const accessTreeData = useAccessTree(permissions, selectedPath);
|
const accessTreeData = useAccessTree(permissions, selectedPath);
|
||||||
const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
|
const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
|
||||||
|
const [initialRender, setInitialRender] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedPath("/");
|
setSelectedPath("/");
|
||||||
}, [environment]);
|
}, [environment]);
|
||||||
|
|
||||||
const { getViewport, setCenter } = useReactFlow();
|
const { getViewport, setCenter, fitView } = useReactFlow();
|
||||||
|
|
||||||
const goToRootNode = useCallback(() => {
|
const goToRootNode = useCallback(() => {
|
||||||
const roleNode = nodes.find((node) => node.type === "role");
|
const roleNode = nodes.find((node) => node.type === "role");
|
||||||
@@ -76,10 +78,17 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setInitialRender(true);
|
||||||
goToRootNode();
|
}, [selectedPath, environment]);
|
||||||
}, 1);
|
|
||||||
}, [nodes, edges, getViewport()]);
|
useEffect(() => {
|
||||||
|
if (initialRender) {
|
||||||
|
setTimeout(() => {
|
||||||
|
goToRootNode();
|
||||||
|
setInitialRender(false);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [nodes, edges, getViewport(), initialRender]);
|
||||||
|
|
||||||
const handleToggleModalView = () =>
|
const handleToggleModalView = () =>
|
||||||
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
||||||
@@ -156,6 +165,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
edgesReconnectable={false}
|
edgesReconnectable={false}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
connectionLineType={ConnectionLineType.SmoothStep}
|
connectionLineType={ConnectionLineType.SmoothStep}
|
||||||
|
minZoom={0.001}
|
||||||
proOptions={{
|
proOptions={{
|
||||||
hideAttribution: false // we need pro license if we want to hide
|
hideAttribution: false // we need pro license if we want to hide
|
||||||
}}
|
}}
|
||||||
@@ -167,12 +177,14 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
)}
|
)}
|
||||||
{viewMode !== ViewMode.Docked && (
|
{viewMode !== ViewMode.Docked && (
|
||||||
<Panel position="top-right" className="flex gap-1.5">
|
<Panel position="top-right" className="flex gap-1.5">
|
||||||
<SecretPathInput
|
{viewMode !== ViewMode.Undocked && (
|
||||||
placeholder="Provide a path, default is /"
|
<AccessTreeSecretPathInput
|
||||||
environment={environment}
|
placeholder="Provide a path, default is /"
|
||||||
value={selectedPath}
|
environment={environment}
|
||||||
onChange={setSelectedPath}
|
value={selectedPath}
|
||||||
/>
|
onChange={setSelectedPath}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
|
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
|
||||||
<IconButton
|
<IconButton
|
||||||
className="mr-1 rounded"
|
className="mr-1 rounded"
|
||||||
@@ -211,7 +223,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
)}
|
)}
|
||||||
{viewMode === ViewMode.Docked && (
|
{viewMode === ViewMode.Docked && (
|
||||||
<Panel position="top-right" className="flex gap-1.5">
|
<Panel position="top-right" className="flex gap-1.5">
|
||||||
<SecretPathInput
|
<AccessTreeSecretPathInput
|
||||||
placeholder="Provide a path, default is /"
|
placeholder="Provide a path, default is /"
|
||||||
environment={environment}
|
environment={environment}
|
||||||
value={selectedPath}
|
value={selectedPath}
|
||||||
@@ -221,10 +233,15 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
)}
|
)}
|
||||||
<PermissionSimulation {...accessTreeData} />
|
<PermissionSimulation {...accessTreeData} />
|
||||||
<Background color="#5d5f64" bgColor="#111419" variant={BackgroundVariant.Dots} />
|
<Background color="#5d5f64" bgColor="#111419" variant={BackgroundVariant.Dots} />
|
||||||
<Controls position="bottom-left" showFitView={false} showInteractive={false}>
|
<Controls position="bottom-left" showInteractive={false} showFitView={false}>
|
||||||
|
<ControlButton onClick={() => fitView({ duration: 800 })}>
|
||||||
|
<Tooltip position="right" content="Set view to fit all nodes">
|
||||||
|
<FontAwesomeIcon icon={faArrowsUpDownLeftRight} />
|
||||||
|
</Tooltip>
|
||||||
|
</ControlButton>
|
||||||
<ControlButton onClick={goToRootNode}>
|
<ControlButton onClick={goToRootNode}>
|
||||||
<Tooltip position="right" content="Go to Root Folder">
|
<Tooltip position="right" content="Go to root folder">
|
||||||
<FontAwesomeIcon icon={faUpLong} />
|
<FontAwesomeIcon icon={faArrowsToDot} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ControlButton>
|
</ControlButton>
|
||||||
</Controls>
|
</Controls>
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { faSearch, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Tooltip } from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
|
||||||
|
type AccessTreeSecretPathInputProps = {
|
||||||
|
placeholder: string;
|
||||||
|
environment: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (path: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AccessTreeSecretPathInput = ({
|
||||||
|
placeholder,
|
||||||
|
environment,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
}: AccessTreeSecretPathInputProps) => {
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setIsFocused(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsFocused(false);
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
const inputElement = inputRef.current?.querySelector("input");
|
||||||
|
if (inputElement) {
|
||||||
|
inputElement.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSearch = () => {
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
if (!isExpanded) {
|
||||||
|
setTimeout(focusInput, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={wrapperRef} className="relative">
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex items-center overflow-hidden rounded transition-all duration-300 ease-in-out",
|
||||||
|
isFocused ? "bg-mineshaft-800 shadow-md" : "bg-mineshaft-700",
|
||||||
|
isExpanded ? "w-64" : "h-10 w-10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<div
|
||||||
|
className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white"
|
||||||
|
onClick={toggleSearch}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
toggleSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTimes} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tooltip position="bottom" content="Search paths">
|
||||||
|
<div
|
||||||
|
className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white"
|
||||||
|
onClick={toggleSearch}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
toggleSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faSearch} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={inputRef}
|
||||||
|
className={twMerge(
|
||||||
|
"flex-1 transition-opacity duration-300",
|
||||||
|
isExpanded ? "opacity-100" : "hidden"
|
||||||
|
)}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
role="search"
|
||||||
|
>
|
||||||
|
<div className="custom-input-wrapper">
|
||||||
|
<SecretPathInput
|
||||||
|
placeholder={placeholder}
|
||||||
|
environment={environment}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,35 +1,30 @@
|
|||||||
import { faChevronRight, faFolderClosed } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||||
|
|
||||||
import { Tooltip } from "@app/components/v2";
|
import { Button, Tooltip } from "@app/components/v2";
|
||||||
|
|
||||||
import { createShowMoreNode } from "../utils/createShowMoreNode";
|
import { createShowMoreNode } from "../utils/createShowMoreNode";
|
||||||
|
|
||||||
export const ShowMoreButtonNode = ({
|
export const ShowMoreButtonNode = ({
|
||||||
data: { onClick, remaining }
|
data: { onClick, remaining }
|
||||||
}: NodeProps & { data: ReturnType<typeof createShowMoreNode>["data"] }) => {
|
}: NodeProps & { data: ReturnType<typeof createShowMoreNode>["data"] }) => {
|
||||||
|
const tooltipText = `${remaining} ${remaining === 1 ? "folder is" : "folders are"} hidden. Click to show ${remaining > 10 ? "10 more" : ""}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Handle type="target" position={Position.Top} style={{ visibility: "hidden" }} />
|
<Handle type="target" position={Position.Top} style={{ visibility: "hidden" }} />
|
||||||
|
|
||||||
<Tooltip position="right" content="Click to show 10 more folders">
|
<Tooltip position="right" content={tooltipText}>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="group relative flex items-center justify-between gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-800/90 px-4 py-3 text-sm font-medium text-mineshaft-200 shadow-sm transition-all duration-200 hover:border-primary/50 hover:bg-mineshaft-700 hover:text-white hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary/30"
|
rightIcon={<FontAwesomeIcon icon={faChevronRight} className="ml-1" />}
|
||||||
aria-label={`Show ${remaining} more folders`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
Show More
|
||||||
<FontAwesomeIcon icon={faFolderClosed} className="h-3.5 w-3.5 text-primary/70" />
|
</Button>
|
||||||
<span>
|
|
||||||
{remaining} hidden folder{remaining !== 1 ? "s" : ""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center rounded-full bg-mineshaft-700/80 p-1 group-hover:bg-primary/20">
|
|
||||||
<FontAwesomeIcon icon={faChevronRight} className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => {
|
|||||||
const showMoreNodes = nodes.filter((node) => node.type === "showMoreButton");
|
const showMoreNodes = nodes.filter((node) => node.type === "showMoreButton");
|
||||||
|
|
||||||
const nodeMap: Record<string, Node> = {};
|
const nodeMap: Record<string, Node> = {};
|
||||||
|
const childrenMap: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
if (!childrenMap[edge.source]) {
|
||||||
|
childrenMap[edge.source] = [];
|
||||||
|
}
|
||||||
|
childrenMap[edge.source].push(edge.target);
|
||||||
|
});
|
||||||
|
|
||||||
const dagre = new Dagre.graphlib.Graph({ directed: true })
|
const dagre = new Dagre.graphlib.Graph({ directed: true })
|
||||||
.setDefaultEdgeLabel(() => ({}))
|
.setDefaultEdgeLabel(() => ({}))
|
||||||
@@ -33,27 +41,49 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => {
|
|||||||
return positionedNode;
|
return positionedNode;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const findLastChildNode = (parentId: string): Node | undefined => {
|
||||||
|
const childrenIds = childrenMap[parentId] || [];
|
||||||
|
if (childrenIds.length === 0) return undefined;
|
||||||
|
|
||||||
|
const childNodes = childrenIds.map((id) => nodeMap[id]).filter(Boolean);
|
||||||
|
|
||||||
|
if (childNodes.length === 0) return undefined;
|
||||||
|
|
||||||
|
childNodes.sort((a, b) => {
|
||||||
|
if (a.position.y === b.position.y) {
|
||||||
|
return b.position.x - a.position.x;
|
||||||
|
}
|
||||||
|
return b.position.y - a.position.y;
|
||||||
|
});
|
||||||
|
|
||||||
|
return childNodes[0];
|
||||||
|
};
|
||||||
|
|
||||||
const positionedShowMoreNodes = showMoreNodes.map((node) => {
|
const positionedShowMoreNodes = showMoreNodes.map((node) => {
|
||||||
const parentId = node.data.parentId as string;
|
const parentId = node.data.parentId as string;
|
||||||
const { isStart } = node.data;
|
|
||||||
|
|
||||||
const parentNode = nodeMap[parentId] || positionedNodes[0];
|
const parentNode = nodeMap[parentId] || positionedNodes[0];
|
||||||
const parentX = parentNode.position.x;
|
const lastChildNode = findLastChildNode(parentId);
|
||||||
const parentY = parentNode.position.y;
|
|
||||||
|
|
||||||
const parentWidth = parentNode.width || 150;
|
const referenceNode = lastChildNode || parentNode;
|
||||||
const buttonWidth = node.width || 100;
|
|
||||||
|
|
||||||
const buttonX = isStart ? parentX - buttonWidth - 20 : parentX + parentWidth + 20;
|
const referenceX = referenceNode.position.x;
|
||||||
|
const referenceY = referenceNode.position.y;
|
||||||
|
|
||||||
|
const referenceWidth = referenceNode.width || 150;
|
||||||
|
|
||||||
|
const buttonX = referenceX + referenceWidth - 85;
|
||||||
|
const buttonY = referenceY - 25;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
position: {
|
position: {
|
||||||
x: buttonX,
|
x: buttonX,
|
||||||
y: parentY
|
y: buttonY
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: [...positionedNodes, ...positionedShowMoreNodes],
|
nodes: [...positionedNodes, ...positionedShowMoreNodes],
|
||||||
edges
|
edges
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const SecretPathInput = ({
|
|||||||
maxHeight: "var(--radix-select-content-available-height)"
|
maxHeight: "var(--radix-select-content-available-height)"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="h-full w-full flex-col items-center justify-center rounded-md text-white">
|
<div className="max-h-[25vh] w-full flex-col items-center justify-center overflow-y-scroll rounded-md text-white">
|
||||||
{suggestions.map((suggestion, i) => (
|
{suggestions.map((suggestion, i) => (
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|||||||
Reference in New Issue
Block a user