import { subject } from "@casl/ability"; import { faAnglesRight, faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal, IconButton, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { SecretType, SecretV3RawSanitized, TDeleteSecretBatchDTO, TSecretFolder } from "@app/hooks/api/types"; import { MoveSecretsModal } from "@app/pages/secret-manager/OverviewPage/components/SelectionPanel/components"; export enum EntryType { FOLDER = "folder", SECRET = "secret" } type Props = { secretPath: string; resetSelectedEntries: () => void; selectedEntries: { [EntryType.FOLDER]: Record>; [EntryType.SECRET]: Record>; }; }; export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntries }: Props) => { const { permission } = useProjectPermission(); const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "bulkDeleteEntries", "bulkMoveSecrets" ] as const); const selectedFolderCount = Object.keys(selectedEntries.folder).length; const selectedKeysCount = Object.keys(selectedEntries.secret).length; const isRotatedSecretSelected = Object.values(selectedEntries.secret).some((record) => Object.values(record).some((secret) => secret.isRotatedSecret) ); const selectedCount = selectedFolderCount + selectedKeysCount; const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; const userAvailableEnvs = currentWorkspace?.environments || []; const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); const { mutateAsync: deleteFolder } = useDeleteFolder(); const isMultiSelectActive = selectedCount > 0; // user should have the ability to delete secrets/folders in at least one of the envs const shouldShowDelete = userAvailableEnvs.some((env) => permission.can( ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath, secretName: "*", secretTags: ["*"] }) ) ); const getDeleteModalTitle = () => { if (selectedFolderCount > 0 && selectedKeysCount > 0) { return "Do you want to delete the selected secrets and folders across environments?"; } if (selectedKeysCount > 0) { return "Do you want to delete the selected secrets across environments?"; } return "Do you want to delete the selected folders across environments?"; }; const handleBulkDelete = async () => { let processedEntries = 0; const promises = userAvailableEnvs.map(async (env) => { // additional check: ensure that bulk delete is only executed on envs that user has access to if ( permission.can( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.SecretFolders, { environment: env.slug, secretPath }) ) ) { await Promise.all( Object.values(selectedEntries.folder).map(async (folderRecord) => { const folder = folderRecord[env.slug]; if (folder) { processedEntries += 1; await deleteFolder({ folderId: folder?.id, path: secretPath, environment: env.slug, projectId: workspaceId }); } }) ); } const secretsToDelete = Object.values(selectedEntries.secret).reduce( (accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => { const entry = secretRecord[env.slug]; if (!entry) return accum; const canDeleteSecret = permission.can( ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath, secretName: entry.key, secretTags: (entry?.tags || []).map((i) => i.slug) }) ); if (entry && canDeleteSecret && !entry.isRotatedSecret) { return [ ...accum, { secretKey: entry.key, type: SecretType.Shared } ]; } return accum; }, [] ); if (secretsToDelete.length > 0) { processedEntries += secretsToDelete.length; await deleteBatchSecretV3({ secretPath, workspaceId, environment: env.slug, secrets: secretsToDelete }); } }); const results = await Promise.allSettled(promises); const areEntriesDeleted = results.some((result) => result.status === "fulfilled"); if (processedEntries === 0) { handlePopUpClose("bulkDeleteEntries"); createNotification({ type: "info", text: "You don't have access to delete selected items" }); } else if (areEntriesDeleted) { handlePopUpClose("bulkDeleteEntries"); resetSelectedEntries(); createNotification({ type: "success", text: "Successfully deleted selected secrets and folders" }); } else { createNotification({ type: "error", text: "Failed to delete selected secrets and folders" }); } }; const areFoldersSelected = Boolean(Object.keys(selectedEntries[EntryType.FOLDER]).length); return ( <>
{selectedCount} Selected
{isRotatedSecretSelected && ( Rotated Secrets will not be affected by action. )} {shouldShowDelete && ( <>
)}
handlePopUpToggle("bulkMoveSecrets", isOpen)} environments={userAvailableEnvs} projectId={workspaceId} projectSlug={currentWorkspace.slug} sourceSecretPath={secretPath} secrets={selectedEntries[EntryType.SECRET]} onComplete={resetSelectedEntries} /> handlePopUpToggle("bulkDeleteEntries", isOpen)} onDeleteApproved={handleBulkDelete} /> ); };