mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +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 { MongoAbility, MongoQuery } from "@casl/ability";
|
||||
import {
|
||||
faArrowsToDot,
|
||||
faArrowsUpDownLeftRight,
|
||||
faArrowUpRightFromSquare,
|
||||
faDownLeftAndUpRightToCenter,
|
||||
faUpLong,
|
||||
faUpRightAndDownLeftFromCenter,
|
||||
faWindowRestore
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -24,9 +25,9 @@ import {
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
|
||||
import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput";
|
||||
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
|
||||
import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components";
|
||||
import { BasePermissionEdge } from "./edges";
|
||||
@@ -46,12 +47,13 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
const [selectedPath, setSelectedPath] = useState<string>("/");
|
||||
const accessTreeData = useAccessTree(permissions, selectedPath);
|
||||
const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
|
||||
const [initialRender, setInitialRender] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedPath("/");
|
||||
}, [environment]);
|
||||
|
||||
const { getViewport, setCenter } = useReactFlow();
|
||||
const { getViewport, setCenter, fitView } = useReactFlow();
|
||||
|
||||
const goToRootNode = useCallback(() => {
|
||||
const roleNode = nodes.find((node) => node.type === "role");
|
||||
@@ -76,10 +78,17 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
goToRootNode();
|
||||
}, 1);
|
||||
}, [nodes, edges, getViewport()]);
|
||||
setInitialRender(true);
|
||||
}, [selectedPath, environment]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialRender) {
|
||||
setTimeout(() => {
|
||||
goToRootNode();
|
||||
setInitialRender(false);
|
||||
}, 500);
|
||||
}
|
||||
}, [nodes, edges, getViewport(), initialRender]);
|
||||
|
||||
const handleToggleModalView = () =>
|
||||
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
||||
@@ -156,6 +165,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
edgesReconnectable={false}
|
||||
nodesConnectable={false}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
minZoom={0.001}
|
||||
proOptions={{
|
||||
hideAttribution: false // we need pro license if we want to hide
|
||||
}}
|
||||
@@ -167,12 +177,14 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
)}
|
||||
{viewMode !== ViewMode.Docked && (
|
||||
<Panel position="top-right" className="flex gap-1.5">
|
||||
<SecretPathInput
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={environment}
|
||||
value={selectedPath}
|
||||
onChange={setSelectedPath}
|
||||
/>
|
||||
{viewMode !== ViewMode.Undocked && (
|
||||
<AccessTreeSecretPathInput
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={environment}
|
||||
value={selectedPath}
|
||||
onChange={setSelectedPath}
|
||||
/>
|
||||
)}
|
||||
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
|
||||
<IconButton
|
||||
className="mr-1 rounded"
|
||||
@@ -211,7 +223,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
)}
|
||||
{viewMode === ViewMode.Docked && (
|
||||
<Panel position="top-right" className="flex gap-1.5">
|
||||
<SecretPathInput
|
||||
<AccessTreeSecretPathInput
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={environment}
|
||||
value={selectedPath}
|
||||
@@ -221,10 +233,15 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
)}
|
||||
<PermissionSimulation {...accessTreeData} />
|
||||
<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}>
|
||||
<Tooltip position="right" content="Go to Root Folder">
|
||||
<FontAwesomeIcon icon={faUpLong} />
|
||||
<Tooltip position="right" content="Go to root folder">
|
||||
<FontAwesomeIcon icon={faArrowsToDot} />
|
||||
</Tooltip>
|
||||
</ControlButton>
|
||||
</Controls>
|
||||
|
||||
@@ -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 { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { Button, Tooltip } from "@app/components/v2";
|
||||
|
||||
import { createShowMoreNode } from "../utils/createShowMoreNode";
|
||||
|
||||
export const ShowMoreButtonNode = ({
|
||||
data: { onClick, remaining }
|
||||
}: NodeProps & { data: ReturnType<typeof createShowMoreNode>["data"] }) => {
|
||||
const tooltipText = `${remaining} ${remaining === 1 ? "folder is" : "folders are"} hidden. Click to show ${remaining > 10 ? "10 more" : ""}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} style={{ visibility: "hidden" }} />
|
||||
|
||||
<Tooltip position="right" content="Click to show 10 more folders">
|
||||
<button
|
||||
type="button"
|
||||
<Tooltip position="right" content={tooltipText}>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
size="xs"
|
||||
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"
|
||||
aria-label={`Show ${remaining} more folders`}
|
||||
rightIcon={<FontAwesomeIcon icon={faChevronRight} className="ml-1" />}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faFolderClosed} className="h-3.5 w-3.5 text-primary/70" />
|
||||
<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>
|
||||
Show More
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,14 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => {
|
||||
const showMoreNodes = nodes.filter((node) => node.type === "showMoreButton");
|
||||
|
||||
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 })
|
||||
.setDefaultEdgeLabel(() => ({}))
|
||||
@@ -33,27 +41,49 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => {
|
||||
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 parentId = node.data.parentId as string;
|
||||
const { isStart } = node.data;
|
||||
|
||||
const parentNode = nodeMap[parentId] || positionedNodes[0];
|
||||
const parentX = parentNode.position.x;
|
||||
const parentY = parentNode.position.y;
|
||||
const lastChildNode = findLastChildNode(parentId);
|
||||
|
||||
const parentWidth = parentNode.width || 150;
|
||||
const buttonWidth = node.width || 100;
|
||||
const referenceNode = lastChildNode || parentNode;
|
||||
|
||||
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 {
|
||||
...node,
|
||||
position: {
|
||||
x: buttonX,
|
||||
y: parentY
|
||||
y: buttonY
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: [...positionedNodes, ...positionedShowMoreNodes],
|
||||
edges
|
||||
|
||||
@@ -141,7 +141,7 @@ export const SecretPathInput = ({
|
||||
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) => (
|
||||
<div
|
||||
tabIndex={0}
|
||||
|
||||
Reference in New Issue
Block a user