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

View File

@@ -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) => {
</ControlButton>
<ControlButton onClick={goToRootNode}>
<Tooltip position="right" content="Go to root folder">
<FontAwesomeIcon icon={faArrowsToDot} />
<FontAwesomeIcon icon={faAnglesUp} />
</Tooltip>
</ControlButton>
</Controls>

View File

@@ -88,21 +88,18 @@ export const useAccessTree = (
if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return;
const { folders } = environmentsFolders[environment];
setTotalFolderCount(folders.length);
const groupedFolders: Record<string, TSecretFolderWithPath[]> = {};
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<string, TSecretFolderWithPath[]> = {};
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) {

View File

@@ -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 = ({
}
}}
>
<FontAwesomeIcon icon={faTimes} />
<FontAwesomeIcon icon={faSearch} />
</div>
) : (
<Tooltip position="bottom" content="Search paths">

View File

@@ -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);
}