import { TypeOptions } from "react-toastify"; import { subject } from "@casl/ability"; import { faAngleDown, faAnglesRight, faCheckCircle, faChevronRight, faCodeCommit, faDownload, faEye, faEyeSlash, faFileImport, faFilter, faFingerprint, faFolder, faFolderPlus, faKey, faMagnifyingGlass, faMinusSquare, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import FileSaver from "file-saver"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, DropdownSubMenu, DropdownSubMenuContent, DropdownSubMenuTrigger, IconButton, Input, Modal, ModalContent, Tooltip, UpgradePlanModal } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, useSubscription } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; import { SecretType, WsTag } from "@app/hooks/api/types"; import { PopUpNames, usePopUpAction, useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store"; import { Filter, RowType } from "../../SecretMainPage.types"; import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm"; import { CreateSecretImportForm } from "./CreateSecretImportForm"; import { FolderForm } from "./FolderForm"; import { MoveSecretsModal } from "./MoveSecretsModal"; type Props = { // switch the secrets type as it gets decrypted after api call environment: string; // @depreciated will be moving all these details to zustand workspaceId: string; projectSlug: string; secretPath?: string; filter: Filter; tags?: WsTag[]; isVisible?: boolean; snapshotCount: number; isSnapshotCountLoading?: boolean; onSearchChange: (term: string) => void; onToggleTagFilter: (tagId: string) => void; onVisibilityToggle: () => void; onToggleRowType: (rowType: RowType) => void; onClickRollbackMode: () => void; }; export const ActionBar = ({ environment, workspaceId, projectSlug, secretPath = "/", filter, tags = [], isVisible, snapshotCount, isSnapshotCountLoading, onSearchChange, onToggleTagFilter, onVisibilityToggle, onClickRollbackMode, onToggleRowType }: Props) => { const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "addFolder", "addDynamicSecret", "addSecretImport", "bulkDeleteSecrets", "moveSecrets", "misc", "upgradePlan" ] as const); const { subscription } = useSubscription(); const { openPopUp } = usePopUpAction(); const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); const { mutateAsync: moveSecrets } = useMoveSecrets(); const selectedSecrets = useSelectedSecrets(); const { reset: resetSelectedSecret } = useSelectedSecretActions(); const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); const { permission } = useProjectPermission(); const shouldCheckFolderPermission = permission.rules.some((rule) => (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders) ); const handleFolderCreate = async (folderName: string) => { try { await createFolder({ name: folderName, path: secretPath, environment, projectId: workspaceId }); handlePopUpClose("addFolder"); createNotification({ type: "success", text: "Successfully created folder" }); } catch (error) { console.log(error); createNotification({ type: "error", text: "Failed to create folder" }); } }; const handleSecretDownload = async () => { const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ workspaceId, expandSecretReferences: true, includeImports: true, environment, secretPath }); const secretsPicked = new Set(); const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; localSecrets.forEach((el) => { secretsPicked.add(el.secretKey); secretsToDownload.push({ key: el.secretKey, value: el.secretValue, comment: el.secretComment }); }); for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { const secret = localImportedSecrets[i].secrets[j]; if (!secretsPicked.has(secret.secretKey)) { secretsToDownload.push({ key: secret.secretKey, value: secret.secretValue, comment: secret.secretComment }); } secretsPicked.add(secret.secretKey); } } const file = secretsToDownload .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) .reduce( (prev, { key, comment, value }, index) => prev + (comment ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` : `${key}=${value}\n`), "" ); const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); FileSaver.saveAs(blob, `${environment}.env`); }; const handleSecretBulkDelete = async () => { const bulkDeletedSecrets = Object.values(selectedSecrets); try { await deleteBatchSecretV3({ secretPath, workspaceId, environment, secrets: bulkDeletedSecrets.map(({ key }) => ({ secretKey: key, type: SecretType.Shared })) }); resetSelectedSecret(); handlePopUpClose("bulkDeleteSecrets"); createNotification({ type: "success", text: "Successfully deleted secrets" }); } catch (error) { console.log(error); createNotification({ type: "error", text: "Failed to delete secrets" }); } }; const handleSecretsMove = async ({ destinationEnvironment, destinationSecretPath, shouldOverwrite }: { destinationEnvironment: string; destinationSecretPath: string; shouldOverwrite: boolean; }) => { try { const secretsToMove = Object.values(selectedSecrets); const { isDestinationUpdated, isSourceUpdated } = await moveSecrets({ projectSlug, shouldOverwrite, sourceEnvironment: environment, sourceSecretPath: secretPath, destinationEnvironment, destinationSecretPath, projectId: workspaceId, secretIds: secretsToMove.map((sec) => sec.id) }); let notificationMessage = ""; let notificationType: TypeOptions = "info"; if (isDestinationUpdated && isSourceUpdated) { notificationMessage = "Successfully moved selected secrets"; notificationType = "success"; } else if (isDestinationUpdated) { notificationMessage = "Successfully created secrets in destination. A secret approval request has been generated for the source."; } else if (isSourceUpdated) { notificationMessage = "A secret approval request has been generated in the destination"; } else { notificationMessage = "A secret approval request has been generated in both the source and the destination."; } createNotification({ type: notificationType, text: notificationMessage }); resetSelectedSecret(); } catch (error) { console.error(error); } }; return ( <>
} value={filter.searchFilter} onChange={(evt) => { onSearchChange(evt.target.value); }} />
!include).length) && "border-primary/50 text-primary" )} > Filter By { e.preventDefault(); onToggleRowType(RowType.Import); }} icon={filter?.include[RowType.Import] && } iconPos="right" >
Imports
{ e.preventDefault(); onToggleRowType(RowType.Folder); }} icon={filter?.include[RowType.Folder] && } iconPos="right" >
Folders
{ e.preventDefault(); onToggleRowType(RowType.DynamicSecret); }} icon={ filter?.include[RowType.DynamicSecret] && } iconPos="right" >
Dynamic Secrets
{ e.preventDefault(); onToggleRowType(RowType.Secret); }} icon={filter?.include[RowType.Secret] && } iconPos="right" >
Secrets
} > Tags Apply tags to filter secrets {tags.map(({ id, slug, color }) => ( { evt.preventDefault(); onToggleTagFilter(slug); }} key={id} icon={filter?.tags[slug] && } iconPos="right" >
{slug}
))}
{(isAllowed) => ( )}
{(isAllowed) => ( )} handlePopUpToggle("misc", isOpen)} >
{(isAllowed) => ( )} {(isAllowed) => ( )} {(isAllowed) => ( )}
{Object.keys(selectedSecrets).length} Selected
{(isAllowed) => ( )} {(isAllowed) => ( )}
{/* all the side triggers from actions like modals etc */} handlePopUpOpen("upgradePlan")} isOpen={popUp.addSecretImport.isOpen} onClose={() => handlePopUpClose("addSecretImport")} onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)} /> handlePopUpToggle("addDynamicSecret", isOpen)} projectSlug={projectSlug} environment={environment} secretPath={secretPath} /> handlePopUpToggle("addFolder", isOpen)} > handlePopUpToggle("bulkDeleteSecrets", isOpen)} onDeleteApproved={handleSecretBulkDelete} /> {subscription && ( handlePopUpToggle("upgradePlan", isOpen)} text={ subscription.slug === null ? "You can perform this action under an Enterprise license" : "You can perform this action if you switch to Infisical's Team plan" } /> )} ); }; ActionBar.displayName = "ActionBar";