diff --git a/frontend/src/hooks/api/folderCommits/queries.tsx b/frontend/src/hooks/api/folderCommits/queries.tsx index 8d0883c5b..1bca7ce9a 100644 --- a/frontend/src/hooks/api/folderCommits/queries.tsx +++ b/frontend/src/hooks/api/folderCommits/queries.tsx @@ -1,8 +1,9 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { format } from "date-fns"; import { apiRequest } from "@app/config/request"; -import { CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types"; +import { Commit, CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types"; export const commitKeys = { count: ({ @@ -242,7 +243,6 @@ export const useGetFolderCommitHistory = ({ workspaceId, environment, directory, - offset = 0, limit = 20, search, sort = "desc" @@ -250,22 +250,33 @@ export const useGetFolderCommitHistory = ({ workspaceId: string; environment: string; directory: string; - offset?: number; limit?: number; search?: string; sort?: "asc" | "desc"; }) => { - return useQuery({ - queryKey: [ - commitKeys.history({ workspaceId, environment, directory }), - offset, - limit, - search, - sort - ], - queryFn: () => - fetchFolderCommitHistory(workspaceId, environment, directory, offset, limit, search, sort), - enabled: Boolean(workspaceId && environment) + return useInfiniteQuery({ + initialPageParam: 0, + queryKey: [commitKeys.history({ workspaceId, environment, directory }), limit, search, sort], + queryFn: ({ pageParam }) => + fetchFolderCommitHistory(workspaceId, environment, directory, pageParam, limit, search, sort), + enabled: Boolean(workspaceId && environment), + select: (data) => { + return (data?.pages ?? []) + ?.map((page) => page.commits) + .flat() + .reduce( + (acc, commit) => { + const date = format(new Date(commit.createdAt), "MMM d, yyyy"); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push(commit); + return acc; + }, + {} as Record + ); + }, + getNextPageParam: (lastPage, pages) => (lastPage.hasMore ? pages.length * limit : undefined) }); }; diff --git a/frontend/src/hooks/api/folderCommits/types.ts b/frontend/src/hooks/api/folderCommits/types.ts index 878e3224d..a5f4e6df6 100644 --- a/frontend/src/hooks/api/folderCommits/types.ts +++ b/frontend/src/hooks/api/folderCommits/types.ts @@ -62,3 +62,16 @@ export type RollbackPreview = { folderPath: string; changes: RollbackChange[]; }; + +interface CommitActorMetadata { + email?: string; + name?: string; +} + +export interface Commit { + id: string; + message: string; + createdAt: string; + actorType: string; + actorMetadata?: CommitActorMetadata; +} diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx index c403f9f3f..483d00861 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/components/CommitDetailsTab/CommitDetailsTab.tsx @@ -1,5 +1,10 @@ import { useEffect, useState } from "react"; -import { faAngleDown } from "@fortawesome/free-solid-svg-icons"; +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"; @@ -7,12 +12,14 @@ 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, - IconButton, - Spinner + EmptyState, + PageHeader } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { @@ -108,25 +115,25 @@ export const CommitDetailsTab = ({ // If no commit is selected or data is loading, show appropriate message if (!selectedCommitId) { return ( -
-

Select a commit to view details

-
+ + + ); } if (isLoading) { - return ( -
- -
- ); + return ; } if (!commitDetails) { return ( -
-

No details found for this commit

-
+ + + ); } @@ -138,9 +145,11 @@ export const CommitDetailsTab = ({ } catch (error) { console.error("Failed to parse commit details:", error); return ( -
-

Error parsing commit details

-
+ + + ); } @@ -223,13 +232,12 @@ export const CommitDetailsTab = ({ // Render an item from the merged list const renderMergedItem = (item: MergedItem): JSX.Element => { return ( -
- toggleItemCollapsed(id)} - /> -
+ toggleItemCollapsed(id)} + /> ); }; @@ -240,114 +248,104 @@ export const CommitDetailsTab = ({ "Unknown"; return ( -
-
-
-
-
-
-

- {parsedCommitDetails.changes?.message || "No message"} -

-
-
-
-

- Commited by - {actorDisplay} - on - - {formatDisplayDate( - parsedCommitDetails.changes?.createdAt || new Date().toISOString() - )} - - {parsedCommitDetails.changes?.isLatest && ( - (Latest) - )} -

-
-
-
- - {(isAllowed) => ( - - + + + Commited by {actorDisplay} on{" "} + {formatDisplayDate(parsedCommitDetails.changes?.createdAt || new Date().toISOString())} + {parsedCommitDetails.changes?.isLatest && ( + (Latest) + )} + + } + > + + {(isAllowed) => ( + + + + + + {!parsedCommitDetails.changes.isLatest && ( + goToRollbackPreview()} > - -

Restore Options

- -
-
- - {!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 - -
+
+
+ + 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)) - ) : ( -
-

No changed items found

-
- )} -
+
+
+ {sortedChangedItems.length > 0 ? ( + sortedChangedItems.map((item) => renderMergedItem(item)) + ) : ( + + )}
- -
+ ); }; diff --git a/frontend/src/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView/SecretVersionDiffView.tsx b/frontend/src/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView/SecretVersionDiffView.tsx index 7744d3626..a1d058ea3 100644 --- a/frontend/src/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView/SecretVersionDiffView.tsx +++ b/frontend/src/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView/SecretVersionDiffView.tsx @@ -2,6 +2,7 @@ import { useCallback, useRef, useState } from "react"; import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; export interface Version { id?: string; @@ -225,15 +226,12 @@ const renderJsonWithDiffs = ( const getLineClass = (different: boolean) => { if (!different) return "flex"; - return isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"; + return isOldVersion + ? "flex bg-red-500/50 rounded-sm text-red-300" + : "flex bg-green-500/50 rounded-sm text-green-300"; }; - const getHighlightClass = (different: boolean) => { - if (!different) return ""; - return isOldVersion ? "bg-red-900 rounded px-1" : "bg-green-900 rounded px-1"; - }; - - const prefix = isDifferent ? (isOldVersion ? "-" : "+") : " "; + const prefix = isDifferent ? (isOldVersion ? " -" : " +") : " "; const keyDisplay = keyName ? `"${keyName}": ` : ""; const comma = !isLastItem ? "," : ""; @@ -255,8 +253,8 @@ const renderJsonWithDiffs = (
{prefix}
{indent} - {keyName && {keyDisplay}} - {valueDisplay} + {keyName && {keyDisplay}} + {valueDisplay} {comma}
@@ -269,8 +267,8 @@ const renderJsonWithDiffs = (
{prefix}
{indent} - {keyName && {keyDisplay}} - [] + {keyName && {keyDisplay}} + [] {comma}
@@ -283,8 +281,8 @@ const renderJsonWithDiffs = (
{prefix}
{indent} - {keyName && {keyDisplay}} - {"{}"} + {keyName && {keyDisplay}} + {"{}"} {comma}
@@ -320,16 +318,12 @@ const renderJsonWithDiffs = (
- {isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "} + {isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
{indent} - {keyName && ( - - {keyDisplay} - - )} - [ + {keyName && {keyDisplay}} + [
@@ -357,11 +351,11 @@ const renderJsonWithDiffs = (
- {isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "} + {isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
{indent} - ] + ] {comma}
@@ -376,16 +370,12 @@ const renderJsonWithDiffs = (
- {isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "} + {isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
{indent} - {keyName && ( - - {keyDisplay} - - )} - {"{"} + {keyName && {keyDisplay}} + {"{"}
@@ -414,11 +404,11 @@ const renderJsonWithDiffs = (
- {isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "} + {isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
{indent} - {"}"} + {"}"} {comma}
@@ -527,7 +517,7 @@ export const SecretVersionDiffView = ({ } oldVersionContent = ( -
+
{renderJsonWithDiffs( cleanOldVersion, diffPaths, @@ -543,7 +533,7 @@ export const SecretVersionDiffView = ({
); newVersionContent = ( -
+
{renderJsonWithDiffs( cleanNewVersion, diffPaths, @@ -583,19 +573,19 @@ export const SecretVersionDiffView = ({ if (item.isDeleted) { textStyle = "line-through text-red-300"; changeBadge = ( - + {isSecret ? "Secret" : "Folder"} Deleted ); } else if (item.isAdded) { changeBadge = ( - + {isSecret ? "Secret" : "Folder"} Added ); } else if (item.isUpdated) { changeBadge = ( - + {isSecret ? "Secret" : "Folder"} Updated ); @@ -615,32 +605,34 @@ export const SecretVersionDiffView = ({ tabIndex={0} aria-expanded={!collapsed} > -
- {key} +
+

{key}

{changeBadge}
- +
); }; return ( -
+
{showHeader && renderHeader()} - {!collapsed && ( -
-
+
+
{oldVersionContent}
- +
{newVersionContent}
diff --git a/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx b/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx index 406a1b112..591c03d4d 100644 --- a/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx +++ b/frontend/src/pages/secret-manager/CommitsPage/CommitsPage.tsx @@ -52,7 +52,7 @@ export const CommitsPage = () => { title="Commits" description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states." /> - +

Secret Snapshots have been officially renamed to Commits. Going forward, all secret changes will be tracked as Commits. If you made changes before this update, you can diff --git a/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx b/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx index 8f529ba86..58191f389 100644 --- a/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx +++ b/frontend/src/pages/secret-manager/CommitsPage/components/CommitHistoryTab/CommitHistoryTab.tsx @@ -1,29 +1,17 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { faArrowDownWideShort, faArrowUpWideShort, + faCodeCommit, faCopy, faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { format, formatDistanceToNow } from "date-fns"; +import { formatDistanceToNow } from "date-fns"; -import { Button, Input, Spinner } from "@app/components/v2"; +import { Button, ContentLoader, EmptyState, IconButton, Input } from "@app/components/v2"; import { CopyButton } from "@app/components/v2/CopyButton"; -import { useGetFolderCommitHistory } from "@app/hooks/api/folderCommits"; - -interface CommitActorMetadata { - email?: string; - name?: string; -} - -interface Commit { - id: string; - message: string; - createdAt: string; - actorType: string; - actorMetadata?: CommitActorMetadata; -} +import { Commit, useGetFolderCommitHistory } from "@app/hooks/api/folderCommits"; const formatTimeAgo = (timestamp: string): string => { return formatDistanceToNow(new Date(timestamp), { addSuffix: true }); @@ -40,58 +28,40 @@ const CommitItem = ({ onSelectCommit: (commitId: string, tab: string) => void; }) => { return ( -

-
-
-
-
- -
-

- - {commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType} -

committed

- - -

-
-
-
- - -
-
+
+ ); }; @@ -108,24 +78,16 @@ const DateGroup = ({ onSelectCommit: (commitId: string, tab: string) => void; }) => { return ( -
-
-
-
-
-
-

Commits on {date}

+
+
+ +

Commits on {date}

-
-
+
{commits.map((commit) => ( -
-
- -
-
+ ))}
@@ -147,10 +109,8 @@ export const CommitHistoryTab = ({ const [searchTerm, setSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); - const [offset, setOffset] = useState(0); - const [allCommits, setAllCommits] = useState([]); const debounceTimeoutRef = useRef(); - const limit = 5; + const limit = 10; // Debounce search term useEffect(() => { @@ -170,55 +130,20 @@ export const CommitHistoryTab = ({ }, [searchTerm]); const { - data: response, + data: groupedCommits, isLoading, - isFetching + fetchNextPage, + isFetchingNextPage, + hasNextPage } = useGetFolderCommitHistory({ workspaceId: projectId, environment, directory: secretPath, - offset, limit, search: debouncedSearchTerm, sort: sortDirection }); - const commits = response?.commits || []; - const hasMore = response?.hasMore || false; - - // Reset accumulated commits when search or sort changes - useEffect(() => { - setAllCommits([]); - setOffset(0); - }, [debouncedSearchTerm, sortDirection]); - - // Accumulate commits instead of replacing them - useEffect(() => { - if (commits.length > 0) { - if (offset === 0) { - // First load or after search/sort change - replace all commits - setAllCommits(commits); - } else { - // Subsequent loads - append new commits - setAllCommits((prev) => [...prev, ...commits]); - } - } - }, [commits, offset]); - - const groupedCommits = useMemo(() => { - return allCommits.reduce( - (acc, commit) => { - const date = format(new Date(commit.createdAt), "MMM d, yyyy"); - if (!acc[date]) { - acc[date] = []; - } - acc[date].push(commit); - return acc; - }, - {} as Record - ); - }, [allCommits]); - const handleSort = useCallback(() => { setSortDirection((prev) => (prev === "desc" ? "asc" : "desc")); }, []); @@ -227,50 +152,39 @@ export const CommitHistoryTab = ({ setSearchTerm(value); }, []); - const loadMoreCommits = useCallback(() => { - if (hasMore && !isFetching) { - setOffset((prev) => prev + limit); - } - }, [hasMore, isFetching, limit]); - return ( -
+
+

Commit History

} placeholder="Search commits..." - className="h-10 w-full rounded-md border-transparent bg-zinc-800 pl-9 pr-3 text-sm text-white placeholder-gray-400 focus:border-gray-600 focus:ring-primary-500/20" onChange={(e) => handleSearch(e.target.value)} value={searchTerm} aria-label="Search commits" /> -
-
- +
- - {isLoading && offset === 0 ? ( -
- -
+ {isLoading ? ( + ) : ( -
- {Object.keys(groupedCommits).length > 0 ? ( +
+ {groupedCommits && Object.keys(groupedCommits).length > 0 ? ( <> {Object.entries(groupedCommits).map(([date, dateCommits]) => ( ) : ( -
-
+ )} - - {hasMore && ( + {hasNextPage && (
)}