mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 03:50:10 +00:00
style: implement new navigation
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
@@ -297,7 +297,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}
|
||||
|
||||
@@ -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";
|
||||
@@ -46,8 +48,19 @@ 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"
|
||||
title="Commits"
|
||||
|
||||
@@ -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 />
|
||||
|
||||
+2
-2
@@ -90,9 +90,9 @@ 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"
|
||||
title={`${integrationSlugNameMapping[integration.integration]} Integration`}
|
||||
|
||||
@@ -45,21 +45,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"
|
||||
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>
|
||||
|
||||
@@ -911,7 +911,7 @@ 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"
|
||||
@@ -959,7 +959,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 && (
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -38,27 +38,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"
|
||||
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 />
|
||||
|
||||
+269
-284
@@ -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")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+244
-254
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+240
-275
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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";
|
||||
@@ -760,7 +765,17 @@ 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"
|
||||
title="Secrets Management"
|
||||
|
||||
+1
-1
@@ -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 */}
|
||||
|
||||
@@ -145,7 +145,7 @@ 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"
|
||||
title="Secret Rotation"
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user