mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 09:26:14 +00:00
Add horizontal limit, search bar and change icons of access tree component
This commit is contained in:
@@ -1,14 +1,11 @@
|
|||||||
import { TSecretFolders } from "@app/db/schemas";
|
import { TSecretFolders } from "@app/db/schemas";
|
||||||
import { InternalServerError } from "@app/lib/errors";
|
|
||||||
|
|
||||||
export const buildFolderPath = (
|
export const buildFolderPath = (
|
||||||
folder: TSecretFolders,
|
folder: TSecretFolders,
|
||||||
foldersMap: Record<string, TSecretFolders>,
|
foldersMap: Record<string, TSecretFolders>,
|
||||||
depth: number = 0
|
depth: number = 0
|
||||||
): string => {
|
): string => {
|
||||||
if (depth > 20) {
|
if (depth > 20) return;
|
||||||
throw new InternalServerError({ message: "Maximum folder depth of 20 exceeded" });
|
|
||||||
}
|
|
||||||
if (!folder.parentId) {
|
if (!folder.parentId) {
|
||||||
return depth === 0 ? "/" : "";
|
return depth === 0 ? "/" : "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { MongoAbility, MongoQuery } from "@casl/ability";
|
import { MongoAbility, MongoQuery } from "@casl/ability";
|
||||||
import {
|
import {
|
||||||
faArrowUpRightFromSquare,
|
faArrowUpRightFromSquare,
|
||||||
|
faDownLeftAndUpRightToCenter,
|
||||||
|
faUpLong,
|
||||||
faUpRightAndDownLeftFromCenter,
|
faUpRightAndDownLeftFromCenter,
|
||||||
faWindowRestore
|
faWindowRestore
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -10,6 +12,7 @@ import {
|
|||||||
Background,
|
Background,
|
||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
ConnectionLineType,
|
ConnectionLineType,
|
||||||
|
ControlButton,
|
||||||
Controls,
|
Controls,
|
||||||
Node,
|
Node,
|
||||||
NodeMouseHandler,
|
NodeMouseHandler,
|
||||||
@@ -21,8 +24,10 @@ 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 { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
|
||||||
import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components";
|
import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components";
|
||||||
import { BasePermissionEdge } from "./edges";
|
import { BasePermissionEdge } from "./edges";
|
||||||
import { useAccessTree } from "./hooks";
|
import { useAccessTree } from "./hooks";
|
||||||
@@ -35,13 +40,29 @@ export type AccessTreeProps = {
|
|||||||
|
|
||||||
const EdgeTypes = { base: BasePermissionEdge };
|
const EdgeTypes = { base: BasePermissionEdge };
|
||||||
|
|
||||||
const NodeTypes = { role: RoleNode, folder: FolderNode };
|
const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode };
|
||||||
|
|
||||||
const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||||
const accessTreeData = useAccessTree(permissions);
|
const [selectedPath, setSelectedPath] = useState<string>("/");
|
||||||
const { edges, nodes, isLoading, viewMode, setViewMode } = accessTreeData;
|
const accessTreeData = useAccessTree(permissions, selectedPath);
|
||||||
|
const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
|
||||||
|
|
||||||
const { fitView, getViewport, setCenter } = useReactFlow();
|
useEffect(() => {
|
||||||
|
setSelectedPath("/");
|
||||||
|
}, [environment]);
|
||||||
|
|
||||||
|
const { getViewport, setCenter } = useReactFlow();
|
||||||
|
|
||||||
|
const goToRootNode = useCallback(() => {
|
||||||
|
const roleNode = nodes.find((node) => node.type === "role");
|
||||||
|
if (roleNode) {
|
||||||
|
setCenter(
|
||||||
|
roleNode.position.x + (roleNode.width ? roleNode.width / 2 : 0),
|
||||||
|
roleNode.position.y + (roleNode.height ? roleNode.height / 2 : 0),
|
||||||
|
{ duration: 800, zoom: 1 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [nodes, setCenter]);
|
||||||
|
|
||||||
const onNodeClick: NodeMouseHandler<Node> = useCallback(
|
const onNodeClick: NodeMouseHandler<Node> = useCallback(
|
||||||
(_, node) => {
|
(_, node) => {
|
||||||
@@ -56,13 +77,9 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fitView({
|
goToRootNode();
|
||||||
padding: 0.2,
|
|
||||||
duration: 1000,
|
|
||||||
maxZoom: 1
|
|
||||||
});
|
|
||||||
}, 1);
|
}, 1);
|
||||||
}, [fitView, nodes, edges, getViewport()]);
|
}, [nodes, edges, getViewport()]);
|
||||||
|
|
||||||
const handleToggleModalView = () =>
|
const handleToggleModalView = () =>
|
||||||
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
||||||
@@ -133,7 +150,6 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
edges={edges}
|
edges={edges}
|
||||||
edgeTypes={EdgeTypes}
|
edgeTypes={EdgeTypes}
|
||||||
nodeTypes={NodeTypes}
|
nodeTypes={NodeTypes}
|
||||||
fitView
|
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
colorMode="dark"
|
colorMode="dark"
|
||||||
nodesDraggable={false}
|
nodesDraggable={false}
|
||||||
@@ -151,6 +167,12 @@ 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
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
environment={environment}
|
||||||
|
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"
|
||||||
@@ -179,7 +201,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={
|
icon={
|
||||||
viewMode === ViewMode.Modal
|
viewMode === ViewMode.Modal
|
||||||
? faArrowUpRightFromSquare
|
? faDownLeftAndUpRightToCenter
|
||||||
: faUpRightAndDownLeftFromCenter
|
: faUpRightAndDownLeftFromCenter
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -187,9 +209,25 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
{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}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
<PermissionSimulation {...accessTreeData} />
|
<PermissionSimulation {...accessTreeData} />
|
||||||
<Background color="#5d5f64" bgColor="#111419" variant={BackgroundVariant.Dots} />
|
<Background color="#5d5f64" bgColor="#111419" variant={BackgroundVariant.Dots} />
|
||||||
<Controls position="bottom-left" />
|
<Controls position="bottom-left" showFitView={false} showInteractive={false}>
|
||||||
|
<ControlButton onClick={goToRootNode}>
|
||||||
|
<Tooltip position="right" content="Go to Root Folder">
|
||||||
|
<FontAwesomeIcon icon={faUpLong} />
|
||||||
|
</Tooltip>
|
||||||
|
</ControlButton>
|
||||||
|
</Controls>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ export const PermissionSimulation = ({
|
|||||||
className="mr-1 rounded"
|
className="mr-1 rounded"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
onClick={handlePermissionSimulation}
|
onClick={handlePermissionSimulation}
|
||||||
|
rightIcon={
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="pl-1 text-sm text-bunker-300 hover:text-primary hover:opacity-80"
|
||||||
|
icon={faChevronDown}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Permission Simulation
|
Permission Simulation
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react";
|
|||||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||||
import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries";
|
import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries";
|
||||||
|
import { TSecretFolderWithPath } from "@app/hooks/api/secretFolders/types";
|
||||||
|
|
||||||
import { useAccessTreeContext } from "../components";
|
import { useAccessTreeContext } from "../components";
|
||||||
import { PermissionAccess } from "../types";
|
import { PermissionAccess } from "../types";
|
||||||
@@ -15,8 +16,24 @@ import {
|
|||||||
getSubjectActionRuleMap,
|
getSubjectActionRuleMap,
|
||||||
positionElements
|
positionElements
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
import { createShowMoreNode } from "../utils/createShowMoreNode";
|
||||||
|
|
||||||
export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, MongoQuery>) => {
|
const INITIAL_FOLDERS_PER_LEVEL = 10;
|
||||||
|
const FOLDERS_INCREMENT = 10;
|
||||||
|
|
||||||
|
type LevelFolderMap = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
folders: TSecretFolderWithPath[];
|
||||||
|
visibleCount: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useAccessTree = (
|
||||||
|
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>,
|
||||||
|
searchPath: string
|
||||||
|
) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
|
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
|
||||||
const [nodes, setNodes] = useNodesState<Node>([]);
|
const [nodes, setNodes] = useNodesState<Node>([]);
|
||||||
@@ -27,10 +44,104 @@ export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, Mo
|
|||||||
currentWorkspace.id
|
currentWorkspace.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [levelFolderMap, setLevelFolderMap] = useState<LevelFolderMap>({});
|
||||||
|
const [totalFolderCount, setTotalFolderCount] = useState(0);
|
||||||
|
|
||||||
|
const showMoreFolders = (parentId: string) => {
|
||||||
|
setLevelFolderMap((prevMap) => {
|
||||||
|
const level = prevMap[parentId];
|
||||||
|
if (!level) return prevMap;
|
||||||
|
|
||||||
|
const newVisibleCount = Math.min(
|
||||||
|
level.visibleCount + FOLDERS_INCREMENT,
|
||||||
|
level.folders.length
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevMap,
|
||||||
|
[parentId]: {
|
||||||
|
...level,
|
||||||
|
visibleCount: newVisibleCount,
|
||||||
|
hasMore: newVisibleCount < level.folders.length
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const levelsWithMoreFolders = Object.entries(levelFolderMap)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
.filter(([_, level]) => level.hasMore)
|
||||||
|
.map(([parentId]) => parentId);
|
||||||
|
|
||||||
|
const getLevelCounts = (parentId: string) => {
|
||||||
|
const level = levelFolderMap[parentId];
|
||||||
|
if (!level) return { visibleCount: 0, totalCount: 0, hasMore: false };
|
||||||
|
|
||||||
|
return {
|
||||||
|
visibleCount: level.visibleCount,
|
||||||
|
totalCount: level.folders.length,
|
||||||
|
hasMore: level.hasMore
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return;
|
if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return;
|
||||||
|
|
||||||
const { folders, name } = environmentsFolders[environment];
|
const { folders } = environmentsFolders[environment];
|
||||||
|
|
||||||
|
setTotalFolderCount(folders.length);
|
||||||
|
|
||||||
|
const groupedFolders: Record<string, TSecretFolderWithPath[]> = {};
|
||||||
|
|
||||||
|
const filteredFolders = folders.filter((folder) => {
|
||||||
|
if (folder.path.startsWith(searchPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
searchPath.startsWith(folder.path) &&
|
||||||
|
(folder.path === "/" ||
|
||||||
|
searchPath === folder.path ||
|
||||||
|
searchPath.indexOf("/", folder.path.length) === folder.path.length)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredFolders.forEach((folder) => {
|
||||||
|
const parentId = folder.parentId || "";
|
||||||
|
if (!groupedFolders[parentId]) {
|
||||||
|
groupedFolders[parentId] = [];
|
||||||
|
}
|
||||||
|
groupedFolders[parentId].push(folder);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newLevelFolderMap: LevelFolderMap = {};
|
||||||
|
|
||||||
|
Object.entries(groupedFolders).forEach(([parentId, folderList]) => {
|
||||||
|
const key = parentId;
|
||||||
|
newLevelFolderMap[key] = {
|
||||||
|
folders: folderList,
|
||||||
|
visibleCount: Math.min(INITIAL_FOLDERS_PER_LEVEL, folderList.length),
|
||||||
|
hasMore: folderList.length > INITIAL_FOLDERS_PER_LEVEL
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setLevelFolderMap(newLevelFolderMap);
|
||||||
|
}, [permissions, environmentsFolders, environment, subject, secretName, searchPath]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!environmentsFolders ||
|
||||||
|
!permissions ||
|
||||||
|
!environmentsFolders[environment] ||
|
||||||
|
Object.keys(levelFolderMap).length === 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const { name } = environmentsFolders[environment];
|
||||||
|
|
||||||
const roleNode = createRoleNode({
|
const roleNode = createRoleNode({
|
||||||
subject,
|
subject,
|
||||||
@@ -39,7 +150,13 @@ export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, Mo
|
|||||||
|
|
||||||
const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
|
const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
|
||||||
|
|
||||||
const folderNodes = folders.map((folder) =>
|
const visibleFolders: TSecretFolderWithPath[] = [];
|
||||||
|
|
||||||
|
Object.values(levelFolderMap).forEach((levelData) => {
|
||||||
|
visibleFolders.push(...levelData.folders.slice(0, levelData.visibleCount));
|
||||||
|
});
|
||||||
|
|
||||||
|
const folderNodes = visibleFolders.map((folder) =>
|
||||||
createFolderNode({
|
createFolderNode({
|
||||||
folder,
|
folder,
|
||||||
permissions,
|
permissions,
|
||||||
@@ -69,10 +186,44 @@ export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, Mo
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const init = positionElements([roleNode, ...folderNodes], [...folderEdges]);
|
const addMoreButtons: Node[] = [];
|
||||||
|
|
||||||
|
Object.entries(levelFolderMap).forEach(([parentId, levelData]) => {
|
||||||
|
const key = parentId === "null" ? null : parentId;
|
||||||
|
|
||||||
|
if (key && levelData.hasMore) {
|
||||||
|
const showMoreButtonNode = createShowMoreNode({
|
||||||
|
parentId: key,
|
||||||
|
onClick: () => showMoreFolders(key),
|
||||||
|
remaining: levelData.folders.length - levelData.visibleCount
|
||||||
|
});
|
||||||
|
|
||||||
|
addMoreButtons.push(showMoreButtonNode);
|
||||||
|
|
||||||
|
folderEdges.push(
|
||||||
|
createBaseEdge({
|
||||||
|
source: key,
|
||||||
|
target: showMoreButtonNode.id,
|
||||||
|
access: PermissionAccess.Full,
|
||||||
|
hideEdge: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const init = positionElements([roleNode, ...folderNodes, ...addMoreButtons], [...folderEdges]);
|
||||||
setNodes(init.nodes);
|
setNodes(init.nodes);
|
||||||
setEdges(init.edges);
|
setEdges(init.edges);
|
||||||
}, [permissions, environmentsFolders, environment, subject, secretName, setNodes, setEdges]);
|
}, [
|
||||||
|
levelFolderMap,
|
||||||
|
permissions,
|
||||||
|
environmentsFolders,
|
||||||
|
environment,
|
||||||
|
subject,
|
||||||
|
secretName,
|
||||||
|
setNodes,
|
||||||
|
setEdges
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes,
|
nodes,
|
||||||
@@ -86,6 +237,11 @@ export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, Mo
|
|||||||
secretName,
|
secretName,
|
||||||
setSecretName,
|
setSecretName,
|
||||||
viewMode,
|
viewMode,
|
||||||
setViewMode
|
setViewMode,
|
||||||
|
levelFolderMap,
|
||||||
|
showMoreFolders,
|
||||||
|
levelsWithMoreFolders,
|
||||||
|
getLevelCounts,
|
||||||
|
totalFolderCount
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { faChevronRight, faFolderClosed } 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 { createShowMoreNode } from "../utils/createShowMoreNode";
|
||||||
|
|
||||||
|
export const ShowMoreButtonNode = ({
|
||||||
|
data: { onClick, remaining }
|
||||||
|
}: NodeProps & { data: ReturnType<typeof createShowMoreNode>["data"] }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Handle type="target" position={Position.Top} style={{ visibility: "hidden" }} />
|
||||||
|
|
||||||
|
<Tooltip position="right" content="Click to show 10 more folders">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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`}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,7 +7,8 @@ export enum PermissionAccess {
|
|||||||
export enum PermissionNode {
|
export enum PermissionNode {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Folder = "folder",
|
Folder = "folder",
|
||||||
Environment = "environment"
|
Environment = "environment",
|
||||||
|
ShowMoreButton = "showMoreButton"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PermissionEdge {
|
export enum PermissionEdge {
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { PermissionAccess, PermissionEdge } from "../types";
|
|||||||
export const createBaseEdge = ({
|
export const createBaseEdge = ({
|
||||||
source,
|
source,
|
||||||
target,
|
target,
|
||||||
access
|
access,
|
||||||
|
hideEdge = false
|
||||||
}: {
|
}: {
|
||||||
source: string;
|
source: string;
|
||||||
target: string;
|
target: string;
|
||||||
access: PermissionAccess;
|
access: PermissionAccess;
|
||||||
|
hideEdge?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const color = access === PermissionAccess.None ? "#707174" : "#ccccce";
|
const color = access === PermissionAccess.None ? "#707174" : "#ccccce";
|
||||||
return {
|
return {
|
||||||
@@ -17,10 +19,12 @@ export const createBaseEdge = ({
|
|||||||
source,
|
source,
|
||||||
target,
|
target,
|
||||||
type: PermissionEdge.Base,
|
type: PermissionEdge.Base,
|
||||||
markerEnd: {
|
markerEnd: hideEdge
|
||||||
type: MarkerType.ArrowClosed,
|
? undefined
|
||||||
color
|
: {
|
||||||
},
|
type: MarkerType.ArrowClosed,
|
||||||
style: { stroke: color }
|
color
|
||||||
|
},
|
||||||
|
style: { stroke: hideEdge ? "transparent" : color }
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { PermissionNode } from "../types";
|
||||||
|
|
||||||
|
export const createShowMoreNode = ({
|
||||||
|
parentId,
|
||||||
|
onClick,
|
||||||
|
remaining
|
||||||
|
}: {
|
||||||
|
parentId: string | null;
|
||||||
|
onClick: () => void;
|
||||||
|
remaining: number;
|
||||||
|
}) => {
|
||||||
|
const id = `show-more-${parentId || "root"}`;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: PermissionNode.ShowMoreButton,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
parentId,
|
||||||
|
onClick,
|
||||||
|
remaining
|
||||||
|
},
|
||||||
|
width: 100,
|
||||||
|
height: 40
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -2,27 +2,60 @@ import Dagre from "@dagrejs/dagre";
|
|||||||
import { Edge, Node } from "@xyflow/react";
|
import { Edge, Node } from "@xyflow/react";
|
||||||
|
|
||||||
export const positionElements = (nodes: Node[], edges: Edge[]) => {
|
export const positionElements = (nodes: Node[], edges: Edge[]) => {
|
||||||
|
const regularNodes = nodes.filter((node) => node.type !== "showMoreButton");
|
||||||
|
const showMoreNodes = nodes.filter((node) => node.type === "showMoreButton");
|
||||||
|
|
||||||
|
const nodeMap: Record<string, Node> = {};
|
||||||
|
|
||||||
const dagre = new Dagre.graphlib.Graph({ directed: true })
|
const dagre = new Dagre.graphlib.Graph({ directed: true })
|
||||||
.setDefaultEdgeLabel(() => ({}))
|
.setDefaultEdgeLabel(() => ({}))
|
||||||
.setGraph({ rankdir: "TB" });
|
.setGraph({ rankdir: "TB" });
|
||||||
|
|
||||||
edges.forEach((edge) => dagre.setEdge(edge.source, edge.target));
|
edges.forEach((edge) => dagre.setEdge(edge.source, edge.target));
|
||||||
nodes.forEach((node) => dagre.setNode(node.id, node));
|
|
||||||
|
regularNodes.forEach((node) => dagre.setNode(node.id, node));
|
||||||
|
|
||||||
Dagre.layout(dagre, {});
|
Dagre.layout(dagre, {});
|
||||||
|
|
||||||
return {
|
const positionedNodes = regularNodes.map((node) => {
|
||||||
nodes: nodes.map((node) => {
|
const { x, y } = dagre.node(node.id);
|
||||||
const { x, y } = dagre.node(node.id);
|
|
||||||
|
|
||||||
return {
|
const positionedNode = {
|
||||||
...node,
|
...node,
|
||||||
position: {
|
position: {
|
||||||
x: x - (node.width ? node.width / 2 : 0),
|
x: x - (node.width ? node.width / 2 : 0),
|
||||||
y: y - (node.height ? node.height / 2 : 0)
|
y: y - (node.height ? node.height / 2 : 0)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}),
|
|
||||||
|
nodeMap[node.id] = positionedNode;
|
||||||
|
|
||||||
|
return positionedNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 parentWidth = parentNode.width || 150;
|
||||||
|
const buttonWidth = node.width || 100;
|
||||||
|
|
||||||
|
const buttonX = isStart ? parentX - buttonWidth - 20 : parentX + parentWidth + 20;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
position: {
|
||||||
|
x: buttonX,
|
||||||
|
y: parentY
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
nodes: [...positionedNodes, ...positionedShowMoreNodes],
|
||||||
edges
|
edges
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user