Allow access tree relative path graph on path filter

This commit is contained in:
carlosmonastyrski
2025-03-26 17:30:20 -03:00
parent c516ce8196
commit a5f198a3d5
4 changed files with 86 additions and 27 deletions
@@ -1,7 +1,7 @@
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, faAnglesUp,
faArrowsUpDownLeftRight, faArrowsUpDownLeftRight,
faArrowUpRightFromSquare, faArrowUpRightFromSquare,
faDownLeftAndUpRightToCenter, faDownLeftAndUpRightToCenter,
@@ -241,7 +241,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
</ControlButton> </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={faArrowsToDot} /> <FontAwesomeIcon icon={faAnglesUp} />
</Tooltip> </Tooltip>
</ControlButton> </ControlButton>
</Controls> </Controls>
@@ -88,21 +88,18 @@ export const useAccessTree = (
if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return; if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return;
const { folders } = environmentsFolders[environment]; const { folders } = environmentsFolders[environment];
setTotalFolderCount(folders.length); setTotalFolderCount(folders.length);
const groupedFolders: Record<string, TSecretFolderWithPath[]> = {}; const searchPathFolder = folders.find((folder) => folder.path === searchPath);
const filteredFolders = folders.filter((folder) => { const filteredFolders = folders.filter((folder) => {
if (folder.path.startsWith(searchPath)) { if (folder.path === searchPath) {
return true; return true;
} }
if ( if (
searchPath.startsWith(folder.path) && folder.path.startsWith(searchPath) &&
(folder.path === "/" || (searchPath === "/" || folder.path.charAt(searchPath.length) === "/")
searchPath === folder.path ||
searchPath.indexOf("/", folder.path.length) === folder.path.length)
) { ) {
return true; return true;
} }
@@ -110,11 +107,17 @@ export const useAccessTree = (
return false; return false;
}); });
const rootFolder = searchPathFolder || filteredFolders.find((f) => f.path === "/");
const groupedFolders: Record<string, TSecretFolderWithPath[]> = {};
filteredFolders.forEach((folder) => { filteredFolders.forEach((folder) => {
const parentId = folder.parentId || ""; const parentId = folder.parentId || "";
if (!groupedFolders[parentId]) { if (!groupedFolders[parentId]) {
groupedFolders[parentId] = []; groupedFolders[parentId] = [];
} }
groupedFolders[parentId].push(folder); 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]); }, [permissions, environmentsFolders, environment, subject, secretName, searchPath]);
useEffect(() => { useEffect(() => {
@@ -151,11 +165,15 @@ export const useAccessTree = (
const actionRuleMap = getSubjectActionRuleMap(subject, permissions); const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
const visibleFolders: TSecretFolderWithPath[] = []; const visibleFolders: TSecretFolderWithPath[] = [];
Object.entries(levelFolderMap).forEach(([key, levelData]) => {
Object.values(levelFolderMap).forEach((levelData) => { if (key !== "__rootFolderId") {
visibleFolders.push(...levelData.folders.slice(0, levelData.visibleCount)); 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) => const folderNodes = visibleFolders.map((folder) =>
createFolderNode({ createFolderNode({
folder, folder,
@@ -167,10 +185,45 @@ export const useAccessTree = (
}) })
); );
const folderEdges = folderNodes.map(({ data: folder }) => { const folderEdges: Edge[] = [];
const actions = Object.values(folder.actions);
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; let access: PermissionAccess;
if (Object.values(actions).some((action) => action === PermissionAccess.Full)) { if (Object.values(actions).some((action) => action === PermissionAccess.Full)) {
access = PermissionAccess.Full; access = PermissionAccess.Full;
} else if (Object.values(actions).some((action) => action === PermissionAccess.Partial)) { } else if (Object.values(actions).some((action) => action === PermissionAccess.Partial)) {
@@ -179,16 +232,20 @@ export const useAccessTree = (
access = PermissionAccess.None; access = PermissionAccess.None;
} }
return createBaseEdge({ folderEdges.push(
source: folder.parentId ?? roleNode.id, createBaseEdge({
target: folder.id, source: folder.parentId ?? roleNode.id,
access target: folder.id,
}); access
})
);
}); });
const addMoreButtons: Node[] = []; const addMoreButtons: Node[] = [];
Object.entries(levelFolderMap).forEach(([parentId, levelData]) => { Object.entries(levelFolderMap).forEach(([parentId, levelData]) => {
if (parentId === "__rootFolderId") return;
const key = parentId === "null" ? null : parentId; const key = parentId === "null" ? null : parentId;
if (key && levelData.hasMore) { if (key && levelData.hasMore) {
@@ -1,5 +1,5 @@
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { faSearch, faTimes } from "@fortawesome/free-solid-svg-icons"; import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -34,6 +34,12 @@ export const AccessTreeSecretPathInput = ({
}, 200); }, 200);
}; };
useEffect(() => {
if (!isFocused) {
setIsExpanded(false);
}
}, [isFocused]);
const focusInput = () => { const focusInput = () => {
const inputElement = inputRef.current?.querySelector("input"); const inputElement = inputRef.current?.querySelector("input");
if (inputElement) { if (inputElement) {
@@ -69,7 +75,7 @@ export const AccessTreeSecretPathInput = ({
} }
}} }}
> >
<FontAwesomeIcon icon={faTimes} /> <FontAwesomeIcon icon={faSearch} />
</div> </div>
) : ( ) : (
<Tooltip position="bottom" content="Search paths"> <Tooltip position="bottom" content="Search paths">
@@ -48,7 +48,6 @@ export const SecretPathInput = ({
}, [propValue]); }, [propValue]);
useEffect(() => { useEffect(() => {
// update secret path if input is valid
if ( if (
(debouncedInputValue.length > 0 && (debouncedInputValue.length > 0 &&
debouncedInputValue[debouncedInputValue.length - 1] === "/") || debouncedInputValue[debouncedInputValue.length - 1] === "/") ||
@@ -59,7 +58,6 @@ export const SecretPathInput = ({
}, [debouncedInputValue]); }, [debouncedInputValue]);
useEffect(() => { useEffect(() => {
// filter suggestions based on matching
const searchFragment = debouncedInputValue.split("/").pop() || ""; const searchFragment = debouncedInputValue.split("/").pop() || "";
const filteredSuggestions = folders const filteredSuggestions = folders
.filter((suggestionEntry) => .filter((suggestionEntry) =>
@@ -78,7 +76,6 @@ export const SecretPathInput = ({
const validPaths = inputValue.split("/"); const validPaths = inputValue.split("/");
validPaths.pop(); validPaths.pop();
// removed trailing slash
const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`; const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`;
onChange?.(newValue); onChange?.(newValue);
setInputValue(newValue); setInputValue(newValue);
@@ -102,7 +99,6 @@ export const SecretPathInput = ({
}; };
const handleInputChange = (e: any) => { const handleInputChange = (e: any) => {
// propagate event to react-hook-form onChange
if (onChange) { if (onChange) {
onChange(e.target.value); onChange(e.target.value);
} }