Merge pull request #4681 from Infisical/nav-restructure

improvement(frontend): Implement new navigation structure
This commit is contained in:
Scott Wilson
2025-10-20 05:08:44 -07:00
committed by GitHub
125 changed files with 2781 additions and 3045 deletions
@@ -64,7 +64,7 @@ export const CommitDetailsPage = () => {
};
return (
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pt-2 pb-4 text-white">
<div className="mx-auto flex w-full max-w-8xl justify-center bg-bunker-800 pt-2 pb-4 text-white">
<div className="w-full max-w-[75vw]">
<ProjectPermissionCan
renderGuardBanner
@@ -29,6 +29,7 @@ import {
import { usePopUp } from "@app/hooks";
import { CommitWithChanges } from "@app/hooks/api/folderCommits";
import { useCommitRevert, useGetCommitDetails } from "@app/hooks/api/folderCommits/queries";
import { ProjectType } from "@app/hooks/api/projects/types";
import { CommitType } from "@app/hooks/api/types";
import { SecretVersionDiffView } from "../SecretVersionDiffView";
@@ -260,7 +261,7 @@ export const CommitDetailsTab = ({
Commit History
</Button>
<PageHeader
scope="project"
scope={ProjectType.SecretManager}
title={`${parsedCommitDetails.changes?.message}` || "No message"}
description={
<>
@@ -24,6 +24,7 @@ import {
} from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks";
import { useCommitRollback, useGetRollbackPreview } from "@app/hooks/api/folderCommits/queries";
import { ProjectType } from "@app/hooks/api/projects/types";
import { SecretVersionDiffView } from "../SecretVersionDiffView";
@@ -297,7 +298,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
};
return (
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pt-2 pb-4 text-white">
<div className="mx-auto flex w-full max-w-8xl justify-center bg-bunker-800 pt-2 pb-4 text-white">
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionCommitsActions.PerformRollback}
@@ -307,7 +308,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
<div className="h-full w-full">
<div>
<PageHeader
scope="project"
scope={ProjectType.SecretManager}
title={`Restore folder at commit ${selectedCommitId.substring(0, 8)}`}
description={`Will return all changes in this folder to how they appeared at the point of commit ${selectedCommitId.substring(0, 8)}. Any modifications made after this commit will be undone.`}
/>
@@ -1,4 +1,6 @@
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
@@ -9,6 +11,7 @@ import {
ProjectPermissionCommitsActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { CommitHistoryTab } from "./components/CommitHistoryTab";
@@ -46,10 +49,21 @@ export const CommitsPage = () => {
};
return (
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white">
<div className="w-full max-w-[75vw]">
<div className="mx-auto mb-4 flex h-full w-full max-w-8xl justify-center bg-bunker-800 text-white">
<div className="w-full">
<Link
to="/projects/secret-management/$projectId/secrets/$envSlug"
params={{
projectId: currentProject.id,
envSlug
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Secrets
</Link>
<PageHeader
scope="project"
scope={ProjectType.SecretManager}
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."
/>
@@ -15,7 +15,7 @@ export const IPAllowListPage = () => {
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6">
<div className="w-full max-w-8xl px-6">
<div className="my-6">
<p className="text-3xl font-medium text-gray-200">IP Allowlist</p>
<div />
@@ -90,11 +90,11 @@ export const IntegrationDetailsByIDPage = () => {
<meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} />
</Helmet>
<div className="mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex max-w-8xl flex-col justify-between bg-bunker-800 text-white">
{integration ? (
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.SecretManager}
title={`${integrationSlugNameMapping[integration.integration]} Integration`}
>
<DropdownMenu>
@@ -7,6 +7,7 @@ import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import {
@@ -45,21 +46,25 @@ export const IntegrationsListPage = () => {
<meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} />
</Helmet>
<div className="relative container mx-auto max-w-7xl pb-12 text-white">
<div className="relative mx-auto max-w-8xl pb-12 text-white">
<div className="mb-8">
<PageHeader
scope="project"
scope={ProjectType.SecretManager}
title="Integrations"
description="Manage integrations with third-party services."
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={IntegrationsListPageTabs.SecretSyncs}>Secret Syncs</Tab>
<Tab value={IntegrationsListPageTabs.NativeIntegrations}>Native Integrations</Tab>
<Tab value={IntegrationsListPageTabs.FrameworkIntegrations}>
<Tab variant="project" value={IntegrationsListPageTabs.SecretSyncs}>
Secret Syncs
</Tab>
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
Native Integrations
</Tab>
<Tab variant="project" value={IntegrationsListPageTabs.FrameworkIntegrations}>
Framework Integrations
</Tab>
<Tab value={IntegrationsListPageTabs.InfrastructureIntegrations}>
<Tab variant="project" value={IntegrationsListPageTabs.InfrastructureIntegrations}>
Infrastructure Integrations
</Tab>
</TabList>
@@ -93,7 +93,7 @@ import {
import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard/queries";
import { DashboardSecretsOrderBy, ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
@@ -929,11 +929,11 @@ export const OverviewPage = () => {
<meta property="og:title" content={String(t("dashboard.og-title"))} />
<meta name="og:description" content={String(t("dashboard.og-description"))} />
</Helmet>
<div className="relative mx-auto max-w-7xl text-mineshaft-50 dark:scheme-dark">
<div className="relative mx-auto max-w-8xl text-mineshaft-50 dark:scheme-dark">
<div className="flex w-full items-baseline justify-between">
<PageHeader
scope="project"
title="Secrets Overview"
scope={ProjectType.SecretManager}
title="Overview"
description={
<p className="text-md text-bunker-300">
Inject your secrets using
@@ -977,7 +977,7 @@ export const OverviewPage = () => {
}
/>
</div>
<div className="mt-4 flex items-center justify-between">
<div className="flex items-center justify-between">
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
<div className="flex flex-row items-center justify-center space-x-2">
{isTableFiltered && (
@@ -46,7 +46,7 @@ export const QuickSearchDynamicSecretItem = ({
</span>
<span className="text-xs text-mineshaft-400">
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "}
<Tooltip className="max-w-7xl" content={groupDynamicSecret.path}>
<Tooltip className="max-w-8xl" content={groupDynamicSecret.path}>
<span>{reverseTruncate(groupDynamicSecret.path)}</span>
</Tooltip>
</span>
@@ -32,7 +32,7 @@ export const QuickSearchFolderItem = ({ folderGroup, onClose }: Props) => {
>
<Td className="w-full whitespace-nowrap">
<FontAwesomeIcon className="text-yellow-700" icon={faFolder} />
<Tooltip content={groupFolder.path} className="max-w-7xl">
<Tooltip content={groupFolder.path} className="max-w-8xl">
<div className="ml-2 inline-block">{reverseTruncate(groupFolder.path)}</div>
</Tooltip>
</Td>
@@ -126,7 +126,7 @@ export const QuickSearchSecretItem = ({
</span>
<span className="text-xs text-mineshaft-400">
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "}
<Tooltip className="max-w-7xl" content={groupSecret.path}>
<Tooltip className="max-w-8xl" content={groupSecret.path}>
<span>{reverseTruncate(groupSecret.path ?? "")}</span>
</Tooltip>
</span>
@@ -42,7 +42,7 @@ export const QuickSearchSecretRotationItem = ({ secretRotationGroup, onClose }:
</span>
<span className="text-xs text-mineshaft-400">
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "}
<Tooltip className="max-w-7xl" content={groupSecretRotation.folder.path}>
<Tooltip className="max-w-8xl" content={groupSecretRotation.folder.path}>
<span>{reverseTruncate(groupSecretRotation.folder.path)}</span>
</Tooltip>
</span>
@@ -25,7 +25,7 @@ export const SecretTableResourceCount = ({
secretRotationCount = 0
}: Props) => {
return (
<div className="flex items-center gap-2 divide-x divide-mineshaft-500 text-sm text-mineshaft-400">
<div className="flex items-center divide-x divide-mineshaft-500 text-sm text-mineshaft-400 [&>*]:pr-2">
{importCount > 0 && (
<Tooltip
className="max-w-sm"
@@ -1,7 +1,7 @@
export const getExpandedRowStyle = (scrollOffset: number) => ({
marginLeft: scrollOffset,
width: "calc(100vw - 275px)", // accounts for sidebar and margin
maxWidth: "1270px" // largest width of table on ultra-wide
width: "calc(100vw - 110px)", // accounts for sidebar and margin
maxWidth: "1404px" // largest width of table on ultra-wide
});
type GetHeaderStyleParams = {
@@ -5,6 +5,7 @@ import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge";
import { useProject } from "@app/context";
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/projects/types";
import { AccessApprovalRequest } from "./components/AccessApprovalRequest";
import { ApprovalPolicyList } from "./components/ApprovalPolicyList";
@@ -38,27 +39,29 @@ export const SecretApprovalsPage = () => {
<meta property="og:title" content={String(t("approval.og-title"))} />
<meta name="og:description" content={String(t("approval.og-description"))} />
</Helmet>
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
<div className="mx-auto h-full w-full max-w-8xl bg-bunker-800 text-white">
<PageHeader
scope="project"
scope={ProjectType.SecretManager}
title="Approval Workflows"
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
/>
<Tabs defaultValue={defaultTab}>
<Tabs orientation="vertical" defaultValue={defaultTab}>
<TabList>
<Tab value={TabSection.SecretApprovalRequests}>
<Tab variant="project" value={TabSection.SecretApprovalRequests}>
Change Requests
{Boolean(secretApprovalReqCount?.open) && (
<Badge className="ml-2">{secretApprovalReqCount?.open}</Badge>
)}
</Tab>
<Tab value={TabSection.ResourceApprovalRequests}>
<Tab variant="project" value={TabSection.ResourceApprovalRequests}>
Access Requests
{Boolean(accessApprovalRequestCount?.pendingCount) && (
<Badge className="ml-2">{accessApprovalRequestCount?.pendingCount}</Badge>
)}
</Tab>
<Tab value={TabSection.Policies}>Policies</Tab>
<Tab variant="project" value={TabSection.Policies}>
Policies
</Tab>
</TabList>
<TabPanel value={TabSection.SecretApprovalRequests}>
<SecretApprovalRequest />
@@ -19,7 +19,6 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistance } from "date-fns";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -298,108 +297,132 @@ export const AccessApprovalRequest = ({
const isFiltered = Boolean(search || envFilter || requestedByFilter);
return (
<AnimatePresence mode="wait">
<motion.div
key="approval-changes-list"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
className="rounded-md text-gray-300"
>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-medium text-mineshaft-100">Access Requests</p>
<a
href="https://infisical.com/docs/documentation/platform/access-controls/access-requests"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-[0.32rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Request and review access to secrets in sensitive environments and folders
</p>
</div>
<Tooltip
content="To submit Access Requests, your project needs to create Access Request policies first."
isDisabled={policiesLoading || !!policies?.length}
>
<Button
onClick={() => {
if (subscription && !subscription?.secretApproval) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("requestAccess");
}}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={policiesLoading || !policies?.length}
<>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-medium text-mineshaft-100">Access Requests</p>
<a
href="https://infisical.com/docs/documentation/platform/access-controls/access-requests"
target="_blank"
rel="noopener noreferrer"
>
Request Access
</Button>
</Tooltip>
<div className="mt-[0.32rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Request and review access to secrets in sensitive environments and folders
</p>
</div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search approval requests by requesting user or environment..."
className="flex-1"
containerClassName="mb-4"
/>
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
<div
role="button"
tabIndex={0}
onClick={() => setStatusFilter("open")}
onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("open");
<Tooltip
content="To submit Access Requests, your project needs to create Access Request policies first."
isDisabled={policiesLoading || !!policies?.length}
>
<Button
onClick={() => {
if (subscription && !subscription?.secretApproval) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("requestAccess");
}}
className={twMerge(
"font-medium",
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
)}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={policiesLoading || !policies?.length}
>
<FontAwesomeIcon icon={faLock} className="mr-2" />
{!!requestCount && requestCount?.pendingCount} Pending
</div>
<div
className={twMerge(
"font-medium",
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
)}
role="button"
tabIndex={0}
onClick={() => setStatusFilter("close")}
onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("close");
}}
>
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{!!requestCount && requestCount.finalizedCount} Closed
</div>
<div className="flex grow justify-end space-x-8">
Request Access
</Button>
</Tooltip>
</div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search approval requests by requesting user or environment..."
className="flex-1"
containerClassName="mb-4"
/>
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
<div
role="button"
tabIndex={0}
onClick={() => setStatusFilter("open")}
onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("open");
}}
className={twMerge(
"font-medium",
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
)}
>
<FontAwesomeIcon icon={faLock} className="mr-2" />
{!!requestCount && requestCount?.pendingCount} Pending
</div>
<div
className={twMerge(
"font-medium",
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
)}
role="button"
tabIndex={0}
onClick={() => setStatusFilter("close")}
onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("close");
}}
>
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{!!requestCount && requestCount.finalizedCount} Closed
</div>
<div className="flex grow justify-end space-x-8">
<DropdownMenu>
<DropdownMenuTrigger>
<Button
variant="plain"
colorSchema="secondary"
className={envFilter ? "text-white" : "text-bunker-300"}
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
>
Environments
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={1}
className="max-h-80 thin-scrollbar overflow-y-auto"
>
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Select an Environment
</DropdownMenuLabel>
{currentProject?.environments.map(({ slug, name }) => (
<DropdownMenuItem
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
key={`request-filter-${slug}`}
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
{name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{!!permission.can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member) && (
<DropdownMenu>
<DropdownMenuTrigger>
<Button
variant="plain"
colorSchema="secondary"
className={envFilter ? "text-white" : "text-bunker-300"}
className={requestedByFilter ? "text-white" : "text-bunker-300"}
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
>
Environments
Requested By
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
@@ -408,210 +431,172 @@ export const AccessApprovalRequest = ({
className="max-h-80 thin-scrollbar overflow-y-auto"
>
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Select an Environment
Select Requesting User
</DropdownMenuLabel>
{currentProject?.environments.map(({ slug, name }) => (
{members?.map(({ user: membershipUser, id }) => (
<DropdownMenuItem
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
key={`request-filter-${slug}`}
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />}
onClick={() =>
setRequestedByFilter((state) =>
state === membershipUser.id ? undefined : membershipUser.id
)
}
key={`request-filter-member-${id}`}
icon={
requestedByFilter === membershipUser.id && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right"
>
{name}
{membershipUser.username}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{!!permission.can(
ProjectPermissionMemberActions.Read,
ProjectPermissionSub.Member
) && (
<DropdownMenu>
<DropdownMenuTrigger>
<Button
variant="plain"
colorSchema="secondary"
className={requestedByFilter ? "text-white" : "text-bunker-300"}
rightIcon={
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
}
>
Requested By
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={1}
className="max-h-80 thin-scrollbar overflow-y-auto"
>
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Select Requesting User
</DropdownMenuLabel>
{members?.map(({ user: membershipUser, id }) => (
<DropdownMenuItem
onClick={() =>
setRequestedByFilter((state) =>
state === membershipUser.id ? undefined : membershipUser.id
)
}
key={`request-filter-member-${id}`}
icon={
requestedByFilter === membershipUser.id && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right"
>
{membershipUser.username}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
{filteredRequests?.length === 0 && !isFiltered && (
<div className="py-12">
<EmptyState
title={`No ${statusFilter === "open" ? "Pending" : "Closed"} Access Requests`}
/>
</div>
)}
{Boolean(!filteredRequests?.length && isFiltered && !areRequestsPending) && (
<div className="py-12">
<EmptyState title="No Requests Match Filters" icon={faSearch} />
</div>
)}
{!!filteredRequests?.length &&
filteredRequests?.slice(offset, perPage * page).map((request) => {
const details = generateRequestDetails(request);
return (
<div
key={request.id}
className="flex w-full cursor-pointer border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700 aria-disabled:opacity-80"
role="button"
tabIndex={0}
onClick={() => handleSelectRequest(request)}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
handleSelectRequest(request);
}
}}
>
<div className="flex w-full items-center justify-between">
<div className="flex w-full flex-col justify-between">
<div className="mb-1 flex w-full items-center">
<FontAwesomeIcon
icon={faLock}
size="xs"
className="mr-1.5 text-mineshaft-300"
/>
{generateRequestText(request)}
</div>
<div className="flex items-center justify-between">
<div className="text-xs leading-3 text-gray-500">
{membersGroupById?.[request.requestedByUserId]?.user && (
<>
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
ago by{" "}
{membersGroupById?.[request.requestedByUserId]?.user?.firstName}{" "}
{membersGroupById?.[request.requestedByUserId]?.user?.lastName} (
{membersGroupById?.[request.requestedByUserId]?.user?.email}){" "}
</>
)}
</div>
</div>
</div>
<div className="flex items-center gap-3">
{request.requestedByUserId === user.id && (
<div className="flex items-center gap-1.5 text-xs whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faUser} size="sm" />
<span>Requested By You</span>
</div>
)}
<Tooltip content={details.displayData.tooltipContent}>
<div>
<Badge
className="flex items-center gap-1.5 whitespace-nowrap"
variant={details.displayData.type}
>
{details.displayData.icon && (
<FontAwesomeIcon icon={details.displayData.icon} />
)}
<span>{details.displayData.label}</span>
</Badge>
</div>
</Tooltip>
</div>
</div>
</div>
);
})}
{Boolean(filteredRequests.length) && (
<Pagination
className="border-none"
count={filteredRequests.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</div>
{!!policies && (
<RequestAccessModal
policies={policies}
isOpen={popUp.requestAccess.isOpen}
onOpenChange={() => {
queryClient.invalidateQueries({
queryKey: accessApprovalKeys.getAccessApprovalRequests(
projectSlug,
envFilter,
requestedByFilter
)
});
handlePopUpClose("requestAccess");
}}
/>
)}
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
{filteredRequests?.length === 0 && !isFiltered && (
<div className="py-12">
<EmptyState
title={`No ${statusFilter === "open" ? "Pending" : "Closed"} Access Requests`}
/>
</div>
)}
{Boolean(!filteredRequests?.length && isFiltered && !areRequestsPending) && (
<div className="py-12">
<EmptyState title="No Requests Match Filters" icon={faSearch} />
</div>
)}
{!!filteredRequests?.length &&
filteredRequests?.slice(offset, perPage * page).map((request) => {
const details = generateRequestDetails(request);
{!!selectedRequest && (
<ReviewAccessRequestModal
selectedEnvSlug={envFilter}
policies={policies || []}
selectedRequester={requestedByFilter}
projectSlug={projectSlug}
request={selectedRequest}
members={members || []}
isOpen={popUp.reviewRequest.isOpen}
onOpenChange={() => {
handlePopUpClose("reviewRequest");
setSelectedRequest(null);
refetchRequests();
}}
onUpdate={(request) => {
// scott: this isn't ideal but our current use of state makes this complicated...
// we shouldn't be using state like this...
handleSelectRequest({
...selectedRequest,
isTemporary: request.isTemporary,
temporaryRange: request.temporaryRange,
reviewers: []
});
}}
canBypass={generateRequestDetails(selectedRequest).canBypass}
/>
)}
<UpgradePlanModal
text="You need to upgrade your plan to access this feature"
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={() => handlePopUpClose("upgradePlan")}
return (
<div
key={request.id}
className="flex w-full cursor-pointer border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700 aria-disabled:opacity-80"
role="button"
tabIndex={0}
onClick={() => handleSelectRequest(request)}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
handleSelectRequest(request);
}
}}
>
<div className="flex w-full items-center justify-between">
<div className="flex w-full flex-col justify-between">
<div className="mb-1 flex w-full items-center">
<FontAwesomeIcon
icon={faLock}
size="xs"
className="mr-1.5 text-mineshaft-300"
/>
{generateRequestText(request)}
</div>
<div className="flex items-center justify-between">
<div className="text-xs leading-3 text-gray-500">
{membersGroupById?.[request.requestedByUserId]?.user && (
<>
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
ago by{" "}
{membersGroupById?.[request.requestedByUserId]?.user?.firstName}{" "}
{membersGroupById?.[request.requestedByUserId]?.user?.lastName} (
{membersGroupById?.[request.requestedByUserId]?.user?.email}){" "}
</>
)}
</div>
</div>
</div>
<div className="flex items-center gap-3">
{request.requestedByUserId === user.id && (
<div className="flex items-center gap-1.5 text-xs whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faUser} size="sm" />
<span>Requested By You</span>
</div>
)}
<Tooltip content={details.displayData.tooltipContent}>
<div>
<Badge
className="flex items-center gap-1.5 whitespace-nowrap"
variant={details.displayData.type}
>
{details.displayData.icon && (
<FontAwesomeIcon icon={details.displayData.icon} />
)}
<span>{details.displayData.label}</span>
</Badge>
</div>
</Tooltip>
</div>
</div>
</div>
);
})}
{Boolean(filteredRequests.length) && (
<Pagination
className="border-none"
count={filteredRequests.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</div>
{!!policies && (
<RequestAccessModal
policies={policies}
isOpen={popUp.requestAccess.isOpen}
onOpenChange={() => {
queryClient.invalidateQueries({
queryKey: accessApprovalKeys.getAccessApprovalRequests(
projectSlug,
envFilter,
requestedByFilter
)
});
handlePopUpClose("requestAccess");
}}
/>
</motion.div>
</AnimatePresence>
)}
{!!selectedRequest && (
<ReviewAccessRequestModal
selectedEnvSlug={envFilter}
policies={policies || []}
selectedRequester={requestedByFilter}
projectSlug={projectSlug}
request={selectedRequest}
members={members || []}
isOpen={popUp.reviewRequest.isOpen}
onOpenChange={() => {
handlePopUpClose("reviewRequest");
setSelectedRequest(null);
refetchRequests();
}}
onUpdate={(request) => {
// scott: this isn't ideal but our current use of state makes this complicated...
// we shouldn't be using state like this...
handleSelectRequest({
...selectedRequest,
isTemporary: request.isTemporary,
temporaryRange: request.temporaryRange,
reviewers: []
});
}}
canBypass={generateRequestDetails(selectedRequest).canBypass}
/>
)}
<UpgradePlanModal
text="You need to upgrade your plan to access this feature"
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={() => handlePopUpClose("upgradePlan")}
/>
</>
);
};
@@ -12,7 +12,6 @@ import {
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -240,262 +239,253 @@ export const ApprovalPolicyList = ({ projectId }: IProps) => {
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
return (
<AnimatePresence mode="wait">
<motion.div
key="approval-changes-list"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
className="rounded-md text-gray-300"
>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-medium text-mineshaft-100">Policies</p>
<a
href="https://infisical.com/docs/documentation/platform/pr-workflows"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-[0.32rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Implement granular policies for access requests and secrets management
</p>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SecretApproval}
>
{(isAllowed) => (
<Button
onClick={() => {
if (subscription && !subscription?.secretApproval) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("policyForm");
}}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
Create Policy
</Button>
)}
</ProjectPermissionCan>
</div>
<div className="mb-4 flex items-center gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search policies by name, type, environment or secret path..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter findings"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="max-h-[70vh] thin-scrollbar overflow-y-auto"
align="end"
<>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-medium text-mineshaft-100">Policies</p>
<a
href="https://infisical.com/docs/documentation/platform/pr-workflows"
target="_blank"
rel="noopener noreferrer"
>
<DropdownMenuLabel>Policy Type</DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
setFilters((prev) => ({
...prev,
type: null
}))
}
icon={!filters && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
All
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
setFilters((prev) => ({
...prev,
type: PolicyType.AccessPolicy
}))
}
icon={
filters.type === PolicyType.AccessPolicy && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right"
>
Access Policy
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
setFilters((prev) => ({
...prev,
type: PolicyType.ChangePolicy
}))
}
icon={
filters.type === PolicyType.ChangePolicy && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right"
>
Change Policy
</DropdownMenuItem>
<DropdownMenuLabel>Environment</DropdownMenuLabel>
{currentProject.environments.map((env) => (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
environmentIds: prev.environmentIds.includes(env.id)
? prev.environmentIds.filter((i) => i !== env.id)
: [...prev.environmentIds, env.id]
}));
}}
key={env.id}
icon={
filters.environmentIds.includes(env.id) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<span className="capitalize">{env.name}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={getClassName(PolicyOrderBy.Name)}
ariaLabel="sort"
onClick={() => handleSort(PolicyOrderBy.Name)}
>
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Name)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Environment
<IconButton
variant="plain"
className={getClassName(PolicyOrderBy.Environment)}
ariaLabel="sort"
onClick={() => handleSort(PolicyOrderBy.Environment)}
>
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Environment)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Secret Path
<IconButton
variant="plain"
className={getClassName(PolicyOrderBy.SecretPath)}
ariaLabel="sort"
onClick={() => handleSort(PolicyOrderBy.SecretPath)}
>
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.SecretPath)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Type
<IconButton
variant="plain"
className={getClassName(PolicyOrderBy.Type)}
ariaLabel="sort"
onClick={() => handleSort(PolicyOrderBy.Type)}
>
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Type)} />
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPoliciesLoading && (
<TableSkeleton
columns={5}
innerKey="secret-policies"
className="bg-mineshaft-700"
<div className="mt-[0.32rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
)}
{!isPoliciesLoading && !policies?.length && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No Policies Found" icon={faFileShield} />
</Td>
</Tr>
)}
{!!currentProject &&
filteredPolicies
?.slice(offset, perPage * page)
.map((policy) => (
<ApprovalPolicyRow
policy={policy}
key={policy.id}
members={members}
groups={groups}
onEdit={() => handlePopUpOpen("policyForm", policy)}
onDelete={() => handlePopUpOpen("deletePolicy", policy)}
/>
))}
</TBody>
</Table>
{Boolean(!filteredPolicies.length && policies.length && !isPoliciesLoading) && (
<EmptyState title="No Policies Match Search" icon={faSearch} />
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Implement granular policies for access requests and secrets management
</p>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SecretApproval}
>
{(isAllowed) => (
<Button
onClick={() => {
if (subscription && !subscription?.secretApproval) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("policyForm");
}}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
Create Policy
</Button>
)}
{Boolean(filteredPolicies.length) && (
<Pagination
count={filteredPolicies.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
</TableContainer>
</ProjectPermissionCan>
</div>
</motion.div>
<div className="mb-4 flex items-center gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search policies by name, type, environment or secret path..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter findings"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="max-h-[70vh] thin-scrollbar overflow-y-auto"
align="end"
>
<DropdownMenuLabel>Policy Type</DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
setFilters((prev) => ({
...prev,
type: null
}))
}
icon={!filters && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
All
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
setFilters((prev) => ({
...prev,
type: PolicyType.AccessPolicy
}))
}
icon={
filters.type === PolicyType.AccessPolicy && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right"
>
Access Policy
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
setFilters((prev) => ({
...prev,
type: PolicyType.ChangePolicy
}))
}
icon={
filters.type === PolicyType.ChangePolicy && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right"
>
Change Policy
</DropdownMenuItem>
<DropdownMenuLabel>Environment</DropdownMenuLabel>
{currentProject.environments.map((env) => (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
environmentIds: prev.environmentIds.includes(env.id)
? prev.environmentIds.filter((i) => i !== env.id)
: [...prev.environmentIds, env.id]
}));
}}
key={env.id}
icon={
filters.environmentIds.includes(env.id) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<span className="capitalize">{env.name}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={getClassName(PolicyOrderBy.Name)}
ariaLabel="sort"
onClick={() => handleSort(PolicyOrderBy.Name)}
>
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Name)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Environment
<IconButton
variant="plain"
className={getClassName(PolicyOrderBy.Environment)}
ariaLabel="sort"
onClick={() => handleSort(PolicyOrderBy.Environment)}
>
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Environment)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Secret Path
<IconButton
variant="plain"
className={getClassName(PolicyOrderBy.SecretPath)}
ariaLabel="sort"
onClick={() => handleSort(PolicyOrderBy.SecretPath)}
>
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.SecretPath)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Type
<IconButton
variant="plain"
className={getClassName(PolicyOrderBy.Type)}
ariaLabel="sort"
onClick={() => handleSort(PolicyOrderBy.Type)}
>
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Type)} />
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPoliciesLoading && (
<TableSkeleton
columns={5}
innerKey="secret-policies"
className="bg-mineshaft-700"
/>
)}
{!isPoliciesLoading && !policies?.length && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No Policies Found" icon={faFileShield} />
</Td>
</Tr>
)}
{!!currentProject &&
filteredPolicies
?.slice(offset, perPage * page)
.map((policy) => (
<ApprovalPolicyRow
policy={policy}
key={policy.id}
members={members}
groups={groups}
onEdit={() => handlePopUpOpen("policyForm", policy)}
onDelete={() => handlePopUpOpen("deletePolicy", policy)}
/>
))}
</TBody>
</Table>
{Boolean(!filteredPolicies.length && policies.length && !isPoliciesLoading) && (
<EmptyState title="No Policies Match Search" icon={faSearch} />
)}
{Boolean(filteredPolicies.length) && (
<Pagination
count={filteredPolicies.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
</TableContainer>
</div>
<AccessPolicyForm
projectId={currentProject.id}
projectSlug={currentProject.slug}
@@ -517,6 +507,6 @@ export const ApprovalPolicyList = ({ projectId }: IProps) => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
/>
</AnimatePresence>
</>
);
};
@@ -14,7 +14,6 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSearch } from "@tanstack/react-router";
import { format, formatDistance } from "date-fns";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import {
@@ -131,284 +130,250 @@ export const SecretApprovalRequest = () => {
const isFiltered = Boolean(searchFilter || envFilter || committerFilter);
return (
<AnimatePresence mode="wait">
{isSecretApprovalScreen ? (
<motion.div
key="approval-changes-details"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<SecretApprovalRequestChanges
approvalRequestId={selectedApprovalId || ""}
onGoBack={handleGoBackSecretRequestDetail}
/>
</motion.div>
) : (
<motion.div
key="approval-changes-list"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
className="rounded-md text-gray-300"
>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-medium text-mineshaft-100">Change Requests</p>
<a
href="https://infisical.com/docs/documentation/platform/pr-workflows"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-[0.32rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">Review pending and closed change requests</p>
</div>
</div>
<Input
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search change requests by author, environment or policy path..."
className="flex-1"
containerClassName="mb-4"
/>
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
<div
role="button"
tabIndex={0}
onClick={() => setStatusFilter("open")}
onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("open");
}}
className={twMerge(
"font-medium",
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
)}
>
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
</div>
<div
className={twMerge(
"font-medium",
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
)}
role="button"
tabIndex={0}
onClick={() => setStatusFilter("close")}
onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("close");
}}
>
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount.closed} Closed
</div>
<div className="flex grow justify-end space-x-8">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="plain"
colorSchema="secondary"
className={envFilter ? "text-white" : "text-bunker-300"}
rightIcon={
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
}
>
Environments
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={1}
className="max-h-80 thin-scrollbar overflow-y-auto"
>
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Select an Environment
</DropdownMenuLabel>
{currentProject?.environments.map(({ slug, name }) => (
<DropdownMenuItem
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
key={`request-filter-${slug}`}
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
{name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{!!permission.can(
ProjectPermissionMemberActions.Read,
ProjectPermissionSub.Member
) && (
<DropdownMenu>
<DropdownMenuTrigger>
<Button
variant="plain"
colorSchema="secondary"
className={committerFilter ? "text-white" : "text-bunker-300"}
rightIcon={
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
}
>
Author
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={1}
className="max-h-80 thin-scrollbar overflow-y-auto"
>
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Select an Author
</DropdownMenuLabel>
{members?.map(({ user, id }) => (
<DropdownMenuItem
onClick={() =>
setCommitterFilter((state) => (state === user.id ? undefined : user.id))
}
key={`request-filter-member-${id}`}
icon={
committerFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />
}
iconPos="right"
>
{user.username}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
{isRequestListEmpty && !isFiltered && (
<div className="py-12">
<EmptyState
title={`No ${statusFilter === "open" ? "Open" : "Closed"} Change Requests`}
/>
</div>
)}
{secretApprovalRequests.map((secretApproval) => {
const {
id: reqId,
commits,
createdAt,
reviewers,
status,
committerUser,
hasMerged,
updatedAt
} = secretApproval;
const isReviewed = reviewers.some(
({ status: reviewStatus, userId }) =>
userId === userSession.id && reviewStatus === ApprovalStatus.APPROVED
);
return (
<div
key={reqId}
className="flex border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setSelectedApprovalId(secretApproval.id)}
onKeyDown={(evt) => {
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
}}
>
<div className="flex flex-col">
<div className="mb-1 text-sm">
<FontAwesomeIcon
icon={faCodeBranch}
size="sm"
className="mr-1.5 text-mineshaft-300"
/>
{secretApproval.isReplicated
? `${commits.length} secret pending import`
: generateCommitText(commits)}
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
</div>
<span className="text-xs leading-3 text-gray-500">
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
{committerUser ? (
<>
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
{committerUser?.email})
</>
) : (
<span className="text-gray-600">Deleted User</span>
)}
{!isReviewed && status === "open" && " - Review required"}
</span>
</div>
{status === "close" && (
<Tooltip
content={updatedAt ? format(new Date(updatedAt), "M/dd/yyyy h:mm a") : ""}
>
<div className="my-auto ml-auto">
<Badge
variant={hasMerged ? "success" : "danger"}
className="flex h-min items-center gap-1"
>
<FontAwesomeIcon icon={hasMerged ? faCodeMerge : faXmark} />
{hasMerged ? "Merged" : "Rejected"}
</Badge>
</div>
</Tooltip>
)}
</div>
);
})}
{Boolean(
!secretApprovalRequests.length && isFiltered && !isApprovalRequestLoading
) && (
<div className="py-12">
<EmptyState title="No Requests Match Filters" icon={faSearch} />
</div>
)}
{Boolean(totalApprovalCount) && (
<Pagination
className="border-none"
count={totalApprovalCount}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
return isSecretApprovalScreen ? (
<SecretApprovalRequestChanges
approvalRequestId={selectedApprovalId || ""}
onGoBack={handleGoBackSecretRequestDetail}
/>
) : (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-medium text-mineshaft-100">Change Requests</p>
<a
href="https://infisical.com/docs/documentation/platform/pr-workflows"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-[0.32rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
)}
{isApprovalRequestLoading && (
<div>
{Array.apply(0, Array(3)).map((_x, index) => (
<div
key={`approval-request-loading-${index + 1}`}
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
>
<div className="mb-2 flex items-center">
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
<Skeleton className="w-1/4 bg-mineshaft-600" />
</div>
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
))}
</div>
</a>
</div>
<p className="text-sm text-bunker-300">Review pending and closed change requests</p>
</div>
</div>
<Input
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search change requests by author, environment or policy path..."
className="flex-1"
containerClassName="mb-4"
/>
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
<div
role="button"
tabIndex={0}
onClick={() => setStatusFilter("open")}
onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("open");
}}
className={twMerge(
"font-medium",
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
)}
>
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
</div>
<div
className={twMerge(
"font-medium",
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
)}
role="button"
tabIndex={0}
onClick={() => setStatusFilter("close")}
onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("close");
}}
>
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount.closed} Closed
</div>
<div className="flex grow justify-end space-x-8">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="plain"
colorSchema="secondary"
className={envFilter ? "text-white" : "text-bunker-300"}
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
>
Environments
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={1}
className="max-h-80 thin-scrollbar overflow-y-auto"
>
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Select an Environment
</DropdownMenuLabel>
{currentProject?.environments.map(({ slug, name }) => (
<DropdownMenuItem
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
key={`request-filter-${slug}`}
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
{name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{!!permission.can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member) && (
<DropdownMenu>
<DropdownMenuTrigger>
<Button
variant="plain"
colorSchema="secondary"
className={committerFilter ? "text-white" : "text-bunker-300"}
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
>
Author
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={1}
className="max-h-80 thin-scrollbar overflow-y-auto"
>
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Select an Author
</DropdownMenuLabel>
{members?.map(({ user, id }) => (
<DropdownMenuItem
onClick={() =>
setCommitterFilter((state) => (state === user.id ? undefined : user.id))
}
key={`request-filter-member-${id}`}
icon={committerFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
{user.username}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
{isRequestListEmpty && !isFiltered && (
<div className="py-12">
<EmptyState
title={`No ${statusFilter === "open" ? "Open" : "Closed"} Change Requests`}
/>
</div>
)}
{secretApprovalRequests.map((secretApproval) => {
const {
id: reqId,
commits,
createdAt,
reviewers,
status,
committerUser,
hasMerged,
updatedAt
} = secretApproval;
const isReviewed = reviewers.some(
({ status: reviewStatus, userId }) =>
userId === userSession.id && reviewStatus === ApprovalStatus.APPROVED
);
return (
<div
key={reqId}
className="flex border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setSelectedApprovalId(secretApproval.id)}
onKeyDown={(evt) => {
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
}}
>
<div className="flex flex-col">
<div className="mb-1 text-sm">
<FontAwesomeIcon
icon={faCodeBranch}
size="sm"
className="mr-1.5 text-mineshaft-300"
/>
{secretApproval.isReplicated
? `${commits.length} secret pending import`
: generateCommitText(commits)}
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
</div>
<span className="text-xs leading-3 text-gray-500">
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
{committerUser ? (
<>
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
{committerUser?.email})
</>
) : (
<span className="text-gray-600">Deleted User</span>
)}
{!isReviewed && status === "open" && " - Review required"}
</span>
</div>
{status === "close" && (
<Tooltip content={updatedAt ? format(new Date(updatedAt), "M/dd/yyyy h:mm a") : ""}>
<div className="my-auto ml-auto">
<Badge
variant={hasMerged ? "success" : "danger"}
className="flex h-min items-center gap-1"
>
<FontAwesomeIcon icon={hasMerged ? faCodeMerge : faXmark} />
{hasMerged ? "Merged" : "Rejected"}
</Badge>
</div>
</Tooltip>
)}
</div>
);
})}
{Boolean(!secretApprovalRequests.length && isFiltered && !isApprovalRequestLoading) && (
<div className="py-12">
<EmptyState title="No Requests Match Filters" icon={faSearch} />
</div>
</motion.div>
)}
</AnimatePresence>
)}
{Boolean(totalApprovalCount) && (
<Pagination
className="border-none"
count={totalApprovalCount}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{isApprovalRequestLoading && (
<div>
{Array.apply(0, Array(3)).map((_x, index) => (
<div
key={`approval-request-loading-${index + 1}`}
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
>
<div className="mb-2 flex items-center">
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
<Skeleton className="w-1/4 bg-mineshaft-600" />
</div>
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
))}
</div>
)}
</div>
</div>
);
};
@@ -228,7 +228,7 @@ export const SecretApprovalRequestChanges = ({ approvalRequestId, onGoBack }: Pr
return (
<div className="flex flex-col space-x-6 lg:flex-row">
<div className="flex-1 lg:max-w-[calc(100%-17rem)]">
<div className="sticky -top-8 z-20 flex items-center space-x-4 bg-bunker-800 pt-2 pb-6">
<div className="sticky -top-10 z-20 flex items-center space-x-4 bg-bunker-800 pt-2 pb-6">
<IconButton variant="outline_bg" ariaLabel="go-back" onClick={onGoBack}>
<FontAwesomeIcon icon={faArrowLeft} />
</IconButton>
@@ -3,9 +3,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
import { faArrowDown, faArrowUp, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import {
faArrowDown,
faArrowUp,
faChevronLeft,
faInfoCircle
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { Link, useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
@@ -51,7 +56,7 @@ import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
import { queryClient } from "@app/hooks/api/reactQuery";
import { PendingAction } from "@app/hooks/api/secretFolders/types";
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
@@ -760,9 +765,19 @@ const Page = () => {
);
return (
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:scheme-dark">
<div className="mx-auto flex max-w-8xl flex-col text-mineshaft-50 dark:scheme-dark">
<Link
to="/projects/secret-management/$projectId/overview"
params={{
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Secrets Overview
</Link>
<PageHeader
scope="project"
scope={ProjectType.SecretManager}
title="Secrets Management"
description={
<p className="text-md text-bunker-300">
@@ -383,7 +383,7 @@ export const CommitForm: React.FC<CommitFormProps> = ({
</div>
}
subTitle="Write a commit message and review the changes you're about to save."
className="max-h-[90vh] max-w-[95%] md:max-w-7xl"
className="max-h-[90vh] max-w-[95%] md:max-w-8xl"
>
<div className="space-y-6">
{/* Changes List */}
@@ -52,6 +52,7 @@ import {
useGetSecretRotations,
useRestartSecretRotation
} from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/projects/types";
import { TSecretRotationProviderTemplate } from "@app/hooks/api/secretRotation/types";
import { CreateRotationForm } from "@app/pages/secret-manager/SecretRotationPage/components/CreateRotationForm";
@@ -145,9 +146,9 @@ const Page = () => {
};
return (
<div className="container mx-auto w-full max-w-7xl bg-bunker-800 text-white">
<div className="container mx-auto w-full max-w-8xl bg-bunker-800 text-white">
<PageHeader
scope="project"
scope={ProjectType.SecretManager}
title="Secret Rotation"
description="Stop manually rotating secrets and automate credential rotation."
>
@@ -73,7 +73,7 @@ const PageContent = () => {
return (
<>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<Button
variant="link"
type="submit"
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useProject } from "@app/context";
import { ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
import { EncryptionTab } from "./components/EncryptionTab";
@@ -40,18 +40,18 @@ export const SettingsPage = () => {
<Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<div className="w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.SecretManager}
title="Settings"
description="Configure your secret manager's encryption, environments, webhooks and other configurations."
/>
<Tabs defaultValue={tabs[0].key}>
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList>
{tabs
.filter((el) => !el.isHidden)
.map((tab) => (
<Tab value={tab.key} key={tab.key}>
<Tab variant="project" value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
@@ -40,7 +40,7 @@ export const AutoCapitalizationSection = () => {
<p className="mb-3 text-xl font-medium">{t("settings.project.enforce-capitalization")}</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<div className="w-max">
<div>
<Checkbox
id="autoCapitalization"
isDisabled={!isAllowed}
@@ -48,6 +48,7 @@ export const AutoCapitalizationSection = () => {
onCheckedChange={(state) => {
handleToggleCapitalizationToggle(state as boolean);
}}
allowMultilineLabel
>
{t("settings.project.enforce-capitalization-description")}
</Checkbox>