diff --git a/backend/src/services/secret-folder/secret-folder-fns.ts b/backend/src/services/secret-folder/secret-folder-fns.ts index 7e799a826..a3783a1b9 100644 --- a/backend/src/services/secret-folder/secret-folder-fns.ts +++ b/backend/src/services/secret-folder/secret-folder-fns.ts @@ -1,11 +1,14 @@ import { TSecretFolders } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; export const buildFolderPath = ( folder: TSecretFolders, foldersMap: Record, depth: number = 0 ): string => { - if (depth > 20) return ""; + if (depth > 20) { + throw new InternalServerError({ message: "Maximum folder depth of 20 exceeded" }); + } if (!folder.parentId) { return depth === 0 ? "/" : ""; } diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index d4f58a1d4..96bcdd6f3 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -619,10 +619,29 @@ export const secretFolderServiceFactory = ({ const relevantFolders = folders.filter((folder) => folder.envId === env.id); const foldersMap = Object.fromEntries(relevantFolders.map((folder) => [folder.id, folder])); - const foldersWithPath = relevantFolders.map((folder) => ({ - ...folder, - path: buildFolderPath(folder, foldersMap) - })); + const foldersWithPath = relevantFolders + .map((folder) => { + try { + return { + ...folder, + path: buildFolderPath(folder, foldersMap) + }; + } catch (error) { + return null; + } + }) + .filter(Boolean) as { + path: string; + id: string; + createdAt: Date; + updatedAt: Date; + name: string; + envId: string; + version?: number | null | undefined; + parentId?: string | null | undefined; + isReserved?: boolean | undefined; + description?: string | undefined; + }[]; return [env.slug, { ...env, folders: foldersWithPath }]; }) diff --git a/frontend/src/components/permissions/AccessTree/AccessTree.tsx b/frontend/src/components/permissions/AccessTree/AccessTree.tsx index 265e87623..4ba2c718b 100644 --- a/frontend/src/components/permissions/AccessTree/AccessTree.tsx +++ b/frontend/src/components/permissions/AccessTree/AccessTree.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useState } from "react"; import { MongoAbility, MongoQuery } from "@casl/ability"; import { faAnglesUp, - faArrowsUpDownLeftRight, faArrowUpRightFromSquare, faDownLeftAndUpRightToCenter, faUpRightAndDownLeftFromCenter, @@ -29,7 +28,7 @@ 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 { AccessTreeErrorBoundary, AccessTreeProvider } from "./components"; import { BasePermissionEdge } from "./edges"; import { useAccessTree } from "./hooks"; import { FolderNode, RoleNode } from "./nodes"; @@ -231,14 +230,12 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { /> )} - - - fitView({ duration: 800 })}> - - - - + fitView({ duration: 800 })} + > diff --git a/frontend/src/components/permissions/AccessTree/hooks/index.ts b/frontend/src/components/permissions/AccessTree/hooks/index.ts index 8651ea543..a03f8fa0f 100644 --- a/frontend/src/components/permissions/AccessTree/hooks/index.ts +++ b/frontend/src/components/permissions/AccessTree/hooks/index.ts @@ -89,17 +89,18 @@ export const useAccessTree = ( const { folders } = environmentsFolders[environment]; setTotalFolderCount(folders.length); - - const searchPathFolder = folders.find((folder) => folder.path === searchPath); + const groupedFolders: Record = {}; const filteredFolders = folders.filter((folder) => { - if (folder.path === searchPath) { + if (folder.path.startsWith(searchPath)) { return true; } if ( - folder.path.startsWith(searchPath) && - (searchPath === "/" || folder.path.charAt(searchPath.length) === "/") + searchPath.startsWith(folder.path) && + (folder.path === "/" || + searchPath === folder.path || + searchPath.indexOf("/", folder.path.length) === folder.path.length) ) { return true; } @@ -107,17 +108,11 @@ export const useAccessTree = ( return false; }); - const rootFolder = searchPathFolder || filteredFolders.find((f) => f.path === "/"); - - const groupedFolders: Record = {}; - filteredFolders.forEach((folder) => { const parentId = folder.parentId || ""; - if (!groupedFolders[parentId]) { groupedFolders[parentId] = []; } - groupedFolders[parentId].push(folder); }); @@ -132,18 +127,7 @@ export const useAccessTree = ( }; }); - if (rootFolder) { - setLevelFolderMap({ - ...newLevelFolderMap, - __rootFolderId: { - folders: [rootFolder], - visibleCount: 1, - hasMore: false - } - }); - } else { - setLevelFolderMap(newLevelFolderMap); - } + setLevelFolderMap(newLevelFolderMap); }, [permissions, environmentsFolders, environment, subject, secretName, searchPath]); useEffect(() => { @@ -155,11 +139,14 @@ export const useAccessTree = ( ) return; - const { name } = environmentsFolders[environment]; + const { slug } = environmentsFolders[environment]; const roleNode = createRoleNode({ subject, - environment: name + environment: slug, + environments: environmentsFolders, + onSubjectChange: setSubject, + onEnvironmentChange: setEnvironment }); const actionRuleMap = getSubjectActionRuleMap(subject, permissions); @@ -252,7 +239,8 @@ export const useAccessTree = ( const showMoreButtonNode = createShowMoreNode({ parentId: key, onClick: () => showMoreFolders(key), - remaining: levelData.folders.length - levelData.visibleCount + remaining: levelData.folders.length - levelData.visibleCount, + subject }); addMoreButtons.push(showMoreButtonNode); @@ -261,8 +249,7 @@ export const useAccessTree = ( createBaseEdge({ source: key, target: showMoreButtonNode.id, - access: PermissionAccess.Full, - hideEdge: true + access: PermissionAccess.Partial }) ); } diff --git a/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx index d38d896f2..f6ffe212e 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx @@ -1,10 +1,42 @@ +import { Dispatch, SetStateAction } from "react"; +import { faFileImport, faFolder, faKey, faLock } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Handle, NodeProps, Position } from "@xyflow/react"; +import { Select, SelectItem } from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; +import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types"; + import { createRoleNode } from "../utils"; +const getSubjectIcon = (subject: ProjectPermissionSub) => { + switch (subject) { + case ProjectPermissionSub.Secrets: + return ; + case ProjectPermissionSub.SecretFolders: + return ; + case ProjectPermissionSub.DynamicSecrets: + return ; + case ProjectPermissionSub.SecretImports: + return ; + default: + return ; + } +}; + +const formatLabel = (text: string) => { + return text.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); +}; + export const RoleNode = ({ - data: { subject, environment } -}: NodeProps & { data: ReturnType["data"] }) => { + data: { subject, environment, onSubjectChange, onEnvironmentChange, environments } +}: NodeProps & { + data: ReturnType["data"] & { + onSubjectChange: Dispatch>; + onEnvironmentChange: (value: string) => void; + environments: TProjectEnvironmentsFolders; + }; +}) => { return ( <> -
-
- {subject.replace("-", " ")} Access -
-

{environment}

+
+
+
+
Subject
+ +
+ +
+
Environment
+
diff --git a/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx b/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx index 2a6cabf43..dc412d8ca 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/ShowMoreButtonNode.tsx @@ -12,20 +12,26 @@ export const ShowMoreButtonNode = ({ const tooltipText = `${remaining} ${remaining === 1 ? "folder is" : "folders are"} hidden. Click to show ${remaining > 10 ? "10 more" : ""}`; return ( - <> - +
+ - - - - +
+ + + +
+
); }; diff --git a/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts b/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts index 354a69482..69ec60896 100644 --- a/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts +++ b/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts @@ -1,17 +1,31 @@ +import { Dispatch, SetStateAction } from "react"; + +import { ProjectPermissionSub } from "@app/context"; +import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types"; + import { PermissionNode } from "../types"; export const createRoleNode = ({ subject, - environment + environment, + environments, + onSubjectChange, + onEnvironmentChange }: { subject: string; environment: string; + environments: TProjectEnvironmentsFolders; + onSubjectChange: Dispatch>; + onEnvironmentChange: (value: string) => void; }) => ({ id: `role-${subject}-${environment}`, position: { x: 0, y: 0 }, data: { subject, - environment + environment, + environments, + onSubjectChange, + onEnvironmentChange }, type: PermissionNode.Role, height: 48, diff --git a/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts b/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts index e5758ffc9..f12cd598e 100644 --- a/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts +++ b/frontend/src/components/permissions/AccessTree/utils/createShowMoreNode.ts @@ -1,14 +1,30 @@ +import { ProjectPermissionSub } from "@app/context"; + import { PermissionNode } from "../types"; export const createShowMoreNode = ({ parentId, onClick, - remaining + remaining, + subject }: { parentId: string | null; onClick: () => void; remaining: number; + subject: ProjectPermissionSub; }) => { + let height: number; + + switch (subject) { + case ProjectPermissionSub.DynamicSecrets: + height = 130; + break; + case ProjectPermissionSub.Secrets: + height = 85; + break; + default: + height = 64; + } const id = `show-more-${parentId || "root"}`; return { id, @@ -19,7 +35,11 @@ export const createShowMoreNode = ({ onClick, remaining }, - width: 100, - height: 40 + width: 150, + height, + style: { + background: "transparent", + border: "none" + } }; }; diff --git a/frontend/src/components/permissions/AccessTree/utils/positionElements.ts b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts index 8b90a752f..c4daf6317 100644 --- a/frontend/src/components/permissions/AccessTree/utils/positionElements.ts +++ b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts @@ -2,8 +2,10 @@ import Dagre from "@dagrejs/dagre"; import { Edge, Node } from "@xyflow/react"; export const positionElements = (nodes: Node[], edges: Edge[]) => { - const regularNodes = nodes.filter((node) => node.type !== "showMoreButton"); const showMoreNodes = nodes.filter((node) => node.type === "showMoreButton"); + const showMoreParentIds = new Set( + showMoreNodes.map((node) => node.data.parentId).filter(Boolean) + ); const nodeMap: Record = {}; const childrenMap: Record = {}; @@ -17,75 +19,79 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => { const dagre = new Dagre.graphlib.Graph({ directed: true }) .setDefaultEdgeLabel(() => ({})) - .setGraph({ rankdir: "TB" }); + .setGraph({ + rankdir: "TB", + nodesep: 50, + ranksep: 70 + }); + + nodes.forEach((node) => { + dagre.setNode(node.id, { + width: node.width || 150, + height: node.height || 40 + }); + }); edges.forEach((edge) => dagre.setEdge(edge.source, edge.target)); - regularNodes.forEach((node) => dagre.setNode(node.id, node)); - Dagre.layout(dagre, {}); - const positionedNodes = regularNodes.map((node) => { + const positionedNodes = nodes.map((node) => { const { x, y } = dagre.node(node.id); - const positionedNode = { - ...node, - position: { - x: x - (node.width ? node.width / 2 : 0), - y: y - (node.height ? node.height / 2 : 0) - } - }; - - nodeMap[node.id] = 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 parentId = node.data.parentId as string; - - const parentNode = nodeMap[parentId] || positionedNodes[0]; - const lastChildNode = findLastChildNode(parentId); - - const referenceNode = lastChildNode || parentNode; - - const referenceX = referenceNode.position.x; - const referenceY = referenceNode.position.y; - - const referenceWidth = referenceNode.width || 150; - - const buttonX = referenceX + referenceWidth - 85; - const buttonY = referenceY - 25; + if (node.type === "role") { + return { + ...node, + position: { + x: x - (node.width ? node.width / 2 : 0), + y: y - 150 + } + }; + } return { ...node, position: { - x: buttonX, - y: buttonY - } + x: x - (node.width ? node.width / 2 : 0), + y: y - (node.height ? node.height / 2 : 0) + }, + style: node.type === "showMoreButton" ? { ...node.style, zIndex: 10 } : node.style }; }); + positionedNodes.forEach((node) => { + nodeMap[node.id] = node; + }); + + Array.from(showMoreParentIds).forEach((parentId) => { + const showMoreNodeIndex = positionedNodes.findIndex( + (node) => node.type === "showMoreButton" && node.data.parentId === parentId + ); + + if (showMoreNodeIndex !== -1) { + const siblings = positionedNodes.filter( + (node) => node.data?.parentId === parentId && node.type !== "showMoreButton" + ); + + if (siblings.length > 0) { + const rightmostSibling = siblings.reduce( + (rightmost, current) => (current.position.x > rightmost.position.x ? current : rightmost), + siblings[0] + ); + + positionedNodes[showMoreNodeIndex] = { + ...positionedNodes[showMoreNodeIndex], + position: { + x: rightmostSibling.position.x + (rightmostSibling.width || 150) + 30, + y: rightmostSibling.position.y + } + }; + } + } + }); + return { - nodes: [...positionedNodes, ...positionedShowMoreNodes], + nodes: positionedNodes, edges }; };