From 1ec87fae75cb7a2ced746862730d9da82fc8cd8a Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 11 Apr 2025 14:00:31 -0300 Subject: [PATCH] Add referenced secret delete warning to overview page --- .../src/server/routes/v1/dashboard-router.ts | 51 ++++++++++ frontend/src/hooks/api/dashboard/types.ts | 17 ++-- .../OverviewPage/OverviewPage.tsx | 4 +- .../SelectionPanel/SelectionPanel.tsx | 95 ++++++++++++++++++- .../components/ActionBar/ActionBar.tsx | 2 +- 5 files changed, 159 insertions(+), 10 deletions(-) diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 8ea4fb48d..5d75a6d2c 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -142,6 +142,34 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }) .array() .optional(), + importedByEnvs: z + .object({ + environment: z.string(), + importedBy: z + .object({ + environment: z.object({ + name: z.string(), + slug: z.string() + }), + folders: z + .object({ + name: z.string(), + isImported: z.boolean(), + secrets: z + .object({ + secretId: z.string(), + referencedSecretKey: z.string() + }) + .array() + .optional() + }) + .array() + }) + .array() + .optional() + }) + .array() + .optional(), totalFolderCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(), totalSecretCount: z.number().optional(), @@ -471,6 +499,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } + const importedByEnvs = []; + + for await (const environment of environments) { + const importedBy = await server.services.secretImport.getFolderIsImportedBy({ + path: secretPath, + environment, + projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secrets: secrets?.filter((s) => s.environment === environment) + }); + + if (importedBy) { + importedByEnvs.push({ + environment, + importedBy + }); + } + } + return { folders, dynamicSecrets, @@ -482,6 +532,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalImportCount, totalSecretCount, totalSecretRotationCount, + importedByEnvs, totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index c88760edc..e80874f8e 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -24,6 +24,7 @@ export type DashboardProjectSecretsOverviewResponse = { totalUniqueDynamicSecretsInPage: number; totalUniqueFoldersInPage: number; totalUniqueSecretImportsInPage: number; + importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]; totalUniqueSecretRotationsInPage: number; }; @@ -41,13 +42,15 @@ export type DashboardProjectSecretsDetailsResponse = { totalSecretCount?: number; totalSecretRotationCount?: number; totalCount: number; - importedBy?: { - environment: { name: string; slug: string }; - folders: { - name: string; - secrets?: { secretId: string; referencedSecretKey: string }[]; - isImported: boolean; - }[]; + importedBy?: ProjectSecretsImportedBy[]; +}; + +export type ProjectSecretsImportedBy = { + environment: { name: string; slug: string }; + folders: { + name: string; + secrets?: { secretId: string; referencedSecretKey: string }[]; + isImported: boolean; }[]; }; diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 4ee50ed4f..5fe31a7b3 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -272,7 +272,8 @@ export const OverviewPage = () => { totalUniqueSecretsInPage, totalUniqueSecretImportsInPage, totalUniqueDynamicSecretsInPage, - totalUniqueSecretRotationsInPage + totalUniqueSecretRotationsInPage, + importedByEnvs } = overview ?? {}; const secretImportsShaped = secretImports @@ -1042,6 +1043,7 @@ export const OverviewPage = () => { secretPath={secretPath} selectedEntries={selectedEntries} resetSelectedEntries={resetSelectedEntries} + importedByEnvs={importedByEnvs} />
>; [EntryType.SECRET]: Record>; }; + importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]; }; -export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntries }: Props) => { +export const SelectionPanel = ({ + secretPath, + resetSelectedEntries, + selectedEntries, + importedByEnvs +}: Props) => { const { permission } = useProjectPermission(); const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ @@ -72,6 +81,81 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri ) ); + const secretsToDeleteKeys = useMemo(() => { + return Object.values(selectedEntries.secret).flatMap((entries) => + Object.values(entries).map((secret) => secret.key) + ); + }, [selectedEntries]); + + const filterAndMergeEnvironments = ( + envNames: string[], + envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[] + ): ProjectSecretsImportedBy[] => { + const filteredEnvs = envs.filter((env) => envNames.includes(env.environment)); + + if (filteredEnvs.length === 0) return []; + + const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy); + const groupedBySlug: Record = {}; + + allImportedBy.forEach((item) => { + const { slug } = item.environment; + if (!groupedBySlug[slug]) groupedBySlug[slug] = []; + groupedBySlug[slug].push(item); + }); + + const mergedImportedBy = Object.values(groupedBySlug).map((group) => { + const { environment } = group[0]; + const allFolders = group.flatMap((item) => item.folders); + + const foldersByName: Record = {}; + allFolders.forEach((folder) => { + if (!foldersByName[folder.name]) foldersByName[folder.name] = []; + foldersByName[folder.name].push(folder); + }); + + const mergedFolders = Object.entries(foldersByName).map(([name, folders]) => { + const isImported = folders.some((folder) => folder.isImported); + const allSecrets = folders.flatMap((folder) => folder.secrets || []); + + const uniqueSecrets: { secretId: string; referencedSecretKey: string }[] = []; + const secretIds = new Set(); + + allSecrets + .filter((secret) => secretsToDeleteKeys.includes(secret.referencedSecretKey)) + .forEach((secret) => { + if (!secretIds.has(secret.secretId)) { + secretIds.add(secret.secretId); + uniqueSecrets.push(secret); + } + }); + + return { + name, + isImported, + ...(uniqueSecrets.length > 0 ? { secrets: uniqueSecrets } : {}) + }; + }); + + return { + environment, + folders: mergedFolders.filter( + (folder) => folder.isImported || (folder.secrets && folder.secrets.length > 0) + ) + }; + }); + + return mergedImportedBy; + }; + + const importedBy = useMemo(() => { + if (selectedKeysCount === 0 || !importedByEnvs) return null; + return filterAndMergeEnvironments( + Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries)), + importedByEnvs + ); + }, [importedByEnvs, selectedEntries, selectedKeysCount]); + const getDeleteModalTitle = () => { if (selectedFolderCount > 0 && selectedKeysCount > 0) { return "Do you want to delete the selected secrets and folders across environments?"; @@ -241,6 +325,15 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri title={getDeleteModalTitle()} onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)} onDeleteApproved={handleBulkDelete} + formContent={ + importedBy && + importedBy.some((element) => element.folders.length > 0) && ( + + ) + } /> ); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 4c598bfe6..c62f15d95 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -81,12 +81,12 @@ import { useSelectedSecrets } from "../../SecretMainPage.store"; import { Filter, RowType } from "../../SecretMainPage.types"; +import { CollapsibleSecretImports } from "../SecretListView/CollapsibleSecretImports"; import { ReplicateFolderFromBoard } from "./ReplicateFolderFromBoard/ReplicateFolderFromBoard"; import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm"; import { CreateSecretImportForm } from "./CreateSecretImportForm"; import { FolderForm } from "./FolderForm"; import { MoveSecretsModal } from "./MoveSecretsModal"; -import { CollapsibleSecretImports } from "../SecretListView/CollapsibleSecretImports"; type TParsedEnv = Record; type TParsedFolderEnv = Record<