import { useEffect, useState } from "react"; import { faAngleDown, faChevronLeft, faCodeCommit, faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu"; import { useSearch } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, ContentLoader, DeleteActionModal, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, EmptyState, PageHeader } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; import { CommitWithChanges } from "@app/hooks/api/folderCommits"; import { useCommitRevert, useGetCommitDetails } from "@app/hooks/api/folderCommits/queries"; import { CommitType } from "@app/hooks/api/types"; import { SecretVersionDiffView } from "../SecretVersionDiffView"; import { MergedItem } from "./types"; const formatDisplayDate = (dateString: string): string => { try { const date = new Date(dateString); const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "numeric", hour12: true }; return new Intl.DateTimeFormat("en-US", options).format(date); } catch { return dateString; } }; export const CommitDetailsTab = ({ selectedCommitId, workspaceId, envSlug, goBackToHistory, goToRollbackPreview }: { selectedCommitId: string; workspaceId: string; envSlug: string; goBackToHistory: () => void; goToRollbackPreview: () => void; }): JSX.Element => { // State for tracking collapsed items (empty by default means all are expanded) const [collapsedItems, setCollapsedItems] = useState>({}); const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "revertChanges" ] as const); const { data: commitDetails, isLoading } = useGetCommitDetails(workspaceId, selectedCommitId); const routerQueryParams: { secretPath?: string } = useSearch({ from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id }); const secretPath = (routerQueryParams.secretPath as string) || "/"; const { mutateAsync: revert } = useCommitRevert({ commitId: selectedCommitId, projectId: workspaceId, environment: envSlug, directory: secretPath }); useEffect(() => { setCollapsedItems({}); }, [selectedCommitId]); const toggleItemCollapsed = (itemId: string): void => { setCollapsedItems((prev) => ({ ...prev, [itemId]: !prev[itemId] })); }; const handleRevertChanges = async (): Promise => { const response = await revert(); if (!response.success) { createNotification({ type: "error", text: response.message }); return; } createNotification({ type: "success", text: response.message }); handlePopUpClose("revertChanges"); goBackToHistory(); }; // If no commit is selected or data is loading, show appropriate message if (!selectedCommitId) { return ( ); } if (isLoading) { return ; } if (!commitDetails) { return ( ); } // Parse the commit details if it's a string let parsedCommitDetails: CommitWithChanges; try { parsedCommitDetails = typeof commitDetails === "string" ? JSON.parse(commitDetails) : commitDetails; } catch (error) { console.error("Failed to parse commit details:", error); return ( ); } // Get all changes from the commit const commitChanges = parsedCommitDetails.changes?.changes || []; // Separate changes by type const addedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && !c.isUpdate); const updatedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && c.isUpdate); const deletedChanges = commitChanges.filter((c) => c.changeType === CommitType.DELETE); // Create merged item list from changes only const changedItems: MergedItem[] = []; // Add items from added changes addedChanges.forEach((change) => { changedItems.push({ id: change.id, type: change.secretVersionId || change.secretKey ? "secret" : "folder", versionId: change.secretVersionId || change.id, folderName: change.folderName, folderVersion: change.folderVersion, secretKey: change.secretKey, secretVersion: change.secretVersion, isAdded: true, versions: change.versions, changeId: change.id }); }); // Add items from updated changes updatedChanges.forEach((change) => { changedItems.push({ id: change.id, type: change.secretVersionId || change.secretKey ? "secret" : "folder", versionId: change.secretVersionId || change.id, folderName: change.folderName, folderVersion: change.folderVersion, secretKey: change.secretKey, secretVersion: change.secretVersion, isUpdated: true, versions: change.versions, changeId: change.id }); }); // Add deleted items deletedChanges.forEach((change) => { changedItems.push({ id: change.id, type: change.secretVersionId || change.secretKey ? "secret" : "folder", secretKey: change.secretKey, folderName: change.folderName, secretVersion: change.secretVersion, folderVersion: change.folderVersion, isDeleted: true, versions: change.versions, changeId: change.id }); }); // Sort items: deleted first, then folders, then secrets, all alphabetically const sortedChangedItems = [...changedItems].sort((a, b) => { // First sort deleted items to the top if (a.isDeleted !== b.isDeleted) { return a.isDeleted ? -1 : 1; } // Then sort by type (folders before secrets) if (a.type !== b.type) { return a.type === "folder" ? -1 : 1; } // Finally sort alphabetically by name const aName = a.type === "folder" ? a.folderName || "" : a.secretKey || ""; const bName = b.type === "folder" ? b.folderName || "" : b.secretKey || ""; return aName.localeCompare(bName); }); // Render an item from the merged list const renderMergedItem = (item: MergedItem): JSX.Element => { return ( toggleItemCollapsed(id)} /> ); }; // Format actor display const actorDisplay = parsedCommitDetails.changes?.actorMetadata?.name || parsedCommitDetails.changes?.actorType || "Unknown"; return ( <> Commited by {actorDisplay} on{" "} {formatDisplayDate(parsedCommitDetails.changes?.createdAt || new Date().toISOString())} {parsedCommitDetails.changes?.isLatest && ( (Latest) )} } > {(isAllowed) => ( {!parsedCommitDetails.changes.isLatest && ( goToRollbackPreview()} >
Roll back to this commit Return this folder to its exact state at the time of this commit, discarding all other changes made after it
)} handlePopUpOpen("revertChanges")} >
Revert changes Will restore to the previous version of affected resources
)}

Commit Changes

{sortedChangedItems.length > 0 ? ( sortedChangedItems.map((item) => renderMergedItem(item)) ) : ( )}
handlePopUpToggle("revertChanges", isOpen)} onDeleteApproved={handleRevertChanges} buttonText="Yes, revert changes" /> ); };