mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 06:45:28 +00:00
PIT: UI views
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { CommitHistoryTab } from "./components/CommitHistoryTab";
|
||||
|
||||
export const CommitsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const envSlug = useParams({
|
||||
from: ROUTE_PATHS.SecretManager.CommitsPage.id,
|
||||
select: (el) => el.environment
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const navigate = useNavigate();
|
||||
const folderId = useParams({
|
||||
from: ROUTE_PATHS.SecretManager.CommitsPage.id,
|
||||
select: (el) => el.folderId
|
||||
});
|
||||
const routerQueryParams: { secretPath?: string } = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.CommitsPage.id
|
||||
});
|
||||
|
||||
const secretPath = routerQueryParams?.secretPath || "/";
|
||||
|
||||
const handleSelectCommit = (commitId: string) => {
|
||||
navigate({
|
||||
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId/$commitId` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
folderId,
|
||||
environment: envSlug,
|
||||
commitId
|
||||
},
|
||||
search: (query) => ({
|
||||
...query,
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Commit History" })}</title>
|
||||
</Helmet>
|
||||
<div className="w-full max-w-[75vw]">
|
||||
<PageHeader
|
||||
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."
|
||||
/>
|
||||
<CommitHistoryTab
|
||||
onSelectCommit={handleSelectCommit}
|
||||
projectId={currentWorkspace.id}
|
||||
environment={envSlug}
|
||||
secretPath={secretPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
faArrowDownWideShort,
|
||||
faArrowUpWideShort,
|
||||
faCopy,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
|
||||
import { Button, Input, Spinner } 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;
|
||||
}
|
||||
|
||||
const formatTimeAgo = (timestamp: string): string => {
|
||||
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Commit Item component for displaying a single commit
|
||||
*/
|
||||
const CommitItem = ({
|
||||
commit,
|
||||
onSelectCommit
|
||||
}: {
|
||||
commit: Commit;
|
||||
onSelectCommit: (commitId: string, tab: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="border-b border-zinc-800 last:border-b-0">
|
||||
<div className="px-4 py-4 transition-colors duration-200 hover:bg-zinc-800">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-white hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectCommit(commit.id, "tab-commit-details");
|
||||
}}
|
||||
>
|
||||
{commit.message}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-white-400 mt-2 flex flex-wrap items-center gap-4 text-sm">
|
||||
<span className="flex items-center">
|
||||
{commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType}
|
||||
<p className="ml-1 mr-1">committed</p>
|
||||
<time dateTime={commit.createdAt}>{formatTimeAgo(commit.createdAt)}</time>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center sm:mt-0">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-white hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectCommit(commit.id, "tab-commit-details");
|
||||
}}
|
||||
>
|
||||
<code className="text-white-400 px-3 py-1 font-mono text-sm">
|
||||
{commit.id?.substring(0, 11)}
|
||||
</code>
|
||||
</Button>
|
||||
<CopyButton
|
||||
value={commit.id}
|
||||
name={commit.id}
|
||||
size="sm"
|
||||
variant="plain"
|
||||
color="text-mineshaft-400"
|
||||
icon={faCopy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Date Group component for displaying commits grouped by date
|
||||
*/
|
||||
const DateGroup = ({
|
||||
date,
|
||||
commits,
|
||||
onSelectCommit
|
||||
}: {
|
||||
date: string;
|
||||
commits: Commit[];
|
||||
onSelectCommit: (commitId: string, tab: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="mb-8 last:mb-0 last:pb-2">
|
||||
<div className="mb-4 flex items-center">
|
||||
<div className="relative mr-3 flex h-6 w-6 items-center justify-center">
|
||||
<div className="z-10 h-3 w-3 rounded-full border-2 border-mineshaft-600 bg-bunker-800" />
|
||||
<div className="absolute left-0 right-0 top-1/2 h-0.5 -translate-y-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
<h2 className="text-sm text-white">Commits on {date}</h2>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute bottom-0 left-3 top-0 w-0.5 bg-mineshaft-600" />
|
||||
<div className="ml-10">
|
||||
{commits.map((commit) => (
|
||||
<div key={commit.id} className="relative mb-3 pb-1">
|
||||
<div className="overflow-hidden rounded-md border border-solid border-mineshaft-600">
|
||||
<CommitItem commit={commit} onSelectCommit={onSelectCommit} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommitHistoryTab = ({
|
||||
onSelectCommit,
|
||||
projectId,
|
||||
environment,
|
||||
secretPath
|
||||
}: {
|
||||
onSelectCommit: (commitId: string, tab: string) => void;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
||||
const [visibleCommits, setVisibleCommits] = useState(10);
|
||||
|
||||
const { data: commits, isLoading } = useGetFolderCommitHistory({
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
directory: secretPath
|
||||
});
|
||||
|
||||
const filteredCommits = useMemo(() => {
|
||||
if (!commits?.length) return [];
|
||||
|
||||
return commits.filter(
|
||||
(commit) =>
|
||||
commit.id?.includes(searchTerm) ||
|
||||
commit.message?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
commit.actorMetadata?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}, [commits, searchTerm]);
|
||||
|
||||
const sortedCommits = useMemo(() => {
|
||||
if (!filteredCommits?.length) return [];
|
||||
|
||||
return [...filteredCommits].sort((a, b) => {
|
||||
const dateA = new Date(a.createdAt).getTime();
|
||||
const dateB = new Date(b.createdAt).getTime();
|
||||
return sortDirection === "desc" ? dateB - dateA : dateA - dateB;
|
||||
});
|
||||
}, [filteredCommits, sortDirection]);
|
||||
|
||||
const displayedCommits = useMemo(() => {
|
||||
return sortedCommits.slice(0, visibleCommits);
|
||||
}, [sortedCommits, visibleCommits]);
|
||||
const groupedCommits = useMemo(() => {
|
||||
return displayedCommits.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<string, Commit[]>
|
||||
);
|
||||
}, [displayedCommits]);
|
||||
|
||||
const handleSort = useCallback(() => {
|
||||
setSortDirection((prev) => (prev === "desc" ? "asc" : "desc"));
|
||||
}, []);
|
||||
|
||||
const loadMoreCommits = useCallback(() => {
|
||||
setVisibleCommits((prev) => prev + 10);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Spinner size="lg" aria-label="Loading commits" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasMoreCommits = sortedCommits.length > visibleCommits;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex flex-col sm:flex-row sm:justify-end">
|
||||
<div className="flex w-full flex-wrap items-center gap-2">
|
||||
<div className="relative flex-grow">
|
||||
<Input
|
||||
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) => setSearchTerm(e.target.value)}
|
||||
aria-label="Search commits"
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400">
|
||||
<FontAwesomeIcon icon={faSearch} aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="md"
|
||||
className="flex h-10 items-center justify-center gap-2 rounded-md bg-zinc-800 px-4 py-2 text-sm text-white transition-colors duration-200 hover:bg-zinc-700 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
onClick={handleSort}
|
||||
aria-label={`Sort by date ${sortDirection === "desc" ? "ascending" : "descending"}`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={sortDirection === "desc" ? faArrowDownWideShort : faArrowUpWideShort}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{Object.keys(groupedCommits).length > 0 ? (
|
||||
<>
|
||||
{Object.entries(groupedCommits).map(([date, dateCommits]) => (
|
||||
<DateGroup
|
||||
key={date}
|
||||
date={date}
|
||||
commits={dateCommits}
|
||||
onSelectCommit={onSelectCommit}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-white-400 flex min-h-40 flex-col items-center justify-center rounded-lg bg-zinc-900 py-8 text-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faSearch}
|
||||
className="text-white-500 mb-3 text-3xl"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p>No matching commits found. Try a different search term.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasMoreCommits && (
|
||||
<div className="flex justify-center pb-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="md"
|
||||
className="rounded-md bg-zinc-900 px-6 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
onClick={loadMoreCommits}
|
||||
aria-label="Load more commits"
|
||||
>
|
||||
Load more commits
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CommitHistoryTab } from "./CommitHistoryTab";
|
||||
@@ -0,0 +1 @@
|
||||
export { CommitsPage } from "./CommitsPage";
|
||||
@@ -0,0 +1,71 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretDashboardPathBreadcrumb } from "@app/components/navigation/SecretDashboardPathBreadcrumb";
|
||||
import { BreadcrumbTypes } from "@app/components/v2";
|
||||
|
||||
import { CommitsPage } from "./CommitsPage";
|
||||
|
||||
const CommitsPageQueryParamsSchema = z.object({
|
||||
secretPath: z.string().catch("/")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/"
|
||||
)({
|
||||
component: CommitsPage,
|
||||
validateSearch: zodValidator(CommitsPageQueryParamsSchema),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ secretPath: "/" })]
|
||||
},
|
||||
beforeLoad: ({ context, params, search }) => {
|
||||
const secretPathSegments = search.secretPath.split("/").filter(Boolean);
|
||||
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
type: BreadcrumbTypes.Dropdown,
|
||||
label:
|
||||
context.project.environments.find((el) => el.slug === params.environment)?.name || "",
|
||||
dropdownTitle: "Environments",
|
||||
links: context.project.environments.map((el) => ({
|
||||
label: el.name,
|
||||
link: linkOptions({
|
||||
to: "/secret-manager/$projectId/secrets/$envSlug",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
envSlug: el.slug
|
||||
}
|
||||
})
|
||||
}))
|
||||
},
|
||||
...secretPathSegments.map((_, index) => ({
|
||||
type: BreadcrumbTypes.Component,
|
||||
component: () => (
|
||||
<SecretDashboardPathBreadcrumb
|
||||
secretPathSegments={secretPathSegments}
|
||||
selectedPathSegmentIndex={index}
|
||||
environmentSlug={params.environment}
|
||||
projectId={params.projectId}
|
||||
disableCopy
|
||||
/>
|
||||
)
|
||||
})),
|
||||
{
|
||||
label: "Commits",
|
||||
link: linkOptions({
|
||||
to: "/secret-manager/$projectId/commits/$environment/$folderId",
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
environment: params.environment,
|
||||
folderId: params.folderId
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user