From a5f198a3d57e54e012a6dd76a15b1b74da9c49e4 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Wed, 26 Mar 2025 17:30:20 -0300 Subject: [PATCH] Allow access tree relative path graph on path filter --- .../permissions/AccessTree/AccessTree.tsx | 4 +- .../permissions/AccessTree/hooks/index.ts | 93 +++++++++++++++---- .../components/AccessTreeSecretPathInput.tsx | 12 ++- .../v2/SecretPathInput/SecretPathInput.tsx | 4 - 4 files changed, 86 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/permissions/AccessTree/AccessTree.tsx b/frontend/src/components/permissions/AccessTree/AccessTree.tsx index 8376ae72f..265e87623 100644 --- a/frontend/src/components/permissions/AccessTree/AccessTree.tsx +++ b/frontend/src/components/permissions/AccessTree/AccessTree.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { MongoAbility, MongoQuery } from "@casl/ability"; import { - faArrowsToDot, + faAnglesUp, faArrowsUpDownLeftRight, faArrowUpRightFromSquare, faDownLeftAndUpRightToCenter, @@ -241,7 +241,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => { - + diff --git a/frontend/src/components/permissions/AccessTree/hooks/index.ts b/frontend/src/components/permissions/AccessTree/hooks/index.ts index 9a0292fb6..8651ea543 100644 --- a/frontend/src/components/permissions/AccessTree/hooks/index.ts +++ b/frontend/src/components/permissions/AccessTree/hooks/index.ts @@ -88,21 +88,18 @@ export const useAccessTree = ( if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return; const { folders } = environmentsFolders[environment]; - setTotalFolderCount(folders.length); - const groupedFolders: Record = {}; + const searchPathFolder = folders.find((folder) => folder.path === searchPath); const filteredFolders = folders.filter((folder) => { - if (folder.path.startsWith(searchPath)) { + if (folder.path === searchPath) { return true; } if ( - searchPath.startsWith(folder.path) && - (folder.path === "/" || - searchPath === folder.path || - searchPath.indexOf("/", folder.path.length) === folder.path.length) + folder.path.startsWith(searchPath) && + (searchPath === "/" || folder.path.charAt(searchPath.length) === "/") ) { return true; } @@ -110,11 +107,17 @@ 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); }); @@ -129,7 +132,18 @@ export const useAccessTree = ( }; }); - setLevelFolderMap(newLevelFolderMap); + if (rootFolder) { + setLevelFolderMap({ + ...newLevelFolderMap, + __rootFolderId: { + folders: [rootFolder], + visibleCount: 1, + hasMore: false + } + }); + } else { + setLevelFolderMap(newLevelFolderMap); + } }, [permissions, environmentsFolders, environment, subject, secretName, searchPath]); useEffect(() => { @@ -151,11 +165,15 @@ export const useAccessTree = ( const actionRuleMap = getSubjectActionRuleMap(subject, permissions); const visibleFolders: TSecretFolderWithPath[] = []; - - Object.values(levelFolderMap).forEach((levelData) => { - visibleFolders.push(...levelData.folders.slice(0, levelData.visibleCount)); + Object.entries(levelFolderMap).forEach(([key, levelData]) => { + if (key !== "__rootFolderId") { + visibleFolders.push(...levelData.folders.slice(0, levelData.visibleCount)); + } }); + // eslint-disable-next-line no-underscore-dangle + const rootFolder = levelFolderMap.__rootFolderId?.folders[0]; + const folderNodes = visibleFolders.map((folder) => createFolderNode({ folder, @@ -167,10 +185,45 @@ export const useAccessTree = ( }) ); - const folderEdges = folderNodes.map(({ data: folder }) => { - const actions = Object.values(folder.actions); + const folderEdges: Edge[] = []; + if (rootFolder) { + const rootFolderNode = folderNodes.find( + (node) => node.data.id === rootFolder.id || node.data.path === rootFolder.path + ); + + if (rootFolderNode) { + const rootActions = Object.values(rootFolderNode.data.actions); + let rootAccess: PermissionAccess; + + if (Object.values(rootActions).some((action) => action === PermissionAccess.Full)) { + rootAccess = PermissionAccess.Full; + } else if ( + Object.values(rootActions).some((action) => action === PermissionAccess.Partial) + ) { + rootAccess = PermissionAccess.Partial; + } else { + rootAccess = PermissionAccess.None; + } + + folderEdges.push( + createBaseEdge({ + source: roleNode.id, + target: rootFolderNode.id, + access: rootAccess + }) + ); + } + } + + folderNodes.forEach(({ data: folder }) => { + if (rootFolder && (folder.id === rootFolder.id || folder.path === rootFolder.path)) { + return; + } + + const actions = Object.values(folder.actions); let access: PermissionAccess; + if (Object.values(actions).some((action) => action === PermissionAccess.Full)) { access = PermissionAccess.Full; } else if (Object.values(actions).some((action) => action === PermissionAccess.Partial)) { @@ -179,16 +232,20 @@ export const useAccessTree = ( access = PermissionAccess.None; } - return createBaseEdge({ - source: folder.parentId ?? roleNode.id, - target: folder.id, - access - }); + folderEdges.push( + createBaseEdge({ + source: folder.parentId ?? roleNode.id, + target: folder.id, + access + }) + ); }); const addMoreButtons: Node[] = []; Object.entries(levelFolderMap).forEach(([parentId, levelData]) => { + if (parentId === "__rootFolderId") return; + const key = parentId === "null" ? null : parentId; if (key && levelData.hasMore) { diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx index 0e3c8777e..1702d1214 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/AccessTreeSecretPathInput.tsx @@ -1,5 +1,5 @@ -import { useRef, useState } from "react"; -import { faSearch, faTimes } from "@fortawesome/free-solid-svg-icons"; +import { useEffect, useRef, useState } from "react"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; @@ -34,6 +34,12 @@ export const AccessTreeSecretPathInput = ({ }, 200); }; + useEffect(() => { + if (!isFocused) { + setIsExpanded(false); + } + }, [isFocused]); + const focusInput = () => { const inputElement = inputRef.current?.querySelector("input"); if (inputElement) { @@ -69,7 +75,7 @@ export const AccessTreeSecretPathInput = ({ } }} > - + ) : ( diff --git a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx index 31d0ff72c..c82e492e8 100644 --- a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx +++ b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx @@ -48,7 +48,6 @@ export const SecretPathInput = ({ }, [propValue]); useEffect(() => { - // update secret path if input is valid if ( (debouncedInputValue.length > 0 && debouncedInputValue[debouncedInputValue.length - 1] === "/") || @@ -59,7 +58,6 @@ export const SecretPathInput = ({ }, [debouncedInputValue]); useEffect(() => { - // filter suggestions based on matching const searchFragment = debouncedInputValue.split("/").pop() || ""; const filteredSuggestions = folders .filter((suggestionEntry) => @@ -78,7 +76,6 @@ export const SecretPathInput = ({ const validPaths = inputValue.split("/"); validPaths.pop(); - // removed trailing slash const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`; onChange?.(newValue); setInputValue(newValue); @@ -102,7 +99,6 @@ export const SecretPathInput = ({ }; const handleInputChange = (e: any) => { - // propagate event to react-hook-form onChange if (onChange) { onChange(e.target.value); }