diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 60ec9143c..001b11820 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -581,6 +581,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { folders: z .object({ folderName: z.string(), + folderImported: z.boolean(), secrets: z.array(z.string()).optional() }) .array() @@ -856,6 +857,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId }); + if (secrets?.length || secretRotations?.length) { const secretCount = (secrets?.length ?? 0) + diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index 2732c8eca..f045df664 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -231,6 +231,7 @@ export const secretImportDALFactory = (db: TDbClient) => { [folderName: string]: { secrets: string[]; folderId: string; + folderImported: boolean; }; }; }; @@ -251,11 +252,13 @@ export const secretImportDALFactory = (db: TDbClient) => { } if (!updatedAcc[env].folders[folder]) { - updatedAcc[env].folders[folder] = { secrets: [], folderId: item.folderId }; + updatedAcc[env].folders[folder] = { secrets: [], folderId: item.folderId, folderImported: false }; } if ("secretId" in item && item.secretId) { updatedAcc[env].folders[folder].secrets = [...updatedAcc[env].folders[folder].secrets, item.secretId]; + } else { + updatedAcc[env].folders[folder].folderImported = true; } return updatedAcc; @@ -271,6 +274,7 @@ export const secretImportDALFactory = (db: TDbClient) => { return { folderName, folderId: folderData.folderId, + folderImported: folderData.folderImported, ...(hasSecrets && { secrets: folderData.secrets }) }; }); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 2d2a71de2..b6356bcfd 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -843,6 +843,7 @@ export const secretImportServiceFactory = ({ ...el, folders: el.folders.map((folderItem) => ({ folderId: folderItem.folderId, + folderImported: folderItem.folderImported, secrets: folderItem.secrets, folderName: deepPaths.find((p) => p.folderId === folderItem.folderId)?.path || `...${folderItem.folderName}` })) diff --git a/backend/src/services/secret-import/secret-import-types.ts b/backend/src/services/secret-import/secret-import-types.ts index b598b2530..217750e87 100644 --- a/backend/src/services/secret-import/secret-import-types.ts +++ b/backend/src/services/secret-import/secret-import-types.ts @@ -65,6 +65,7 @@ export type FolderInfo = { folderName: string; secrets?: string[]; folderId: string; + folderImported: boolean; envSlug?: string; }; diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 4b74b5f4e..4024394a6 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -41,7 +41,10 @@ export type DashboardProjectSecretsDetailsResponse = { totalSecretCount?: number; totalSecretRotationCount?: number; totalCount: number; - importedBy?: { envName: string; folders: { folderName: string; secrets?: string[] }[] }[]; + importedBy?: { + envName: string; + folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; + }[]; }; export type DashboardProjectSecretsByKeys = { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx index 73640a8c8..8efcd3ac4 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx @@ -1,201 +1,209 @@ -import React, { KeyboardEvent, useMemo, useState } from "react"; +import React, { useMemo, useState } from "react"; import { - faChevronDown, + faChevronLeft, 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"; + type: "folder" | "secret"; path: string; secretKey?: string; - envName?: string; - depth?: number; + reference: string; id: string; } interface CollapsibleSecretImportsProps { - importedBy?: Environment[]; + importedBy?: { + envName: string; + folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; + }[]; } export const CollapsibleSecretImports: React.FC = ({ importedBy = [] }) => { - const [expandedEnvs, setExpandedEnvs] = useState>( - importedBy.reduce((acc, env) => ({ ...acc, [env.envName]: true }), {}) - ); + const [currentEnvIndex, setCurrentEnvIndex] = useState(0); - const toggleEnvironment = (envName: string) => { - setExpandedEnvs((prev) => ({ - ...prev, - [envName]: !prev[envName] - })); - }; + const truncatePath = (path: string, maxLength = 45): string => + path.length <= maxLength ? path : `...${path.slice(-(maxLength - 3))}`; - 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(() => { + const groupedItems = useMemo(() => { return importedBy.map((env) => { - const items: FlatItem[] = [ - { - type: "environment", - path: env.envName, - envName: env.envName, - id: `env-${env.envName}` - } - ]; + const envItems: FlatItem[] = []; env.folders.forEach((folder) => { - const folderId = `folder-${env.envName}-${folder.folderName}`; - items.push({ - type: "folder", - path: folder.folderName, - id: folderId - }); + if (folder.folderImported) { + envItems.push({ + type: "folder", + path: folder.folderName, + id: `folder-${env.envName}-${folder.folderName}`, + reference: "Imported" + }); + } - if (folder.secrets && folder.secrets.length > 0) { + if (folder.secrets?.length) { folder.secrets.forEach((secret) => { const secretPath = folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`; - const secretId = `secret-${env.envName}-${secretPath}`; - items.push({ + envItems.push({ type: "secret", path: secretPath, secretKey: secret, - id: secretId + id: `secret-${env.envName}-${secretPath}`, + reference: "Referenced" }); }); } }); - 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] + envName: env.envName, + items: envItems.sort((a, b) => { + const aPath = a.path.startsWith("/") ? a.path : `/${a.path}`; + const bPath = b.path.startsWith("/") ? b.path : `/${b.path}`; + + const aSegments = aPath.split("/").filter(Boolean).length; + const bSegments = bPath.split("/").filter(Boolean).length; + + if (aSegments !== bSegments) { + return aSegments - bSegments; + } + + return aPath.localeCompare(bPath); + }) }; }); }, [importedBy]); + const totalEnvironments = groupedItems.length; + const currentItems = totalEnvironments > 0 ? groupedItems[currentEnvIndex]?.items || [] : []; + + const handlePrevEnvironment = () => { + if (currentEnvIndex > 0) { + setCurrentEnvIndex(currentEnvIndex - 1); + } + }; + + const handleNextEnvironment = () => { + if (currentEnvIndex < totalEnvironments - 1) { + setCurrentEnvIndex(currentEnvIndex + 1); + } + }; + + const renderPaginationButton = ( + direction: "prev" | "next", + onClick: () => void, + disabled: boolean + ) => { + const icon = direction === "prev" ? faChevronLeft : faChevronRight; + const label = direction === "prev" ? "Previous environment" : "Next environment"; + + return ( + + ); + }; + return ( -
-
-
-