Add referenced secret delete warning to overview page

This commit is contained in:
carlosmonastyrski
2025-04-11 14:00:31 -03:00
parent aec131543f
commit 1ec87fae75
5 changed files with 159 additions and 10 deletions

View File

@@ -142,6 +142,34 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}) })
.array() .array()
.optional(), .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(), totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(),
totalSecretCount: 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 { return {
folders, folders,
dynamicSecrets, dynamicSecrets,
@@ -482,6 +532,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalImportCount, totalImportCount,
totalSecretCount, totalSecretCount,
totalSecretRotationCount, totalSecretRotationCount,
importedByEnvs,
totalCount: totalCount:
(totalFolderCount ?? 0) + (totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) + (totalDynamicSecretCount ?? 0) +

View File

@@ -24,6 +24,7 @@ export type DashboardProjectSecretsOverviewResponse = {
totalUniqueDynamicSecretsInPage: number; totalUniqueDynamicSecretsInPage: number;
totalUniqueFoldersInPage: number; totalUniqueFoldersInPage: number;
totalUniqueSecretImportsInPage: number; totalUniqueSecretImportsInPage: number;
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
totalUniqueSecretRotationsInPage: number; totalUniqueSecretRotationsInPage: number;
}; };
@@ -41,13 +42,15 @@ export type DashboardProjectSecretsDetailsResponse = {
totalSecretCount?: number; totalSecretCount?: number;
totalSecretRotationCount?: number; totalSecretRotationCount?: number;
totalCount: number; totalCount: number;
importedBy?: { importedBy?: ProjectSecretsImportedBy[];
environment: { name: string; slug: string }; };
folders: {
name: string; export type ProjectSecretsImportedBy = {
secrets?: { secretId: string; referencedSecretKey: string }[]; environment: { name: string; slug: string };
isImported: boolean; folders: {
}[]; name: string;
secrets?: { secretId: string; referencedSecretKey: string }[];
isImported: boolean;
}[]; }[];
}; };

View File

@@ -272,7 +272,8 @@ export const OverviewPage = () => {
totalUniqueSecretsInPage, totalUniqueSecretsInPage,
totalUniqueSecretImportsInPage, totalUniqueSecretImportsInPage,
totalUniqueDynamicSecretsInPage, totalUniqueDynamicSecretsInPage,
totalUniqueSecretRotationsInPage totalUniqueSecretRotationsInPage,
importedByEnvs
} = overview ?? {}; } = overview ?? {};
const secretImportsShaped = secretImports const secretImportsShaped = secretImports
@@ -1042,6 +1043,7 @@ export const OverviewPage = () => {
secretPath={secretPath} secretPath={secretPath}
selectedEntries={selectedEntries} selectedEntries={selectedEntries}
resetSelectedEntries={resetSelectedEntries} resetSelectedEntries={resetSelectedEntries}
importedByEnvs={importedByEnvs}
/> />
<div className="thin-scrollbar mt-4"> <div className="thin-scrollbar mt-4">
<TableContainer <TableContainer

View File

@@ -1,3 +1,4 @@
import { useMemo } from "react";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faAnglesRight, faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faAnglesRight, faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -14,6 +15,7 @@ import {
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
import { ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types";
import { import {
SecretType, SecretType,
SecretV3RawSanitized, SecretV3RawSanitized,
@@ -21,6 +23,7 @@ import {
TSecretFolder TSecretFolder
} from "@app/hooks/api/types"; } from "@app/hooks/api/types";
import { MoveSecretsModal } from "@app/pages/secret-manager/OverviewPage/components/SelectionPanel/components"; import { MoveSecretsModal } from "@app/pages/secret-manager/OverviewPage/components/SelectionPanel/components";
import { CollapsibleSecretImports } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports";
export enum EntryType { export enum EntryType {
FOLDER = "folder", FOLDER = "folder",
@@ -34,9 +37,15 @@ type Props = {
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>; [EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>; [EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
}; };
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
}; };
export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntries }: Props) => { export const SelectionPanel = ({
secretPath,
resetSelectedEntries,
selectedEntries,
importedByEnvs
}: Props) => {
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ 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<string, ProjectSecretsImportedBy[]> = {};
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<string, (typeof allFolders)[number][]> = {};
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<string>();
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 = () => { const getDeleteModalTitle = () => {
if (selectedFolderCount > 0 && selectedKeysCount > 0) { if (selectedFolderCount > 0 && selectedKeysCount > 0) {
return "Do you want to delete the selected secrets and folders across environments?"; 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()} title={getDeleteModalTitle()}
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)} onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
onDeleteApproved={handleBulkDelete} onDeleteApproved={handleBulkDelete}
formContent={
importedBy &&
importedBy.some((element) => element.folders.length > 0) && (
<CollapsibleSecretImports
importedBy={importedBy}
secretsToDelete={secretsToDeleteKeys}
/>
)
}
/> />
</> </>
); );

View File

@@ -81,12 +81,12 @@ import {
useSelectedSecrets useSelectedSecrets
} from "../../SecretMainPage.store"; } from "../../SecretMainPage.store";
import { Filter, RowType } from "../../SecretMainPage.types"; import { Filter, RowType } from "../../SecretMainPage.types";
import { CollapsibleSecretImports } from "../SecretListView/CollapsibleSecretImports";
import { ReplicateFolderFromBoard } from "./ReplicateFolderFromBoard/ReplicateFolderFromBoard"; import { ReplicateFolderFromBoard } from "./ReplicateFolderFromBoard/ReplicateFolderFromBoard";
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm"; import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
import { CreateSecretImportForm } from "./CreateSecretImportForm"; import { CreateSecretImportForm } from "./CreateSecretImportForm";
import { FolderForm } from "./FolderForm"; import { FolderForm } from "./FolderForm";
import { MoveSecretsModal } from "./MoveSecretsModal"; import { MoveSecretsModal } from "./MoveSecretsModal";
import { CollapsibleSecretImports } from "../SecretListView/CollapsibleSecretImports";
type TParsedEnv = Record<string, { value: string; comments: string[]; secretPath?: string }>; type TParsedEnv = Record<string, { value: string; comments: string[]; secretPath?: string }>;
type TParsedFolderEnv = Record< type TParsedFolderEnv = Record<