diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx new file mode 100644 index 000000000..73640a8c8 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx @@ -0,0 +1,202 @@ +import React, { KeyboardEvent, useMemo, useState } from "react"; +import { + faChevronDown, + faChevronRight, + faCodeBranch, + faFolder, + faKey, + faWarning +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +interface Folder { + folderName: string; + secrets?: string[]; +} + +interface Environment { + envName: string; + folders: Folder[]; +} + +interface FlatItem { + type: "folder" | "secret" | "environment"; + path: string; + secretKey?: string; + envName?: string; + depth?: number; + id: string; +} + +interface CollapsibleSecretImportsProps { + importedBy?: Environment[]; +} + +export const CollapsibleSecretImports: React.FC = ({ + importedBy = [] +}) => { + const [expandedEnvs, setExpandedEnvs] = useState>( + importedBy.reduce((acc, env) => ({ ...acc, [env.envName]: true }), {}) + ); + + const toggleEnvironment = (envName: string) => { + setExpandedEnvs((prev) => ({ + ...prev, + [envName]: !prev[envName] + })); + }; + + const handleKeyDown = (envName: string, e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggleEnvironment(envName); + } + }; + + const truncatePath = (path: string, maxLength: number = 45) => { + if (path.length <= maxLength) return path; + return `...${path.slice(-(maxLength - 3))}`; + }; + + const processedEnvironments = useMemo(() => { + return importedBy.map((env) => { + const items: FlatItem[] = [ + { + type: "environment", + path: env.envName, + envName: env.envName, + id: `env-${env.envName}` + } + ]; + + env.folders.forEach((folder) => { + const folderId = `folder-${env.envName}-${folder.folderName}`; + items.push({ + type: "folder", + path: folder.folderName, + id: folderId + }); + + if (folder.secrets && folder.secrets.length > 0) { + folder.secrets.forEach((secret) => { + const secretPath = + folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`; + const secretId = `secret-${env.envName}-${secretPath}`; + + items.push({ + type: "secret", + path: secretPath, + secretKey: secret, + id: secretId + }); + }); + } + }); + + const [envItem, ...rest] = items; + rest.sort((a, b) => { + const aSegments = a.path.split("/").filter(Boolean); + const bSegments = b.path.split("/").filter(Boolean); + + for (let i = 0; i < Math.min(aSegments.length, bSegments.length); i += 1) { + if (aSegments[i] !== bSegments[i]) { + return aSegments[i].localeCompare(bSegments[i]); + } + } + + return aSegments.length - bSegments.length; + }); + + return { + ...env, + items: [envItem, ...rest] + }; + }); + }, [importedBy]); + + return ( +
+
+
+
+
+

+ Warning: The following resources will be affected by this change +

+
    +
  • Deleting will remove it from all folders where it's imported
  • +
  • + Any secrets referencing this will display their reference syntax (like{" "} + + {"{env.secretPath.key}"} + + ) instead of actual values +
  • +
  • Secrets referencing this will not be automatically deleted
  • +
+
+
+ +
+
+ {processedEnvironments.map((env) => ( +
+ + + {expandedEnvs[env.envName] && ( +
+ {env.items.slice(1).map((item) => { + const isSecret = item.type === "secret"; + + return ( +
+
+
+
+ ); + })} +
+ )} +
+ ))} +
+
+
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDeletionImpact.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDeletionImpact.tsx deleted file mode 100644 index 6c65950ac..000000000 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDeletionImpact.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { faWarning } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { Select, SelectItem } from "@app/components/v2"; - -import { SecretTreeView } from "../ActionBar/ReplicateFolderFromBoard/SecretTreeView"; - -interface Folder { - folderName: string; - secrets?: string[]; -} - -interface Environment { - envName: string; - folders: Folder[]; -} - -interface SecretDeletionImpactProps { - importedBy?: Environment[]; -} - -interface SecretItem { - id: string; - secretKey: string; - secretValue?: string; - secretPath?: string; -} - -interface FolderStructure { - items: SecretItem[]; - subFolders: { - [key: string]: FolderStructure; - }; -} - -export const SecretDeletionImpact: React.FC = ({ importedBy = [] }) => { - const [treeData, setTreeData] = useState(null); - const [selectedEnv, setSelectedEnv] = useState(importedBy[0]?.envName || ""); - - const handleEnvironmentChange = (value: string) => { - setSelectedEnv(value); - }; - - function transformImportedDataToTreeView( - environments: Environment[], - selectedEnvironment: string - ): FolderStructure | null { - const environment = environments.find((env) => env.envName === selectedEnvironment); - if (!environment) return null; - - const rootStructure: FolderStructure = { - items: [], - subFolders: {} - }; - - environment.folders.forEach((folder) => { - const path = folder.folderName; - const pathParts = path.split("/").filter((part) => part !== ""); - - if (pathParts.length === 0) { - if (folder.secrets && folder.secrets.length > 0) { - folder.secrets.forEach((secret) => { - rootStructure.items.push({ - id: `${selectedEnvironment}:${path}:${secret}`, - secretKey: secret, - secretPath: path - }); - }); - } - return; - } - - let current = rootStructure; - - pathParts.forEach((part, index) => { - if (!current.subFolders[part]) { - current.subFolders[part] = { - items: [], - subFolders: {} - }; - } - - current = current.subFolders[part]; - - const isLastPart = index === pathParts.length - 1; - if (isLastPart && folder.secrets && folder.secrets.length > 0) { - folder.secrets.forEach((secret) => { - current.items.push({ - id: `${selectedEnvironment}:${path}:${secret}`, - secretKey: secret, - secretPath: path - }); - }); - } - }); - }); - - return rootStructure; - } - - useEffect(() => { - const transformedData = transformImportedDataToTreeView(importedBy, selectedEnv); - setTreeData(transformedData); - }, [selectedEnv, importedBy]); - - return ( -
-
-
- -
-
-

- Warning: This secret is imported by another folder -

-
    -
  • Deleting will remove it from all locations where it's imported
  • -
  • - Any dependent secrets will display their reference syntax (like{" "} - {"{env.secretPath.key}"}) instead of actual values -
  • -
  • Dependent secrets themselves will not be automatically deleted
  • -
-
-
- -
- {}} isDisabled /> -
- -
- -
-
- ); -}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx index e8110f720..c6ce9a958 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx @@ -16,10 +16,10 @@ import { WsTag } from "@app/hooks/api/types"; import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal"; import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store"; +import { CollapsibleSecretImports } from "./CollapsibleSecretImports"; import { SecretDetailSidebar } from "./SecretDetailSidebar"; import { SecretItem } from "./SecretItem"; import { FontAwesomeSpriteSymbols } from "./SecretListView.utils"; -import { SecretDeletionImpact } from "./SecretDeletionImpact"; type Props = { secrets?: SecretV3RawSanitized[]; @@ -361,7 +361,9 @@ export const SecretListView = ({ onDeleteApproved={handleSecretDelete} buttonText="Delete Secret" > - {importedBy && importedBy.length > 0 && } + {importedBy && importedBy.length > 0 && ( + + )}