style: implement new navigation

This commit is contained in:
Scott Wilson
2025-10-15 18:06:34 -07:00
parent d00c9e422f
commit 4af3f6fc3f
116 changed files with 2644 additions and 2952 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ const badgeVariants = cva(
namespace: namespace:
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1 w-min whitespace-nowrap", "bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1 w-min whitespace-nowrap",
project: project:
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap", "bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 w-min gap-x-1 whitespace-nowrap",
instance: instance:
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap" "bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap"
} }
@@ -120,10 +120,16 @@ export type TBreadcrumbFormat =
icon?: ReactNode; icon?: ReactNode;
}; };
const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => ( const BreadcrumbContainer = ({
<div className="mx-auto max-w-7xl text-white"> breadcrumbs,
<Breadcrumb> className
<BreadcrumbList> }: {
breadcrumbs: TBreadcrumbFormat[];
className?: string;
}) => (
<div className={twMerge("mx-auto max-w-8xl overflow-hidden text-white", className)}>
<Breadcrumb className="overflow-hidden">
<BreadcrumbList className="overflow-hidden">
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => { {(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => {
const isNotLastCrumb = index + 1 !== breadcrumbs.length; const isNotLastCrumb = index + 1 !== breadcrumbs.length;
const BreadcrumbSegment = isNotLastCrumb ? BreadcrumbLink : BreadcrumbPage; const BreadcrumbSegment = isNotLastCrumb ? BreadcrumbLink : BreadcrumbPage;
@@ -165,8 +171,8 @@ const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[]
const Component = el.component; const Component = el.component;
return ( return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}> <React.Fragment key={`breadcrumb-group-${index + 1}`}>
<BreadcrumbItem> <BreadcrumbItem className="overflow-hidden">
<BreadcrumbSegment> <BreadcrumbSegment className="overflow-hidden">
<Component /> <Component />
</BreadcrumbSegment> </BreadcrumbSegment>
</BreadcrumbItem> </BreadcrumbItem>
@@ -22,7 +22,7 @@ const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: Ico
}; };
export const PageHeader = ({ title, description, children, className, scope }: Props) => ( export const PageHeader = ({ title, description, children, className, scope }: Props) => (
<div className={twMerge("mb-4 w-full", className)}> <div className={twMerge("mb-10 w-full border-b border-mineshaft-500 pb-10", className)}>
<div className="flex w-full justify-between"> <div className="flex w-full justify-between">
<div className="mr-4 flex w-full items-center"> <div className="mr-4 flex w-full items-center">
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1> <h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
+32 -5
View File
@@ -1,10 +1,19 @@
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as TabsPrimitive from "@radix-ui/react-tabs"; import * as TabsPrimitive from "@radix-ui/react-tabs";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export type TabsProps = TabsPrimitive.TabsProps; export type TabsProps = TabsPrimitive.TabsProps;
export const Tabs = ({ className, children, ...props }: TabsProps) => ( export const Tabs = ({ className, children, ...props }: TabsProps) => (
<TabsPrimitive.Root className={twMerge("flex flex-col", className)} {...props}> <TabsPrimitive.Root
className={twMerge(
"flex",
className,
props.orientation === "vertical" ? "flex-col lg:flex-row lg:gap-x-12" : "flex-col"
)}
{...props}
>
{children} {children}
</TabsPrimitive.Root> </TabsPrimitive.Root>
); );
@@ -13,7 +22,11 @@ export type TabListProps = TabsPrimitive.TabsListProps;
export const TabList = ({ className, children, ...props }: TabListProps) => ( export const TabList = ({ className, children, ...props }: TabListProps) => (
<TabsPrimitive.List <TabsPrimitive.List
className={twMerge("flex shrink-0 border-b-2 border-mineshaft-800", className)} className={twMerge(
"no-scrollbar flex shrink-0 overflow-auto border-b-2 border-mineshaft-800",
"data-[orientation=vertical]:lg:flex-col data-[orientation=vertical]:lg:items-start data-[orientation=vertical]:lg:gap-y-6 data-[orientation=vertical]:lg:border-b-0",
className
)}
{...props} {...props}
> >
{children} {children}
@@ -26,18 +39,29 @@ export const Tab = ({
className, className,
children, children,
variant = "project", variant = "project",
icon,
...props ...props
}: TabProps & { variant?: "project" | "namespace" | "org" }) => ( }: TabProps & {
icon?: IconDefinition;
variant?: "project" | "namespace" | "org" | "instance";
}) => (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
className={twMerge( className={twMerge(
"flex h-10 items-center justify-center px-3 text-sm font-medium text-mineshaft-400 transition-all select-none first:rounded-tl-md last:rounded-tr-md hover:text-mineshaft-200 data-[state=active]:border-b data-[state=active]:text-white", "flex h-10 cursor-pointer items-center justify-center border-transparent",
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-400 transition-all select-none",
"data-[orientation=vertical]:lg:h-5 data-[orientation=vertical]:lg:border-b-0 data-[orientation=vertical]:lg:border-l",
"border-b hover:text-mineshaft-200",
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",
"hover:border-mineshaft-400",
variant === "project" && "data-[state=active]:border-primary", variant === "project" && "data-[state=active]:border-primary",
variant === "namespace" && "data-[state=active]:border-namespace-v1", variant === "namespace" && "data-[state=active]:border-namespace-v1",
variant === "org" && "data-[state=active]:border-org-v1", variant === "org" && "data-[state=active]:border-org-v1",
variant === "instance" && "data-[state=active]:border-mineshaft-300",
className className
)} )}
{...props} {...props}
> >
{icon && <FontAwesomeIcon icon={icon} className="mr-2" size="xs" />}
{children} {children}
</TabsPrimitive.Trigger> </TabsPrimitive.Trigger>
); );
@@ -46,7 +70,10 @@ export type TabPanelProps = TabsPrimitive.TabsContentProps;
export const TabPanel = ({ className, children, ...props }: TabPanelProps) => ( export const TabPanel = ({ className, children, ...props }: TabPanelProps) => (
<TabsPrimitive.Content <TabsPrimitive.Content
className={twMerge("grow rounded-br-md rounded-bl-md py-5 outline-hidden", className)} className={twMerge(
"grow rounded-br-md rounded-bl-md py-5 outline-hidden data-[orientation=vertical]:lg:py-0",
className
)}
{...props} {...props}
> >
{children} {children}
+1 -1
View File
@@ -42,7 +42,7 @@
--font-inter: "Inter", sans-serif; --font-inter: "Inter", sans-serif;
--color-org-v1: #30B3FF; --color-org-v1: #30B3FF;
--color-namespace-v1: #96ff59; --color-namespace-v1: #96ff59;
--max-width-8xl: 88rem; /* 1408px */
/* Primary */ /* Primary */
--color-primary-50: #fffff5; --color-primary-50: #fffff5;
--color-primary-100: #fcfce8; --color-primary-100: #fcfce8;
@@ -12,7 +12,7 @@ import { RedisBanner } from "@app/layouts/OrganizationLayout/components/RedisBan
import { SmtpBanner } from "@app/layouts/OrganizationLayout/components/SmtpBanner"; import { SmtpBanner } from "@app/layouts/OrganizationLayout/components/SmtpBanner";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner"; import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { AdminSidebar } from "./Sidebar"; import { AdminNavBar } from "./AdminNavBar";
export const AdminLayout = () => { export const AdminLayout = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -33,9 +33,9 @@ export const AdminLayout = () => {
{!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />} {!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />}
{!isLoading && subscription.auditLogs && <AuditLogBanner />} {!isLoading && subscription.auditLogs && <AuditLogBanner />}
{!window.isSecureContext && <InsecureConnectionBanner />} {!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex grow flex-col overflow-y-hidden md:flex-row"> <div className="flex grow flex-col overflow-y-hidden">
<AdminSidebar /> <AdminNavBar />
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-4 pt-8 pb-4 dark:scheme-dark"> <div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark">
<Outlet /> <Outlet />
</div> </div>
</div> </div>
@@ -0,0 +1,102 @@
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faArrowLeft,
faBuilding,
faCog,
faDatabase,
faGlobe,
faKey,
faLock,
faPlug,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Tab, TabList, Tabs } from "@app/components/v2";
const generalTabs = [
{
label: "General",
icon: faCog,
link: "/admin/"
},
{
label: "Encryption",
icon: faLock,
link: "/admin/encryption"
},
{
label: "Authentication",
icon: faCheckCircle,
link: "/admin/authentication"
},
{
label: "Integrations",
icon: faPlug,
link: "/admin/integrations"
},
{
label: "Caching",
icon: faDatabase,
link: "/admin/caching"
},
{
label: "Environment Variables",
icon: faKey,
link: "/admin/environment"
}
];
const othersTabs = [
{
label: "Access Controls",
icon: faUserTie,
link: "/admin/access-management"
},
{
label: "Resource Overview",
icon: faBuilding,
link: "/admin/resources/overview"
}
];
export const AdminNavBar = () => {
const matchRoute = useMatchRoute();
return (
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="px-4"
>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link to="/organization/projects">
<Tab value="back" className="flex gap-x-2">
<FontAwesomeIcon icon={faGlobe} />
<FontAwesomeIcon icon={faArrowLeft} />
</Tab>
</Link>
{[...generalTabs, ...othersTabs].map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<Tab variant="instance" value={isActive ? "selected" : ""}>
{tab.label}
</Tab>
</Link>
);
})}
</TabList>
</Tabs>
</nav>
</motion.div>
</div>
);
};
@@ -1,126 +0,0 @@
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faBuilding,
faChevronLeft,
faCog,
faDatabase,
faKey,
faLock,
faPlug,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
const generalTabs = [
{
label: "General",
icon: faCog,
link: "/admin/"
},
{
label: "Encryption",
icon: faLock,
link: "/admin/encryption"
},
{
label: "Authentication",
icon: faCheckCircle,
link: "/admin/authentication"
},
{
label: "Integrations",
icon: faPlug,
link: "/admin/integrations"
},
{
label: "Caching",
icon: faDatabase,
link: "/admin/caching"
},
{
label: "Environment Variables",
icon: faKey,
link: "/admin/environment"
}
];
const othersTabs = [
{
label: "Access Controls",
icon: faUserTie,
link: "/admin/access-management"
},
{
label: "Resource Overview",
icon: faBuilding,
link: "/admin/resources/overview"
}
];
export const AdminSidebar = () => {
const matchRoute = useMatchRoute();
return (
<aside className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:scheme-dark">
<div className="flex-1">
<Menu>
<MenuGroup title="Configuration">
{generalTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={tab.icon} />
</div>
{tab.label}
</div>
</MenuItem>
</Link>
);
})}
</MenuGroup>
<MenuGroup title="Others">
{othersTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={tab.icon} />
</div>
{tab.label}
</div>
</MenuItem>
</Link>
);
})}
</MenuGroup>
</Menu>
</div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faChevronLeft}
flip="vertical"
/>
}
>
Back to Organization
</MenuItem>
</Link>
</Menu>
</nav>
</aside>
);
};
+71 -122
View File
@@ -1,9 +1,9 @@
import { faBook, faCog, faCube, faHome, faLock, faUsers } from "@fortawesome/free-solid-svg-icons"; import { faArrowLeft, faGlobe } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router"; import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; import { Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context"; import { useProject, useProjectPermission } from "@app/context";
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
@@ -12,138 +12,87 @@ export const KmsLayout = () => {
const { currentProject } = useProject(); const { currentProject } = useProject();
const { assumedPrivilegeDetails } = useProjectPermission(); const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
return ( return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row"> <div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: -150 }} exit={{ x: -150 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" className="px-4"
> >
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark"> <nav className="w-full">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white"> <Tabs value="selected">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="unlock" /> <TabList className="border-b-0">
KMS
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/kms/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCube} />
</div>
Overview
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/kmip"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faLock} />
</div>
KMIP
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/kms/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects"> <Link to="/organization/projects">
<MenuItem <Tab value="back" className="flex gap-x-2">
variant="project" <FontAwesomeIcon icon={faGlobe} />
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300" <FontAwesomeIcon icon={faArrowLeft} />
leftIcon={ </Tab>
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link> </Link>
</Menu> <Link
</div> to="/projects/kms/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Overview</Tab>}
</Link>
<Link
to="/projects/kms/$projectId/kmip"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>KMIP</Tab>}
</Link>
<Link
to="/projects/kms/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/kms/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/kms/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav> </nav>
</motion.div> </motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8"> </div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />} {assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet /> <div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
</div> <Outlet />
</div> </div>
</div> </div>
); );
@@ -13,7 +13,7 @@ import { useFetchServerStatus } from "@app/hooks/api";
import { AuditLogBanner } from "./components/AuditLogBanner"; import { AuditLogBanner } from "./components/AuditLogBanner";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner"; import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { Navbar } from "./components/NavBar"; import { Navbar } from "./components/NavBar";
import { OrgSidebar } from "./components/OrgSidebar"; import { OrgNavBar } from "./components/OrgNavBar";
import { RedisBanner } from "./components/RedisBanner"; import { RedisBanner } from "./components/RedisBanner";
import { SmtpBanner } from "./components/SmtpBanner"; import { SmtpBanner } from "./components/SmtpBanner";
@@ -41,16 +41,16 @@ export const OrganizationLayout = () => {
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`} className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
> >
<Navbar /> <Navbar />
{!isLoading && !serverDetails?.redisConfigured && <RedisBanner />} <div className="flex grow flex-col overflow-y-hidden">
{!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />} <OrgNavBar isHidden={isInsideProject} />
{!isLoading && subscription.auditLogs && <AuditLogBanner />} {!isLoading && !isInsideProject && !serverDetails?.redisConfigured && <RedisBanner />}
{!window.isSecureContext && <InsecureConnectionBanner />} {!isLoading && !isInsideProject && !serverDetails?.emailConfigured && <SmtpBanner />}
<div className="flex grow flex-col overflow-y-hidden md:flex-row"> {!isLoading && !isInsideProject && subscription.auditLogs && <AuditLogBanner />}
<OrgSidebar isHidden={isInsideProject} /> {!window.isSecureContext && !isInsideProject && <InsecureConnectionBanner />}
<main <main
className={twMerge( className={twMerge(
"flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-4 pt-8 pb-4 dark:scheme-dark", "flex-1 overflow-x-hidden bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark",
isInsideProject && "p-0" isInsideProject ? "overflow-y-hidden p-0" : "overflow-y-auto"
)} )}
> >
<Outlet /> <Outlet />
@@ -9,6 +9,7 @@ import {
faEnvelope, faEnvelope,
faExclamationTriangle, faExclamationTriangle,
faGlobe, faGlobe,
faInfinity,
faInfo, faInfo,
faInfoCircle, faInfoCircle,
faServer, faServer,
@@ -43,7 +44,7 @@ import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context"; import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform"; import { isInfisicalCloud } from "@app/helpers/platform";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { projectKeys, useGetOrganizations, useLogoutUser } from "@app/hooks/api"; import { projectKeys, useGetOrganizations, useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api";
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types"; import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery"; import { getAuthToken } from "@app/hooks/api/reactQuery";
@@ -162,6 +163,8 @@ export const Navbar = () => {
await navigateUserToOrg(navigate, orgId); await navigateUserToOrg(navigate, orgId);
}; };
const { mutateAsync } = useGetOrgTrialUrl();
const logout = useLogoutUser(); const logout = useLogoutUser();
const logOutUser = async () => { const logOutUser = async () => {
try { try {
@@ -201,144 +204,191 @@ export const Navbar = () => {
const isServerAdminPanel = location.pathname.startsWith("/admin"); const isServerAdminPanel = location.pathname.startsWith("/admin");
const isOrgScope = breadcrumbs?.length === 1; // TODO: scott/akhil is this adequate? const isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate?
return ( return (
<div className="z-10 flex min-h-12 items-center border-b border-mineshaft-600 bg-mineshaft-800 px-4"> <div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1">
<div> <div className="mr-auto flex items-center overflow-hidden">
<Link to="/organization/projects"> <div className="shrink-0">
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" /> <Link to="/organization/projects">
</Link> <img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
</div>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{isServerAdminPanel ? (
<>
<Link
to="/admin"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
>
<div>
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
</div>
<div className="whitespace-nowrap">Server Console</div>
</Link> </Link>
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p> </div>
{breadcrumbs ? ( <p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
// scott: remove /admin as we show server console above {isServerAdminPanel ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} /> <>
) : null} <Link
</> to="/admin"
) : ( className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
<> >
<div className="flex items-center"> <div>
<DropdownMenu modal={false}> <FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
<Link to="/organization/projects"> </div>
<div className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"> <div className="whitespace-nowrap">Server Console</div>
<Badge </Link>
variant="org" <p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
className={twMerge("text-sm", !isOrgScope && "bg-transparent opacity-75")} {breadcrumbs ? (
> // scott: remove /admin as we show server console above
<FontAwesomeIcon icon={faGlobe} /> <BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
{currentOrg?.name} ) : null}
</Badge> </>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!"> ) : (
{getPlan(subscription)} <>
</div> <div className="flex items-center overflow-hidden">
{subscription.cardDeclined && ( <DropdownMenu modal={false}>
<Tooltip <Link className="overflow-hidden" to="/organization/projects">
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`} <div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
className="max-w-xs" <Badge
variant="org"
className={twMerge(
"max-w-full min-w-0 text-sm",
!isOrgScope && "bg-transparent opacity-75"
)}
> >
<div className="flex items-center"> <FontAwesomeIcon icon={faGlobe} />
<FontAwesomeIcon <p className="truncate">{currentOrg?.name}</p>
icon={faExclamationTriangle} </Badge>
className="animate-pulse cursor-help text-xs text-primary-400" <div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
/> {getPlan(subscription)}
</div> </div>
</Tooltip> {subscription.cardDeclined && (
)} <Tooltip
</div> content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
</Link> className="max-w-xs"
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="px-2 py-1"
>
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">organizations</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
> >
<div className="flex w-full max-w-[150px] items-center justify-between truncate"> <div className="flex items-center">
{org.name} <FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</div> </div>
</Button> </Tooltip>
</DropdownMenuItem> )}
); </div>
})} </Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <DropdownMenuTrigger asChild>
<DropdownMenuItem icon={<FontAwesomeIcon icon={faSignOut} />} onClick={logOutUser}> <div>
Log Out <IconButton
</DropdownMenuItem> variant="plain"
</DropdownMenuContent> colorSchema="secondary"
</DropdownMenu> ariaLabel="switch-org"
</div> className="px-2 py-1"
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p> >
{breadcrumbs ? ( <FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} /> </IconButton>
) : null} </div>
</> </DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
organizations
</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faSignOut} />}
onClick={logOutUser}
>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{!isOrgScope && (
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
<BreadcrumbContainer
className="min-w-[10rem] flex-1"
breadcrumbs={[breadcrumbs[0]] as TBreadcrumbFormat[]}
/>
) : null}
</>
)}
</>
)}
</div>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<Tooltip content="Start Free Pro Trial">
<Button
variant="plain"
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
leftIcon={<FontAwesomeIcon icon={faInfinity} />}
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
>
Free Pro Trial
</Button>
</Tooltip>
)}
{user.superAdmin && !location.pathname.startsWith("/admin") && (
<Link
className="mr-2 rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
to="/admin"
>
<FontAwesomeIcon icon={faServer} className="mr-2" />
Server Console
</Link>
)} )}
<div className="grow" />
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2.5 py-1 hover:bg-mineshaft-600"> <div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2.5 py-1 hover:bg-mineshaft-600">
@@ -0,0 +1,110 @@
import { Link, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
type Props = {
isHidden?: boolean;
};
export const OrgNavBar = ({ isHidden }: Props) => {
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { pathname } = useLocation();
return (
<>
{!isHidden && (
<div className="dark hidden w-full flex-col overflow-x-hidden border-b border-mineshaft-600 bg-mineshaft-900 px-4 md:flex">
<motion.div
key="menu-project-items"
initial={{ x: 150 }}
animate={{ x: 0 }}
exit={{ x: 150 }}
transition={{ duration: 0.2 }}
className=""
>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link to="/organization/projects">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Overview
</Tab>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<Tab
variant="org"
value={
isActive ||
pathname.match(
/organization\/members|organization\/identities|organization\/groups|organization\/roles/
)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link to="/organization/app-connections">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
App Connections
</Tab>
)}
</Link>
<Link to="/organization/networking">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Networking
</Tab>
)}
</Link>
<Link to="/organization/audit-logs">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Audit Logs
</Tab>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Settings
</Tab>
)}
</Link>
<Link to="/organization/billing">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Usage & Billing
</Tab>
)}
</Link>
<Link className="mr-auto" to="/organization/secret-sharing">
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""} variant="org">
Secret Sharing
</Tab>
)}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
</div>
)}
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};
@@ -0,0 +1 @@
export { OrgNavBar } from "./OrgNavBar";
@@ -1,214 +0,0 @@
import {
faBook,
faCog,
faInfinity,
faMoneyBill,
faNetworkWired,
faPlug,
faShare,
faTable,
faUsers,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Menu, MenuGroup, MenuItem, Tooltip } from "@app/components/v2";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useGetOrgTrialUrl } from "@app/hooks/api";
type Props = {
isHidden?: boolean;
};
export const OrgSidebar = ({ isHidden }: Props) => {
const { subscription } = useSubscription();
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
const { currentOrg } = useOrganization();
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
return (
<>
<AnimatePresence mode="popLayout">
{!isHidden && (
<motion.aside
key="org-sidebar"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: -240 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -240 }}
layout
className="dark z-10 w-60 border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-800 to-mineshaft-900"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<Menu>
<MenuGroup title="Overview">
<Link to="/organization/projects">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faTable} />
</div>
Overview
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Organization Access
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/billing">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faMoneyBill} className="mr-4" />
</div>
Usage & Billing
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/audit-logs">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} className="mr-4" />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} className="mr-4" />
</div>
Organization Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Resources">
<Link to="/organization/app-connections">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} className="mr-4" />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/networking">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faNetworkWired} className="mr-4" />
</div>
Networking
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
<div className="grow" />
<Menu>
{subscription &&
subscription.slug === "starter" &&
!subscription.has_used_trial && (
<Tooltip content="Start Free Pro Trial">
<MenuItem
variant="org"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faInfinity}
/>
}
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
>
Pro Trial
</MenuItem>
</Tooltip>
)}
<Link to="/organization/secret-sharing">
<MenuItem
variant="org"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faShare} />
</div>
}
>
Share Secret
</MenuItem>
</Link>
{user.superAdmin && (
<Link to="/admin">
<MenuItem
variant="org"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faUserTie}
/>
</div>
}
>
Server Console
</MenuItem>
</Link>
)}
</Menu>
</nav>
</motion.aside>
)}
</AnimatePresence>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};
@@ -1 +0,0 @@
export { OrgSidebar } from "./OrgSidebar";
+78 -147
View File
@@ -1,19 +1,11 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { import { faArrowLeft, faGlobe } from "@fortawesome/free-solid-svg-icons";
faBook,
faBoxOpen,
faCog,
faDisplay,
faHome,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router"; import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; import { Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission, useSubscription } from "@app/context"; import { useProject, useProjectPermission, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
@@ -23,7 +15,7 @@ export const PamLayout = () => {
const { currentProject } = useProject(); const { currentProject } = useProject();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { assumedPrivilegeDetails } = useProjectPermission(); const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
useEffect(() => { useEffect(() => {
@@ -35,152 +27,91 @@ export const PamLayout = () => {
return ( return (
<> <>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row"> <div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: -150 }} exit={{ x: -150 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" className="px-4"
> >
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark"> <nav className="w-full">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white"> <Tabs value="selected">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="groups" /> <TabList className="border-b-0">
PAM
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/pam/$projectId/accounts"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUser} />
</div>
Accounts
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/resources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBoxOpen} />
</div>
Resources
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/sessions"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faDisplay} />
</div>
Sessions
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/pam/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects"> <Link to="/organization/projects">
<MenuItem <Tab value="back" className="flex gap-x-2">
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300" <FontAwesomeIcon icon={faGlobe} />
leftIcon={ <FontAwesomeIcon icon={faArrowLeft} />
<div className="w-6"> </Tab>
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link> </Link>
</Menu> <Link
</div> to="/projects/pam/$projectId/accounts"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Accounts</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/resources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Resources</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/sessions"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Sessions</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/pam/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav> </nav>
</motion.div> </motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8"> </div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />} {assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet /> <div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
</div> <Outlet />
</div> </div>
</div> </div>
<UpgradePlanModal <UpgradePlanModal
@@ -1,23 +1,10 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { faArrowLeft, faGlobe, faMobile } from "@fortawesome/free-solid-svg-icons";
faBell,
faBook,
faCertificate,
faCog,
faFileLines,
faHome,
faMobile,
faPlug,
faPuzzlePiece,
faSitemap,
faStamp,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router"; import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; import { Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context"; import { useProject, useProjectPermission } from "@app/context";
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
@@ -27,223 +14,145 @@ export const PkiManagerLayout = () => {
const { assumedPrivilegeDetails } = useProjectPermission(); const { assumedPrivilegeDetails } = useProjectPermission();
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation();
return ( return (
<> <>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row"> <div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: -150 }} exit={{ x: -150 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" className="px-4"
> >
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark"> <nav className="w-full">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white"> <Tabs value="selected">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="note" /> <TabList className="border-b-0">
PKI Manager
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/cert-management/$projectId/subscribers"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faSitemap} />
</div>
Subscribers
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-templates"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faFileLines} />
</div>
Certificate Templates
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificates"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCertificate} />
</div>
Certificates
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faStamp} />
</div>
Certificates Authority
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/alerting"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBell} />
</div>
Alerting
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPuzzlePiece} />
</div>
Integrations
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/cert-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects"> <Link to="/organization/projects">
<MenuItem <Tab value="back" className="flex gap-x-2">
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300" <FontAwesomeIcon icon={faGlobe} />
leftIcon={ <FontAwesomeIcon icon={faArrowLeft} />
<div className="w-6"> </Tab>
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link> </Link>
</Menu> <Link
</div> to="/projects/cert-management/$projectId/subscribers"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Subscribers</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-templates"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>Certificate Templates</Tab>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificates"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive || location.pathname.match(/\/pki-collections\//)
? "selected"
: ""
}
>
Certificates
</Tab>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive || location.pathname.match(/\/ca\//) ? "selected" : ""}>
Certificate Authorities
</Tab>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/alerting"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Alerting</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>App Connections</Tab>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav> </nav>
</motion.div> </motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8"> </div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />} {assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet /> <div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
</div> <Outlet />
</div> </div>
</div> </div>
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden"> <div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
@@ -15,7 +15,7 @@ export const AssumePrivilegeModeBanner = () => {
if (!assumedPrivilegeDetails) return null; if (!assumedPrivilegeDetails) return null;
return ( return (
<div className="z-10 -mx-4 flex items-center justify-center gap-2 rounded-sm border border-mineshaft-600 bg-primary-400 p-2 text-mineshaft-800 shadow-sm"> <div className="flex w-full items-center border-b border-yellow/50 bg-yellow/30 px-4 py-2 text-sm text-yellow-200">
<div> <div>
<FontAwesomeIcon icon={faInfoCircle} className="mr-2" /> <FontAwesomeIcon icon={faInfoCircle} className="mr-2" />
You are currently viewing the project with privileges of{" "} You are currently viewing the project with privileges of{" "}
@@ -24,7 +24,7 @@ export const AssumePrivilegeModeBanner = () => {
{assumedPrivilegeDetails?.actorName} {assumedPrivilegeDetails?.actorName}
</b> </b>
</div> </div>
<div> <div className="ml-auto">
<Button <Button
size="xs" size="xs"
variant="outline_bg" variant="outline_bg"
@@ -99,26 +99,21 @@ export const ProjectSelect = () => {
}, [projects, projectFavorites, currentWorkspace]); }, [projects, projectFavorites, currentWorkspace]);
return ( return (
<div className="-mr-2 flex w-full items-center gap-1"> <div className="mr-2 flex items-center gap-1 overflow-hidden">
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<Link <Link
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)} to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
params={{ params={{
projectId: currentWorkspace.id projectId: currentWorkspace.id
}} }}
className="overflow-hidden"
> >
<div className="relative flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary"> <Tooltip content={currentWorkspace.name} className="max-w-96 break-words">
<Tooltip content={currentWorkspace.name} className="max-w-96 break-words"> <Badge variant="project" className="max-w-full min-w-0 text-sm">
<Badge <FontAwesomeIcon icon={faCube} />
variant="project" <p className="truncate">{currentWorkspace?.name}</p>
className="max-w-44 overflow-hidden text-sm text-ellipsis whitespace-nowrap" </Badge>
> </Tooltip>
<FontAwesomeIcon icon={faCube} />
{currentWorkspace?.name}
</Badge>
</Tooltip>
</div>
</Link> </Link>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div> <div>
@@ -1,21 +1,10 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { faArrowLeft, faGlobe, faMobile } from "@fortawesome/free-solid-svg-icons";
faArrowsSpin,
faBook,
faCheckToSlot,
faCog,
faHome,
faMobile,
faPlug,
faPuzzlePiece,
faUsers,
faVault
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet, useLocation } from "@tanstack/react-router"; import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context"; import { useProject, useProjectPermission } from "@app/context";
import { import {
useGetAccessRequestsCount, useGetAccessRequestsCount,
@@ -28,10 +17,10 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
export const SecretManagerLayout = () => { export const SecretManagerLayout = () => {
const { currentProject, projectId } = useProject(); const { currentProject, projectId } = useProject();
const { assumedPrivilegeDetails } = useProjectPermission(); const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
const { t } = useTranslation(); const { t } = useTranslation();
const projectSlug = currentProject?.slug || ""; const projectSlug = currentProject?.slug || "";
const location = useLocation();
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
projectId projectId
@@ -54,208 +43,137 @@ export const SecretManagerLayout = () => {
return ( return (
<> <>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row"> <div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: -150 }} exit={{ x: -150 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" className="px-4"
> >
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark"> <nav className="w-full">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white"> <Tabs value="selected">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="vault" /> <TabList className="border-b-0">
Secrets Manager
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/secret-management/$projectId/overview"
params={{
projectId: currentProject.id,
...(currentProject.environments.length
? { envSlug: currentProject.environments[0]?.slug }
: {})
}}
>
{({ isActive }) => (
<MenuItem
variant="project"
isSelected={
isActive ||
location.pathname.startsWith(
`/projects/secret-management/${currentProject.id}/overview`
)
}
>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faVault} />
</div>
Secrets
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPuzzlePiece} />
</div>
Integrations
</div>
</MenuItem>
)}
</Link>
{Boolean(secretRotations?.length) && (
<Link
to="/projects/secret-management/$projectId/secret-rotation"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faArrowsSpin} />
</div>
Secret Rotations
</div>
</MenuItem>
)}
</Link>
)}
<Link
to="/projects/secret-management/$projectId/approval"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCheckToSlot} />
</div>
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/secret-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects"> <Link to="/organization/projects">
<MenuItem <Tab value="back" className="flex gap-x-2">
variant="project" <FontAwesomeIcon icon={faGlobe} />
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300" <FontAwesomeIcon icon={faArrowLeft} />
leftIcon={ </Tab>
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link> </Link>
</Menu> <Link
</div> to="/projects/secret-management/$projectId/overview"
params={{
projectId: currentProject.id,
...(currentProject.environments.length
? { envSlug: currentProject.environments[0]?.slug }
: {})
}}
>
{({ isActive }) => (
<Tab
value={
isActive || location.pathname.match(/\/secrets\/|\/commits\//)
? "selected"
: ""
}
>
Overview
</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
</Link>
{Boolean(secretRotations?.length) && (
<Link
to="/projects/secret-management/$projectId/secret-rotation"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>Secret Rotations</Tab>
)}
</Link>
)}
<Link
to="/projects/secret-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>App Connections</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/approval"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>
Approvals
{Boolean(
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/secret-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav> </nav>
</motion.div> </motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8"> </div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />} {assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet /> <div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
</div> <Outlet />
</div> </div>
</div> </div>
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden"> <div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
@@ -1,17 +1,9 @@
import { import { faArrowLeft, faGlobe } from "@fortawesome/free-solid-svg-icons";
faBook,
faCog,
faDatabase,
faHome,
faMagnifyingGlass,
faPlug,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router"; import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import { import {
ProjectPermissionSub, ProjectPermissionSub,
useProject, useProject,
@@ -29,6 +21,7 @@ export const SecretScanningLayout = () => {
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const location = useLocation();
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount( const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(
currentProject.id, currentProject.id,
@@ -45,158 +38,101 @@ export const SecretScanningLayout = () => {
return ( return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row"> <div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: -150 }} exit={{ x: -150 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" className="px-4"
> >
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark"> <nav className="w-full">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white"> <Tabs value="selected">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="secret-scan" /> <TabList className="border-b-0">
Secret Scanning
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/secret-scanning/$projectId/data-sources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faDatabase} />
</div>
Data Sources
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/findings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex w-full gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faMagnifyingGlass} />
</div>
<span>Findings</span>
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="mr-2 ml-auto h-min">
{unresolvedFindings}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/secret-scanning/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects"> <Link to="/organization/projects">
<MenuItem <Tab value="back" className="flex gap-x-2">
variant="project" <FontAwesomeIcon icon={faGlobe} />
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300" <FontAwesomeIcon icon={faArrowLeft} />
leftIcon={ </Tab>
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link> </Link>
</Menu> <Link
</div> to="/projects/secret-scanning/$projectId/data-sources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Data Sources</Tab>}
</Link>
<Link
to="/projects/secret-scanning/$projectId/findings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>
Findings
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="ml-2 h-min">
{unresolvedFindings}
</Badge>
)}
</Tab>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>App Connections</Tab>}
</Link>
<Link
to="/projects/secret-scanning/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/secret-scanning/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav> </nav>
</motion.div> </motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8"> </div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />} {assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet /> <div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
</div> <Outlet />
</div> </div>
</div> </div>
); );
+93 -139
View File
@@ -1,17 +1,10 @@
import { import { faArrowLeft, faGlobe } from "@fortawesome/free-solid-svg-icons";
faBook,
faCog,
faHome,
faServer,
faStamp,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router"; import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; import { Tab, TabList, Tabs } from "@app/components/v2";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
@@ -24,149 +17,110 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
export const SshLayout = () => { export const SshLayout = () => {
const { currentProject } = useProject(); const { currentProject } = useProject();
const { assumedPrivilegeDetails } = useProjectPermission(); const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
return ( return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row"> <div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: -150 }} exit={{ x: -150 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60" className="px-4"
> >
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark"> <nav className="w-full">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white"> <Tabs value="selected">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="terminal" /> <TabList className="border-b-0">
SSH
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/ssh/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faServer} />
</div>
Hosts
</div>
</MenuItem>
)}
</Link>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to="/projects/ssh/$projectId/cas"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faStamp} />
</div>
Certificates Authority
</div>
</MenuItem>
)}
</Link>
)
}
</ProjectPermissionCan>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/ssh/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/ssh/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/ssh/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects"> <Link to="/organization/projects">
<MenuItem <Tab value="back" className="flex gap-x-2">
variant="project" <FontAwesomeIcon icon={faGlobe} />
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300" <FontAwesomeIcon icon={faArrowLeft} />
leftIcon={ </Tab>
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link> </Link>
</Menu> <Link
</div> to="/projects/ssh/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive || location.pathname.match(/\/ssh-host-groups\//) ? "selected" : ""
}
>
Hosts
</Tab>
)}
</Link>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to="/projects/ssh/$projectId/cas"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={isActive || location.pathname.match(/\/ca\//) ? "selected" : ""}
>
Certificate Authorities
</Tab>
)}
</Link>
)
}
</ProjectPermissionCan>
<Link
to="/projects/ssh/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/ssh/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/ssh/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav> </nav>
</motion.div> </motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8"> </div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />} {assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet /> <div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
</div> <Outlet />
</div> </div>
</div> </div>
); );
@@ -13,8 +13,8 @@ export const AccessManagementPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Access Control" })}</title> <title>{t("common.head-title", { title: "Access Control" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="instance" scope="instance"
title="Access Control" title="Access Control"
@@ -13,8 +13,8 @@ export const AuthenticationPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title> <title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="instance" scope="instance"
title="Authentication" title="Authentication"
@@ -13,8 +13,8 @@ export const CachingPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title> <title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="instance" scope="instance"
title="Caching" title="Caching"
@@ -13,8 +13,8 @@ export const EncryptionPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title> <title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="instance" scope="instance"
title="Encryption" title="Encryption"
@@ -13,8 +13,8 @@ export const EnvironmentPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title> <title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="instance" scope="instance"
title="Environment Variables" title="Environment Variables"
@@ -15,8 +15,8 @@ export const GeneralPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title> <title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="instance" scope="instance"
title="General" title="General"
@@ -13,8 +13,8 @@ export const IntegrationsPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title> <title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="instance" scope="instance"
title="Integrations" title="Integrations"
@@ -13,18 +13,24 @@ export const ResourceOverviewPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Resource Overview" })}</title> <title>{t("common.head-title", { title: "Resource Overview" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="instance" scope="instance"
title="Resource Overview" title="Resource Overview"
description="Manage resources within your Infisical instance." description="Manage resources within your Infisical instance."
/> />
<Tabs defaultValue="tab-organizations"> <Tabs orientation="vertical" defaultValue="tab-organizations">
<TabList> <TabList>
<Tab value="tab-organizations">Organizations</Tab> <Tab variant="instance" value="tab-organizations">
<Tab value="tab-users">Users</Tab> Organizations
<Tab value="tab-identities">Identities</Tab> </Tab>
<Tab variant="instance" value="tab-users">
Users
</Tab>
<Tab variant="instance" value="tab-identities">
Identities
</Tab>
</TabList> </TabList>
<TabPanel value="tab-organizations"> <TabPanel value="tab-organizations">
<OrganizationsTable /> <OrganizationsTable />
@@ -10,11 +10,11 @@ import { PkiAlertsSection } from "./components";
export const AlertingPage = () => { export const AlertingPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Alerting" })}</title> <title>{t("common.head-title", { title: "Alerting" })}</title>
</Helmet> </Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Alerting" title="Alerting"
@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useNavigate, useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -84,10 +86,20 @@ const Page = () => {
}; };
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<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={data.name}> <Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Certificate Authorities
</Link>
<PageHeader scope="project" description="Manage certificate authority" title={data.name}>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -11,11 +11,11 @@ import { CaSection } from "./components";
export const CertificateAuthoritiesPage = () => { export const CertificateAuthoritiesPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Certificate Authorities" })}</title> <title>{t("common.head-title", { title: "Certificate Authorities" })}</title>
</Helmet> </Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Certificate Authorities" title="Certificate Authorities"
@@ -27,11 +27,11 @@ export const CertificatesPage = () => {
); );
return ( return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title> <title>{t("common.head-title", { title: "Certificates" })}</title>
</Helmet> </Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Certificates" title="Certificates"
@@ -42,16 +42,18 @@ export const IntegrationsListPage = () => {
<meta property="og:title" content="Manage your certificates in seconds" /> <meta property="og:title" content="Manage your certificates in seconds" />
<meta name="og:description" content="Sync and manage PKI certificates across services" /> <meta name="og:description" content="Sync and manage PKI certificates across services" />
</Helmet> </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"> <div className="mb-8">
<PageHeader <PageHeader
scope="project" scope="project"
title="Integrations" title="Integrations"
description="Manage integrations with third-party certificate services." description="Manage integrations with third-party certificate services."
/> />
<Tabs value={currentTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={currentTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab value={IntegrationsListPageTabs.PkiSyncs}>Certificate Syncs</Tab> <Tab variant="project" value={IntegrationsListPageTabs.PkiSyncs}>
Certificate Syncs
</Tab>
</TabList> </TabList>
<TabPanel value={IntegrationsListPageTabs.PkiSyncs}> <TabPanel value={IntegrationsListPageTabs.PkiSyncs}>
<ProjectPermissionCan <ProjectPermissionCan
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -70,10 +72,20 @@ export const PkiCollectionPage = () => {
}; };
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<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={data.name}> <Link
to="/projects/cert-management/$projectId/certificates"
params={{
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Certificates
</Link>
<PageHeader scope="project" title={data.name} description="Manage certificate collection">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -75,10 +77,20 @@ const Page = () => {
}; };
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<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={data.name}> <Link
to="/projects/cert-management/$projectId/subscribers"
params={{
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Subscribers
</Link>
<PageHeader scope="project" title={data.name} description="Manage PKI subscriber">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -13,8 +13,8 @@ export const PkiSubscribersPage = () => {
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title> <title>{t("common.head-title", { title: "PKI Subscribers" })}</title>
</Helmet> </Helmet>
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between text-white"> <div className="mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Subscribers" title="Subscribers"
@@ -68,8 +68,8 @@ const PageContent = () => {
return ( return (
<> <>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white"> <div className="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 <Button
variant="link" variant="link"
type="submit" type="submit"
@@ -103,15 +103,15 @@ export const PkiTemplateListPage = () => {
<title>{t("common.head-title", { title: "PKI Templates" })}</title> <title>{t("common.head-title", { title: "PKI Templates" })}</title>
</Helmet> </Helmet>
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between text-white"> <div className="mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Certificate Templates" title="Certificate Templates"
description="Manage certificate template to request and issue dynamic certificates following a strict format." description="Manage certificate template to request and issue dynamic certificates following a strict format."
/> />
</div> </div>
<div className="container mx-auto mb-6 max-w-7xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="container mx-auto mb-6 max-w-8xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Templates</p> <p className="text-xl font-medium text-mineshaft-100">Templates</p>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
@@ -267,7 +267,7 @@ export const PkiTemplateListPage = () => {
onDeleteApproved={() => onRemovePkiSubscriberSubmit()} onDeleteApproved={() => onRemovePkiSubscriberSubmit()}
/> />
</div> </div>
<div className="container mx-auto max-w-7xl" /> <div className="container mx-auto max-w-8xl" />
</div> </div>
<Modal <Modal
isOpen={popUp?.certificateTemplate?.isOpen} isOpen={popUp?.certificateTemplate?.isOpen}
@@ -20,12 +20,12 @@ export const SettingsPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title> <title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet> </Helmet>
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader scope="project" title={t("settings.project.title")} /> <PageHeader scope="project" title={t("settings.project.title")} />
<Tabs defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}> <Tab value={tab.key} variant="project" key={tab.key}>
{tab.name} {tab.name}
</Tab> </Tab>
))} ))}
+2 -2
View File
@@ -15,8 +15,8 @@ export const KmipPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title> <title>{t("common.head-title", { title: "KMS" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="KMIP" title="KMIP"
@@ -15,8 +15,8 @@ export const OverviewPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title> <title>{t("common.head-title", { title: "KMS" })}</title>
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Overview" title="Overview"
@@ -20,12 +20,16 @@ export const SettingsPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title> <title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet> </Helmet>
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader scope="project" title="Settings" /> <PageHeader
<Tabs defaultValue={tabs[0].key}> scope="project"
title="Settings"
description="Configure general project settings"
/>
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}> <Tab variant="project" value={tab.key} key={tab.key}>
{tab.name} {tab.name}
</Tab> </Tab>
))} ))}
@@ -76,11 +76,11 @@ export const AccessManagementPage = () => {
const hasNoAccess = tabSections.every((tab) => tab.isHidden); const hasNoAccess = tabSections.every((tab) => tab.isHidden);
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet> </Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="org" scope="org"
title="Access Control" title="Access Control"
@@ -111,7 +111,7 @@ export const AccessManagementPage = () => {
isOpen={isUpgradePrivilegeSystemModalOpen} isOpen={isUpgradePrivilegeSystemModalOpen}
onOpenChange={setIsUpgradePrivilegeSystemModalOpen} onOpenChange={setIsUpgradePrivilegeSystemModalOpen}
/> />
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
{tabSections {tabSections
.filter((el) => !el.isHidden) .filter((el) => !el.isHidden)
@@ -1,17 +1,5 @@
import { motion } from "framer-motion";
import { OrgGroupsSection } from "./components"; import { OrgGroupsSection } from "./components";
export const OrgGroupsTab = () => { export const OrgGroupsTab = () => {
return ( return <OrgGroupsSection />;
<motion.div
key="panel-org-groups"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgGroupsSection />
</motion.div>
);
}; };
@@ -1,17 +1,5 @@
import { motion } from "framer-motion";
import { IdentitySection } from "./components"; import { IdentitySection } from "./components";
export const OrgIdentityTab = () => { export const OrgIdentityTab = () => {
return ( return <IdentitySection />;
<motion.div
key="panel-service-token"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<IdentitySection />
</motion.div>
);
}; };
@@ -1,17 +1,5 @@
import { motion } from "framer-motion";
import { OrgMembersSection } from "./components"; import { OrgMembersSection } from "./components";
export const OrgMembersTab = () => { export const OrgMembersTab = () => {
return ( return <OrgMembersSection />;
<motion.div
key="panel-org-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgMembersSection />
</motion.div>
);
}; };
@@ -1,17 +1,5 @@
import { motion } from "framer-motion";
import { OrgRoleTable } from "./OrgRoleTable"; import { OrgRoleTable } from "./OrgRoleTable";
export const OrgRoleTabSection = () => { export const OrgRoleTabSection = () => {
return ( return <OrgRoleTable />;
<motion.div
key="role-list"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgRoleTable />
</motion.div>
);
}; };
@@ -20,7 +20,7 @@ export const AppConnectionsPage = withPermission(
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope="org" scope="org"
className="w-full" className="w-full"
@@ -12,9 +12,8 @@ export const AuditLogsPage = () => {
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 pb-6 text-white">
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="w-full max-w-8xl">
<div className="w-full max-w-7xl">
<PageHeader <PageHeader
scope="org" scope="org"
title="Audit Logs" title="Audit Logs"
@@ -18,7 +18,7 @@ export const BillingPage = () => {
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope="org" scope="org"
title={t("billing.title")} title={t("billing.title")}
@@ -17,14 +17,14 @@ const tabs = [
export const BillingTabGroup = withPermission( export const BillingTabGroup = withPermission(
() => { () => {
const tabsFiltered = isInfisicalCloud() if (!isInfisicalCloud()) {
? tabs return <BillingCloudTab />;
: [{ name: "Infisical Self-Hosted", key: "tab-infisical-cloud" }]; }
return ( return (
<Tabs defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabsFiltered.map((tab) => ( {tabs.map((tab) => (
<Tab variant="org" value={tab.key}> <Tab variant="org" value={tab.key}>
{tab.name} {tab.name}
</Tab> </Tab>
@@ -33,19 +33,15 @@ export const BillingTabGroup = withPermission(
<TabPanel value={tabs[0].key}> <TabPanel value={tabs[0].key}>
<BillingCloudTab /> <BillingCloudTab />
</TabPanel> </TabPanel>
{isInfisicalCloud() && ( <TabPanel value={tabs[1].key}>
<> <BillingSelfHostedTab />
<TabPanel value={tabs[1].key}> </TabPanel>
<BillingSelfHostedTab /> <TabPanel value={tabs[2].key}>
</TabPanel> <BillingReceiptsTab />
<TabPanel value={tabs[2].key}> </TabPanel>
<BillingReceiptsTab /> <TabPanel value={tabs[3].key}>
</TabPanel> <BillingDetailsTab />
<TabPanel value={tabs[3].key}> </TabPanel>
<BillingDetailsTab />
</TabPanel>
</>
)}
</Tabs> </Tabs>
); );
}, },
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -80,10 +82,20 @@ const Page = () => {
if (isPending) return <Spinner size="sm" className="mt-2 ml-2" />; if (isPending) return <Spinner size="sm" className="mt-2 ml-2" />;
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto w-full max-w-8xl">
<PageHeader scope="org" title={data.group.name}> <Link
to="/organization/access-management"
search={{
selectedTab: TabSections.Groups
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Groups
</Link>
<PageHeader scope="org" description="Organization Group" title={data.group.name}>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -10,6 +12,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api"; import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from "@app/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage";
import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal"; import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal";
import { OrgAccessControlTabSections } from "@app/types/org"; import { OrgAccessControlTabSections } from "@app/types/org";
@@ -72,10 +75,20 @@ const Page = () => {
}; };
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto w-full max-w-8xl">
<PageHeader scope="org" title={data.identity.name} /> <Link
to="/organization/access-management"
search={{
selectedTab: TabSections.Identities
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Identities
</Link>
<PageHeader scope="org" description="Organization Identity" title={data.identity.name} />
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} /> <IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
@@ -12,7 +12,7 @@ export const NetworkingPage = () => {
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope="org" scope="org"
title="Networking" title="Networking"
@@ -19,7 +19,7 @@ export const NetworkingTabGroup = () => {
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
return ( return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab variant="org" value={tab.key} key={tab.key}> <Tab variant="org" value={tab.key} key={tab.key}>
@@ -58,18 +58,16 @@ export const ProjectsPage = () => {
: true; : true;
return ( return (
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800"> <div className="mx-auto flex max-w-8xl flex-col justify-start bg-bunker-800">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title> <title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="mb-4 flex flex-col items-start justify-start"> <PageHeader
<PageHeader scope="org"
scope="org" title="Overview"
title="Overview" description="Your team's complete security toolkit - organized and ready when you need them."
description="Your team's complete security toolkit - organized and ready when you need them." />
/>
</div>
{projectListView === ProjectListView.MyProjects ? ( {projectListView === ProjectListView.MyProjects ? (
<MyProjectView <MyProjectView
onAddNewProject={() => handlePopUpOpen("addNewWs")} onAddNewProject={() => handlePopUpOpen("addNewWs")}
@@ -1,8 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faCopy, faEllipsisV } from "@fortawesome/free-solid-svg-icons"; import { faChevronLeft, faCopy, faEllipsisV } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
@@ -19,6 +19,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgRole, useGetOrgRole } from "@app/hooks/api"; import { useDeleteOrgRole, useGetOrgRole } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from "@app/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage";
import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal"; import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal";
import { OrgAccessControlTabSections } from "@app/types/org"; import { OrgAccessControlTabSections } from "@app/types/org";
@@ -77,20 +78,26 @@ export const Page = () => {
const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? ""); const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? "");
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto w-full max-w-8xl">
<Link
to="/organization/access-management"
search={{
selectedTab: TabSections.Roles
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Roles
</Link>
<PageHeader <PageHeader
scope="org" scope="org"
title={ title={data.name}
<div className="flex flex-col"> description={
<div> <>
<span>{data.name}</span> {data.slug} {data.description && `- ${data.description}`}
<p className="text-sm leading-3 font-normal text-mineshaft-400 normal-case"> </>
{data.slug} {data.description && `- ${data.description}`}
</p>
</div>
</div>
} }
> >
{isCustomRole && ( {isCustomRole && (
@@ -20,7 +20,7 @@ export const SecretSharingPage = () => {
<meta name="og:description" content={String(t("approval.og-description"))} /> <meta name="og:description" content={String(t("approval.og-description"))} />
</Helmet> </Helmet>
<div className="h-full"> <div className="h-full">
<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 <PageHeader
scope="org" scope="org"
title="Secret Sharing" title="Secret Sharing"
@@ -1,7 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { Badge, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab"; import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
@@ -34,14 +34,13 @@ export const ShareSecretSection = () => {
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab value={SecretSharingPageTabs.ShareSecret}>Share Secrets</Tab> <Tab variant="org" value={SecretSharingPageTabs.ShareSecret}>
<Tab value={SecretSharingPageTabs.RequestSecret}> Share Secrets
</Tab>
<Tab variant="org" value={SecretSharingPageTabs.RequestSecret}>
Request Secrets Request Secrets
<Badge variant="primary" className="ml-1 cursor-pointer text-xs">
New
</Badge>
</Tab> </Tab>
</TabList> </TabList>
<TabPanel value={SecretSharingPageTabs.ShareSecret}> <TabPanel value={SecretSharingPageTabs.ShareSecret}>
@@ -20,7 +20,7 @@ export const SecretSharingSettingsPage = withPermission(
<title>{t("common.head-title", { title: "Secret Share Settings" })}</title> <title>{t("common.head-title", { title: "Secret Share Settings" })}</title>
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader scope="org" title="Secret Share Settings" /> <PageHeader scope="org" title="Secret Share Settings" />
<SecretSharingSettingsTabGroup /> <SecretSharingSettingsTabGroup />
</div> </div>
@@ -14,8 +14,12 @@ export const SettingsPage = () => {
<title>{t("common.head-title", { title: t("settings.org.title") })}</title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader scope="org" title={t("settings.org.title")} /> <PageHeader
scope="org"
description="Configure organization-wide settings"
title={t("settings.org.title")}
/>
<OrgTabGroup /> <OrgTabGroup />
</div> </div>
</div> </div>
@@ -55,7 +55,7 @@ export const OrgTabGroup = () => {
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
return ( return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab variant="org" value={tab.key} key={tab.key}> <Tab variant="org" value={tab.key} key={tab.key}>
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -30,6 +32,7 @@ import {
useUpdateOrgMembership useUpdateOrgMembership
} from "@app/hooks/api"; } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from "@app/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage";
import { OrgAccessControlTabSections } from "@app/types/org"; import { OrgAccessControlTabSections } from "@app/types/org";
import { UserAuditLogsSection } from "./components/UserProjectsSection/UserAuditLogsSection"; import { UserAuditLogsSection } from "./components/UserProjectsSection/UserAuditLogsSection";
@@ -115,16 +118,30 @@ const Page = withPermission(
}; };
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{membership && ( {membership && (
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto w-full max-w-8xl">
<Link
to="/organization/access-management"
search={{
selectedTab: TabSections.Member
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Users
</Link>
<PageHeader <PageHeader
scope="org" scope="org"
title={ title={
membership.user.firstName || membership.user.lastName membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim() ? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
: "-" : (membership.user.username ??
membership.user.email ??
membership.inviteEmail ??
"Unknown User")
} }
description="Organization User Membership"
> >
<div> <div>
{userId !== membership.user.id && ( {userId !== membership.user.id && (
@@ -21,8 +21,8 @@ export const PamAccountsPage = () => {
a={ProjectPermissionSub.PamAccounts} a={ProjectPermissionSub.PamAccounts}
> >
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Accounts" title="Accounts"
@@ -0,0 +1,57 @@
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
type Props = {
path: string;
};
export const FolderBreadCrumbs = ({ path = "/" }: Props) => {
const navigate = useNavigate({
from: "/projects/secret-management/$projectId/overview"
});
const onFolderCrumbClick = (index: number) => {
let newAccountPath = `/${path.split("/").filter(Boolean).slice(0, index).join("/")}`;
if (!newAccountPath.endsWith("/")) {
newAccountPath += "/";
}
if (path === newAccountPath) return;
navigate({
search: (prev) => ({ ...prev, accountPath: newAccountPath })
});
};
return (
<div className="flex items-center space-x-2">
<div
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 py-1 pr-2 pl-5 text-sm hover:bg-mineshaft-600"
onClick={() => onFolderCrumbClick(0)}
onKeyDown={() => null}
role="button"
tabIndex={0}
>
<FontAwesomeIcon icon={faFolderOpen} className="text-primary-700" />
</div>
{(path || "")
.split("/")
.filter(Boolean)
.map((pathSegment, index, arr) => (
<div
key={`path-${index + 1}`}
className={`breadcrumb relative z-20 ${
index + 1 === arr.length ? "cursor-default" : "cursor-pointer"
} border-solid border-mineshaft-600 py-1 pr-2 pl-5 text-sm text-mineshaft-200`}
onClick={() => onFolderCrumbClick(index + 1)}
onKeyDown={() => null}
role="button"
tabIndex={0}
>
{pathSegment}
</div>
))}
</div>
);
};
@@ -45,6 +45,7 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { PAM_RESOURCE_TYPE_MAP, TPamAccount, TPamFolder } from "@app/hooks/api/pam"; import { PAM_RESOURCE_TYPE_MAP, TPamAccount, TPamFolder } from "@app/hooks/api/pam";
import { AccountView, AccountViewToggle } from "./AccountViewToggle"; import { AccountView, AccountViewToggle } from "./AccountViewToggle";
import { FolderBreadCrumbs } from "./FolderBreadCrumbs";
import { PamAccessAccountModal } from "./PamAccessAccountModal"; import { PamAccessAccountModal } from "./PamAccessAccountModal";
import { PamAccountRow } from "./PamAccountRow"; import { PamAccountRow } from "./PamAccountRow";
import { PamAddAccountModal } from "./PamAddAccountModal"; import { PamAddAccountModal } from "./PamAddAccountModal";
@@ -255,6 +256,7 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
return ( return (
<div> <div>
{accountView === AccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
<div className="mt-4 flex gap-2"> <div className="mt-4 flex gap-2">
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}> <ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
{(isAllowed) => {(isAllowed) =>
@@ -21,8 +21,8 @@ export const PamResourcesPage = () => {
a={ProjectPermissionSub.PamResources} a={ProjectPermissionSub.PamResources}
> >
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Resources" title="Resources"
@@ -1,10 +1,12 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useParams } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub, useProject } from "@app/context";
import { ProjectPermissionPamSessionActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionPamSessionActions } from "@app/context/ProjectPermissionContext/types";
import { useGetPamSessionById } from "@app/hooks/api/pam"; import { useGetPamSessionById } from "@app/hooks/api/pam";
@@ -17,11 +19,21 @@ const Page = () => {
select: (el) => el.sessionId select: (el) => el.sessionId
}); });
const { data: session } = useGetPamSessionById(sessionId); const { data: session } = useGetPamSessionById(sessionId);
const { currentProject } = useProject();
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{session && ( {session && (
<div className="mx-auto mb-6 flex w-full max-w-7xl flex-col"> <div className="mx-auto mb-6 flex w-full max-w-8xl flex-col">
<Link
to="/projects/pam/$projectId/sessions"
params={{
projectId: currentProject.id
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Sessions
</Link>
<PageHeader <PageHeader
scope="project" scope="project"
title={`${session.accountName} Session`} title={`${session.accountName} Session`}
@@ -21,8 +21,8 @@ export const PamSessionPage = () => {
a={ProjectPermissionSub.PamSessions} a={ProjectPermissionSub.PamSessions}
> >
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Sessions" title="Sessions"
@@ -12,11 +12,13 @@ export const SettingsPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title> <title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet> </Helmet>
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader scope="project" title="Settings" description="Configure your PAM project." /> <PageHeader scope="project" title="Settings" description="Configure your PAM project." />
<Tabs defaultValue="tab-project-general"> <Tabs orientation="vertical" defaultValue="tab-project-general">
<TabList> <TabList>
<Tab value="tab-project-general">General</Tab> <Tab variant="project" value="tab-project-general">
General
</Tab>
</TabList> </TabList>
<TabPanel value="tab-project-general"> <TabPanel value="tab-project-general">
<ProjectGeneralTab /> <ProjectGeneralTab />
@@ -37,26 +37,32 @@ const Page = () => {
const isSecretManager = currentProject.type === ProjectType.SecretManager; const isSecretManager = currentProject.type === ProjectType.SecretManager;
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Access Control" title="Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your project resources." description="Manage fine-grained access for users, groups, roles, and identities within your project resources."
/> />
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab value={ProjectAccessControlTabs.Member}>Users</Tab> <Tab variant="project" value={ProjectAccessControlTabs.Member}>
<Tab value={ProjectAccessControlTabs.Groups}>Groups</Tab> Users
<Tab value={ProjectAccessControlTabs.Identities}> </Tab>
<div className="flex items-center"> <Tab variant="project" value={ProjectAccessControlTabs.Groups}>
<p>Machine Identities</p> Groups
</div> </Tab>
<Tab variant="project" value={ProjectAccessControlTabs.Identities}>
Identities
</Tab> </Tab>
{isSecretManager && ( {isSecretManager && (
<Tab value={ProjectAccessControlTabs.ServiceTokens}>Service Tokens</Tab> <Tab variant="project" value={ProjectAccessControlTabs.ServiceTokens}>
Service Tokens
</Tab>
)} )}
<Tab value={ProjectAccessControlTabs.Roles}>Project Roles</Tab> <Tab variant="project" value={ProjectAccessControlTabs.Roles}>
Roles
</Tab>
</TabList> </TabList>
<TabPanel value={ProjectAccessControlTabs.Member}> <TabPanel value={ProjectAccessControlTabs.Member}>
<MembersTab /> <MembersTab />
@@ -1,5 +1,3 @@
import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
@@ -7,17 +5,7 @@ import { GroupsSection } from "./components";
export const GroupsTab = withProjectPermission( export const GroupsTab = withProjectPermission(
() => { () => {
return ( return <GroupsSection />;
<motion.div
key="panel-groups"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<GroupsSection />
</motion.div>
);
}, },
{ {
action: ProjectPermissionActions.Read, action: ProjectPermissionActions.Read,
@@ -14,7 +14,6 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns"; import { format } from "date-fns";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -158,307 +157,295 @@ export const IdentityTab = withProjectPermission(
}; };
return ( return (
<motion.div <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
key="identity-role-panel" <div className="mb-4 flex items-center justify-between">
transition={{ duration: 0.15 }} <div className="flex items-center gap-1">
initial={{ opacity: 0, translateX: 30 }} <p className="text-xl font-medium text-mineshaft-100">Identities</p>
animate={{ opacity: 1, translateX: 0 }} <a
exit={{ opacity: 0, translateX: 30 }} href="https://infisical.com/docs/documentation/platform/identities/overview"
> target="_blank"
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> rel="noopener noreferrer"
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-[0.16rem] 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>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Identity}
> >
{(isAllowed) => ( <div className="mt-[0.16rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<Button <FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
colorSchema="secondary" <span>Docs</span>
type="submit" <FontAwesomeIcon
leftIcon={<FontAwesomeIcon icon={faPlus} />} icon={faArrowUpRightFromSquare}
onClick={() => handlePopUpOpen("identity")} className="mb-[0.07rem] ml-1.5 text-[10px]"
isDisabled={!isAllowed} />
> </div>
Add Identity </a>
</Button>
)}
</ProjectPermissionCan>
</div> </div>
<Input <ProjectPermissionCan
containerClassName="mb-4" I={ProjectPermissionActions.Create}
value={search} a={ProjectPermissionSub.Identity}
onChange={(e) => setSearch(e.target.value)} >
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} {(isAllowed) => (
placeholder="Search identities by name..." <Button
/> colorSchema="secondary"
<TableContainer> type="submit"
<Table> leftIcon={<FontAwesomeIcon icon={faPlus} />}
<THead> onClick={() => handlePopUpOpen("identity")}
<Tr className="h-14"> isDisabled={!isAllowed}
<Th className="w-1/3"> >
<div className="flex items-center"> Add Identity
Name </Button>
<IconButton )}
variant="plain" </ProjectPermissionCan>
className={`ml-2 ${ </div>
orderBy === ProjectIdentityOrderBy.Name ? "" : "opacity-30" <Input
}`} containerClassName="mb-4"
ariaLabel="sort" value={search}
onClick={() => handleSort(ProjectIdentityOrderBy.Name)} onChange={(e) => setSearch(e.target.value)}
> leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
<FontAwesomeIcon placeholder="Search identities by name..."
icon={ />
orderDirection === OrderByDirection.DESC && <TableContainer>
orderBy === ProjectIdentityOrderBy.Name <Table>
? faArrowUp <THead>
: faArrowDown <Tr className="h-14">
} <Th className="w-1/3">
/> <div className="flex items-center">
</IconButton> Name
</div> <IconButton
</Th> variant="plain"
<Th className="w-1/3">Role</Th> className={`ml-2 ${
<Th>Added on</Th> orderBy === ProjectIdentityOrderBy.Name ? "" : "opacity-30"
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th> }`}
</Tr> ariaLabel="sort"
</THead> onClick={() => handleSort(ProjectIdentityOrderBy.Name)}
<TBody> >
{isPending && <TableSkeleton columns={4} innerKey="project-identities" />} <FontAwesomeIcon
{!isPending && icon={
data && orderDirection === OrderByDirection.DESC &&
data.identityMemberships.length > 0 && orderBy === ProjectIdentityOrderBy.Name
data.identityMemberships.map((identityMember) => { ? faArrowUp
const { : faArrowDown
identity: { id, name }, }
roles, />
createdAt </IconButton>
} = identityMember; </div>
return ( </Th>
<Tr <Th className="w-1/3">Role</Th>
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" <Th>Added on</Th>
key={`st-v3-${id}`} <Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
role="button" </Tr>
tabIndex={0} </THead>
onKeyDown={(evt) => { <TBody>
if (evt.key === "Enter") { {isPending && <TableSkeleton columns={4} innerKey="project-identities" />}
navigate({ {!isPending &&
to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const, data &&
params: { data.identityMemberships.length > 0 &&
projectId: currentProject.id, data.identityMemberships.map((identityMember) => {
identityId: id const {
} identity: { id, name },
}); roles,
} createdAt
}} } = identityMember;
onClick={() => return (
<Tr
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`st-v3-${id}`}
role="button"
tabIndex={0}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
navigate({ navigate({
to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const, to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const,
params: { params: {
projectId: currentProject.id, projectId: currentProject.id,
identityId: id identityId: id
} }
}) });
} }
> }}
<Td>{name}</Td> onClick={() =>
navigate({
to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const,
params: {
projectId: currentProject.id,
identityId: id
}
})
}
>
<Td>{name}</Td>
<Td> <Td>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{roles {roles
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map( .map(
({ ({
role, role,
customRoleName, customRoleName,
id: roleId, id: roleId,
isTemporary, isTemporary,
temporaryAccessEndTime temporaryAccessEndTime
}) => { }) => {
const isExpired = const isExpired =
new Date() > new Date(temporaryAccessEndTime || ("" as string)); new Date() > new Date(temporaryAccessEndTime || ("" as string));
return ( return (
<Tag key={roleId}> <Tag key={roleId}>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="capitalize"> <div className="capitalize">
{formatProjectRoleName(role, customRoleName)} {formatProjectRoleName(role, customRoleName)}
</div>
{isTemporary && (
<div>
<Tooltip
content={
isExpired
? "Timed role expired"
: "Timed role access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(isExpired && "text-red-600")}
/>
</Tooltip>
</div>
)}
</div> </div>
</Tag> {isTemporary && (
); <div>
} <Tooltip
)} content={
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && ( isExpired ? "Timed role expired" : "Timed role access"
<HoverCard> }
<HoverCardTrigger> >
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag> <FontAwesomeIcon
</HoverCardTrigger> icon={faClock}
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4"> className={twMerge(isExpired && "text-red-600")}
{roles />
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE) </Tooltip>
.map( </div>
({ )}
role, </div>
customRoleName, </Tag>
id: roleId, );
isTemporary, }
temporaryAccessEndTime
}) => {
const isExpired =
new Date() >
new Date(temporaryAccessEndTime || ("" as string));
return (
<Tag key={roleId} className="capitalize">
<div className="flex items-center space-x-2">
<div>
{formatProjectRoleName(role, customRoleName)}
</div>
{isTemporary && (
<div>
<Tooltip
content={
isExpired
? "Access expired"
: "Temporary access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(
new Date() >
new Date(
temporaryAccessEndTime as string
) && "text-red-600"
)}
/>
</Tooltip>
</div>
)}
</div>
</Tag>
);
}
)}
</HoverCardContent>
</HoverCard>
)} )}
</div> {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
</Td> <HoverCard>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td> <HoverCardTrigger>
<Td className="flex justify-end space-x-2"> <Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
<Tooltip className="max-w-sm text-center" content="Options"> </HoverCardTrigger>
<DropdownMenu> <HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
<DropdownMenuTrigger asChild> {roles
<IconButton .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
ariaLabel="Options" .map(
colorSchema="secondary" ({
className="w-6" role,
variant="plain" customRoleName,
> id: roleId,
<FontAwesomeIcon icon={faEllipsisV} /> isTemporary,
</IconButton> temporaryAccessEndTime
</DropdownMenuTrigger> }) => {
<DropdownMenuContent sideOffset={2} align="end"> const isExpired =
<ProjectPermissionCan new Date() >
I={ProjectPermissionActions.Delete} new Date(temporaryAccessEndTime || ("" as string));
a={subject(ProjectPermissionSub.Identity, { return (
identityId: id <Tag key={roleId} className="capitalize">
})} <div className="flex items-center space-x-2">
> <div>{formatProjectRoleName(role, customRoleName)}</div>
{(isAllowed) => ( {isTemporary && (
<DropdownMenuItem <div>
icon={<FontAwesomeIcon icon={faCircleXmark} />} <Tooltip
isDisabled={!isAllowed} content={
onClick={(evt) => { isExpired
evt.stopPropagation(); ? "Access expired"
evt.preventDefault(); : "Temporary access"
handlePopUpOpen("deleteIdentity", { }
identityId: id, >
name <FontAwesomeIcon
}); icon={faClock}
}} className={twMerge(
> new Date() >
Remove Identity From Project new Date(
</DropdownMenuItem> temporaryAccessEndTime as string
) && "text-red-600"
)}
/>
</Tooltip>
</div>
)}
</div>
</Tag>
);
}
)} )}
</ProjectPermissionCan> </HoverCardContent>
</DropdownMenuContent> </HoverCard>
</DropdownMenu> )}
</Tooltip> </div>
</Td> </Td>
</Tr> <Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
); <Td className="flex justify-end space-x-2">
})} <Tooltip className="max-w-sm text-center" content="Options">
</TBody> <DropdownMenu>
</Table> <DropdownMenuTrigger asChild>
{!isPending && data && totalCount > 0 && ( <IconButton
<Pagination ariaLabel="Options"
count={totalCount} colorSchema="secondary"
page={page} className="w-6"
perPage={perPage} variant="plain"
onChangePage={(newPage) => setPage(newPage)} >
onChangePerPage={handlePerPageChange} <FontAwesomeIcon icon={faEllipsisV} />
/> </IconButton>
)} </DropdownMenuTrigger>
{!isPending && data && data?.identityMemberships.length === 0 && ( <DropdownMenuContent sideOffset={2} align="end">
<EmptyState <ProjectPermissionCan
title={ I={ProjectPermissionActions.Delete}
debouncedSearch.trim().length > 0 a={subject(ProjectPermissionSub.Identity, {
? "No identities match search filter" identityId: id
: "No identities have been added to this project" })}
} >
icon={faServer} {(isAllowed) => (
/> <DropdownMenuItem
)} icon={<FontAwesomeIcon icon={faCircleXmark} />}
</TableContainer> isDisabled={!isAllowed}
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> onClick={(evt) => {
<DeleteActionModal evt.stopPropagation();
isOpen={popUp.deleteIdentity.isOpen} evt.preventDefault();
title={`Are you sure you want to remove ${ handlePopUpOpen("deleteIdentity", {
(popUp?.deleteIdentity?.data as { name: string })?.name || "" identityId: id,
} from the project?`} name
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)} });
deleteKey="confirm" }}
onDeleteApproved={() => >
onRemoveIdentitySubmit( Remove Identity From Project
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId </DropdownMenuItem>
) )}
} </ProjectPermissionCan>
/> </DropdownMenuContent>
</div> </DropdownMenu>
</motion.div> </Tooltip>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && data && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
{!isPending && data && data?.identityMemberships.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0
? "No identities match search filter"
: "No identities have been added to this project"
}
icon={faServer}
/>
)}
</TableContainer>
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure you want to remove ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveIdentitySubmit(
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
)
}
/>
</div>
); );
}, },
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity } { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity }
@@ -1,5 +1,3 @@
import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
@@ -7,17 +5,7 @@ import { MembersSection } from "./components";
export const MembersTab = withProjectPermission( export const MembersTab = withProjectPermission(
() => { () => {
return ( return <MembersSection />;
<motion.div
key="panel-project-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<MembersSection />
</motion.div>
);
}, },
{ {
action: ProjectPermissionActions.Read, action: ProjectPermissionActions.Read,
@@ -1,5 +1,3 @@
import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
@@ -7,17 +5,7 @@ import { ProjectRoleList } from "./components/ProjectRoleList";
export const ProjectRoleListTab = withProjectPermission( export const ProjectRoleListTab = withProjectPermission(
() => { () => {
return ( return <ProjectRoleList />;
<motion.div
key="role-list"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectRoleList />
</motion.div>
);
}, },
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Role } { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Role }
); );
@@ -1,20 +1,12 @@
// import { faWarning } from "@fortawesome/free-solid-svg-icons"; // import { faWarning } from "@fortawesome/free-solid-svg-icons";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; // import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { ServiceTokenSection } from "./components"; import { ServiceTokenSection } from "./components";
export const ServiceTokenTab = () => { export const ServiceTokenTab = () => {
return ( return (
<motion.div <div className="space-y-3">
key="panel-service-token" {/* <div className="flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="space-y-3">
{/* <div className="flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
<FontAwesomeIcon icon={faWarning} className="pr-6 text-4xl text-white/80" /> <FontAwesomeIcon icon={faWarning} className="pr-6 text-4xl text-white/80" />
<div className="flex w-full flex-col text-sm"> <div className="flex w-full flex-col text-sm">
<span className="mb-4 text-lg font-medium">Deprecation Notice</span> <span className="mb-4 text-lg font-medium">Deprecation Notice</span>
@@ -42,8 +34,7 @@ export const ServiceTokenTab = () => {
</p> </p>
</div> </div>
</div> */} </div> */}
<ServiceTokenSection /> <ServiceTokenSection />
</div> </div>
</motion.div>
); );
}; };
@@ -21,7 +21,7 @@ export const AppConnectionsPage = withProjectPermission(
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
className="w-full" className="w-full"
@@ -8,13 +8,13 @@ export const AuditLogsPage = () => {
const { currentProject } = useProject(); const { currentProject } = useProject();
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet> <Helmet>
<title>Project Audit Logs</title> <title>Project Audit Logs</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title="Audit logs" title="Audit logs"
@@ -1,11 +1,16 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useParams } from "@tanstack/react-router";
import { formatRelative } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, PageHeader, Spinner } from "@app/components/v2"; import { EmptyState, PageHeader, Spinner } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import { useGetWorkspaceGroupMembershipDetails } from "@app/hooks/api/projects/queries"; import { useGetWorkspaceGroupMembershipDetails } from "@app/hooks/api/projects/queries";
import { ProjectAccessControlTabs } from "@app/types/project";
import { GroupDetailsSection } from "./components/GroupDetailsSection"; import { GroupDetailsSection } from "./components/GroupDetailsSection";
import { GroupMembersSection } from "./components/GroupMembersSection"; import { GroupMembersSection } from "./components/GroupMembersSection";
@@ -31,10 +36,27 @@ const Page = () => {
); );
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{groupMembership ? ( {groupMembership ? (
<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={groupMembership.group.name} /> <Link
to={`${getProjectBaseURL(currentProject.type)}/access-management`}
params={{
projectId: currentProject.id
}}
search={{
selectedTab: ProjectAccessControlTabs.Groups
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Groups
</Link>
<PageHeader
scope="project"
title={groupMembership.group.name}
description={`Group joined on ${groupMembership?.createdAt && formatRelative(new Date(groupMembership?.createdAt || ""), new Date())}`}
/>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<GroupDetailsSection groupMembership={groupMembership} /> <GroupDetailsSection groupMembership={groupMembership} />
@@ -1,7 +1,9 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { useNavigate, useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { formatRelative } from "date-fns"; import { formatRelative } from "date-fns";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -28,6 +30,7 @@ import {
useGetWorkspaceIdentityMembershipDetails useGetWorkspaceIdentityMembershipDetails
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { ProjectAccessControlTabs } from "@app/types/project";
import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection"; import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection";
import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSection"; import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSection";
@@ -113,9 +116,22 @@ const Page = () => {
} }
return ( return (
<div className="container 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">
{identityMembershipDetails ? ( {identityMembershipDetails ? (
<> <>
<Link
to={`${getProjectBaseURL(currentProject.type)}/access-management`}
params={{
projectId
}}
search={{
selectedTab: ProjectAccessControlTabs.Identities
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Identities
</Link>
<PageHeader <PageHeader
scope="project" scope="project"
title={identityMembershipDetails?.identity?.name} title={identityMembershipDetails?.identity?.name}
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { formatRelative } from "date-fns"; import { formatRelative } from "date-fns";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -29,6 +31,7 @@ import {
useGetWorkspaceUserDetails useGetWorkspaceUserDetails
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { ProjectAccessControlTabs } from "@app/types/project";
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection"; import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection"; import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
@@ -115,9 +118,22 @@ export const Page = () => {
} }
return ( return (
<div className="container 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">
{membershipDetails ? ( {membershipDetails ? (
<> <>
<Link
to={`${getProjectBaseURL(currentProject.type)}/access-management`}
params={{
projectId: currentProject.id
}}
search={{
selectedTab: ProjectAccessControlTabs.Member
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Users
</Link>
<PageHeader <PageHeader
scope="project" scope="project"
title={ title={
@@ -1,8 +1,14 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faCopy, faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons"; import {
faChevronLeft,
faCopy,
faEdit,
faEllipsisV,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -86,20 +92,29 @@ const Page = () => {
); );
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to={`${getProjectBaseURL(currentProject.type)}/access-management`}
params={{
projectId
}}
search={{
selectedTab: ProjectAccessControlTabs.Roles
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Roles
</Link>
<PageHeader <PageHeader
scope="project" scope="project"
title={ title={data.name}
<div className="flex flex-col"> description={
<div> <>
<span>{data.name}</span> {data.slug} {data.description && `- ${data.description}`}
<p className="text-sm leading-3 font-normal text-mineshaft-400 normal-case"> </>
{data.slug} {data.description && `- ${data.description}`}
</p>
</div>
</div>
} }
> >
{isCustomRole && ( {isCustomRole && (
@@ -64,7 +64,7 @@ export const CommitDetailsPage = () => {
}; };
return ( 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]"> <div className="w-full max-w-[75vw]">
<ProjectPermissionCan <ProjectPermissionCan
renderGuardBanner renderGuardBanner
@@ -297,7 +297,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
}; };
return ( 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 <ProjectPermissionCan
renderGuardBanner renderGuardBanner
I={ProjectPermissionCommitsActions.PerformRollback} 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 { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
@@ -46,8 +48,19 @@ export const CommitsPage = () => {
}; };
return ( return (
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white"> <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 max-w-[75vw]"> <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 <PageHeader
scope="project" scope="project"
title="Commits" title="Commits"
@@ -15,7 +15,7 @@ export const IPAllowListPage = () => {
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <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"> <div className="my-6">
<p className="text-3xl font-medium text-gray-200">IP Allowlist</p> <p className="text-3xl font-medium text-gray-200">IP Allowlist</p>
<div /> <div />
@@ -90,9 +90,9 @@ export const IntegrationDetailsByIDPage = () => {
<meta property="og:title" content="Manage your .env files in seconds" /> <meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} /> <meta name="og:description" content={t("integrations.description") as string} />
</Helmet> </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 ? ( {integration ? (
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="project" scope="project"
title={`${integrationSlugNameMapping[integration.integration]} Integration`} title={`${integrationSlugNameMapping[integration.integration]} Integration`}
@@ -45,21 +45,25 @@ export const IntegrationsListPage = () => {
<meta property="og:title" content="Manage your .env files in seconds" /> <meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} /> <meta name="og:description" content={t("integrations.description") as string} />
</Helmet> </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"> <div className="mb-8">
<PageHeader <PageHeader
scope="project" scope="project"
title="Integrations" title="Integrations"
description="Manage integrations with third-party services." description="Manage integrations with third-party services."
/> />
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab value={IntegrationsListPageTabs.SecretSyncs}>Secret Syncs</Tab> <Tab variant="project" value={IntegrationsListPageTabs.SecretSyncs}>
<Tab value={IntegrationsListPageTabs.NativeIntegrations}>Native Integrations</Tab> Secret Syncs
<Tab value={IntegrationsListPageTabs.FrameworkIntegrations}> </Tab>
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
Native Integrations
</Tab>
<Tab variant="project" value={IntegrationsListPageTabs.FrameworkIntegrations}>
Framework Integrations Framework Integrations
</Tab> </Tab>
<Tab value={IntegrationsListPageTabs.InfrastructureIntegrations}> <Tab variant="project" value={IntegrationsListPageTabs.InfrastructureIntegrations}>
Infrastructure Integrations Infrastructure Integrations
</Tab> </Tab>
</TabList> </TabList>
@@ -911,7 +911,7 @@ export const OverviewPage = () => {
<meta property="og:title" content={String(t("dashboard.og-title"))} /> <meta property="og:title" content={String(t("dashboard.og-title"))} />
<meta name="og:description" content={String(t("dashboard.og-description"))} /> <meta name="og:description" content={String(t("dashboard.og-description"))} />
</Helmet> </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"> <div className="flex w-full items-baseline justify-between">
<PageHeader <PageHeader
scope="project" scope="project"
@@ -959,7 +959,7 @@ export const OverviewPage = () => {
} }
/> />
</div> </div>
<div className="mt-4 flex items-center justify-between"> <div className="flex items-center justify-between">
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} /> <FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
<div className="flex flex-row items-center justify-center space-x-2"> <div className="flex flex-row items-center justify-center space-x-2">
{isTableFiltered && ( {isTableFiltered && (
@@ -46,7 +46,7 @@ export const QuickSearchDynamicSecretItem = ({
</span> </span>
<span className="text-xs text-mineshaft-400"> <span className="text-xs text-mineshaft-400">
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "} <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> <span>{reverseTruncate(groupDynamicSecret.path)}</span>
</Tooltip> </Tooltip>
</span> </span>
@@ -32,7 +32,7 @@ export const QuickSearchFolderItem = ({ folderGroup, onClose }: Props) => {
> >
<Td className="w-full whitespace-nowrap"> <Td className="w-full whitespace-nowrap">
<FontAwesomeIcon className="text-yellow-700" icon={faFolder} /> <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> <div className="ml-2 inline-block">{reverseTruncate(groupFolder.path)}</div>
</Tooltip> </Tooltip>
</Td> </Td>
@@ -126,7 +126,7 @@ export const QuickSearchSecretItem = ({
</span> </span>
<span className="text-xs text-mineshaft-400"> <span className="text-xs text-mineshaft-400">
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "} <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> <span>{reverseTruncate(groupSecret.path ?? "")}</span>
</Tooltip> </Tooltip>
</span> </span>
@@ -42,7 +42,7 @@ export const QuickSearchSecretRotationItem = ({ secretRotationGroup, onClose }:
</span> </span>
<span className="text-xs text-mineshaft-400"> <span className="text-xs text-mineshaft-400">
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "} <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> <span>{reverseTruncate(groupSecretRotation.folder.path)}</span>
</Tooltip> </Tooltip>
</span> </span>
@@ -1,7 +1,7 @@
export const getExpandedRowStyle = (scrollOffset: number) => ({ export const getExpandedRowStyle = (scrollOffset: number) => ({
marginLeft: scrollOffset, marginLeft: scrollOffset,
width: "calc(100vw - 275px)", // accounts for sidebar and margin width: "calc(100vw - 110px)", // accounts for sidebar and margin
maxWidth: "1270px" // largest width of table on ultra-wide maxWidth: "1404px" // largest width of table on ultra-wide
}); });
type GetHeaderStyleParams = { type GetHeaderStyleParams = {
@@ -38,27 +38,29 @@ export const SecretApprovalsPage = () => {
<meta property="og:title" content={String(t("approval.og-title"))} /> <meta property="og:title" content={String(t("approval.og-title"))} />
<meta name="og:description" content={String(t("approval.og-description"))} /> <meta name="og:description" content={String(t("approval.og-description"))} />
</Helmet> </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 <PageHeader
scope="project" scope="project"
title="Approval Workflows" title="Approval Workflows"
description="Create approval policies for any modifications to secrets in sensitive environments and folders." description="Create approval policies for any modifications to secrets in sensitive environments and folders."
/> />
<Tabs defaultValue={defaultTab}> <Tabs orientation="vertical" defaultValue={defaultTab}>
<TabList> <TabList>
<Tab value={TabSection.SecretApprovalRequests}> <Tab variant="project" value={TabSection.SecretApprovalRequests}>
Change Requests Change Requests
{Boolean(secretApprovalReqCount?.open) && ( {Boolean(secretApprovalReqCount?.open) && (
<Badge className="ml-2">{secretApprovalReqCount?.open}</Badge> <Badge className="ml-2">{secretApprovalReqCount?.open}</Badge>
)} )}
</Tab> </Tab>
<Tab value={TabSection.ResourceApprovalRequests}> <Tab variant="project" value={TabSection.ResourceApprovalRequests}>
Access Requests Access Requests
{Boolean(accessApprovalRequestCount?.pendingCount) && ( {Boolean(accessApprovalRequestCount?.pendingCount) && (
<Badge className="ml-2">{accessApprovalRequestCount?.pendingCount}</Badge> <Badge className="ml-2">{accessApprovalRequestCount?.pendingCount}</Badge>
)} )}
</Tab> </Tab>
<Tab value={TabSection.Policies}>Policies</Tab> <Tab variant="project" value={TabSection.Policies}>
Policies
</Tab>
</TabList> </TabList>
<TabPanel value={TabSection.SecretApprovalRequests}> <TabPanel value={TabSection.SecretApprovalRequests}>
<SecretApprovalRequest /> <SecretApprovalRequest />
@@ -19,7 +19,6 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistance } from "date-fns"; import { format, formatDistance } from "date-fns";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -298,108 +297,132 @@ export const AccessApprovalRequest = ({
const isFiltered = Boolean(search || envFilter || requestedByFilter); const isFiltered = Boolean(search || envFilter || requestedByFilter);
return ( return (
<AnimatePresence mode="wait"> <>
<motion.div <div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
key="approval-changes-list" <div className="mb-4 flex items-center justify-between">
transition={{ duration: 0.1 }} <div>
initial={{ opacity: 0, translateX: 30 }} <div className="flex items-start gap-1">
animate={{ opacity: 1, translateX: 0 }} <p className="text-xl font-medium text-mineshaft-100">Access Requests</p>
exit={{ opacity: 0, translateX: 30 }} <a
className="rounded-md text-gray-300" href="https://infisical.com/docs/documentation/platform/access-controls/access-requests"
> target="_blank"
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> rel="noopener noreferrer"
<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}
> >
Request Access <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">
</Button> <FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
</Tooltip> <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> </div>
<Input <Tooltip
value={search} content="To submit Access Requests, your project needs to create Access Request policies first."
onChange={(e) => setSearch(e.target.value)} isDisabled={policiesLoading || !!policies?.length}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} >
placeholder="Search approval requests by requesting user or environment..." <Button
className="flex-1" onClick={() => {
containerClassName="mb-4" if (subscription && !subscription?.secretApproval) {
/> handlePopUpOpen("upgradePlan");
<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"> return;
<div }
role="button" handlePopUpOpen("requestAccess");
tabIndex={0}
onClick={() => setStatusFilter("open")}
onKeyDown={(evt) => {
if (evt.key === "Enter") setStatusFilter("open");
}} }}
className={twMerge( colorSchema="secondary"
"font-medium", leftIcon={<FontAwesomeIcon icon={faPlus} />}
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400" isDisabled={policiesLoading || !policies?.length}
)}
> >
<FontAwesomeIcon icon={faLock} className="mr-2" /> Request Access
{!!requestCount && requestCount?.pendingCount} Pending </Button>
</div> </Tooltip>
<div </div>
className={twMerge( <Input
"font-medium", value={search}
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400" onChange={(e) => setSearch(e.target.value)}
)} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
role="button" placeholder="Search approval requests by requesting user or environment..."
tabIndex={0} className="flex-1"
onClick={() => setStatusFilter("close")} containerClassName="mb-4"
onKeyDown={(evt) => { />
if (evt.key === "Enter") setStatusFilter("close"); <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"
<FontAwesomeIcon icon={faCheck} className="mr-2" /> tabIndex={0}
{!!requestCount && requestCount.finalizedCount} Closed onClick={() => setStatusFilter("open")}
</div> onKeyDown={(evt) => {
<div className="flex grow justify-end space-x-8"> 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> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Button <Button
variant="plain" variant="plain"
colorSchema="secondary" 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" />} rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
> >
Environments Requested By
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
@@ -408,210 +431,172 @@ export const AccessApprovalRequest = ({
className="max-h-80 thin-scrollbar overflow-y-auto" className="max-h-80 thin-scrollbar overflow-y-auto"
> >
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900"> <DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Select an Environment Select Requesting User
</DropdownMenuLabel> </DropdownMenuLabel>
{currentProject?.environments.map(({ slug, name }) => ( {members?.map(({ user: membershipUser, id }) => (
<DropdownMenuItem <DropdownMenuItem
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))} onClick={() =>
key={`request-filter-${slug}`} setRequestedByFilter((state) =>
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />} state === membershipUser.id ? undefined : membershipUser.id
)
}
key={`request-filter-member-${id}`}
icon={
requestedByFilter === membershipUser.id && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right" iconPos="right"
> >
{name} {membershipUser.username}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </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>
</div> </div>
{!!policies && ( <div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
<RequestAccessModal {filteredRequests?.length === 0 && !isFiltered && (
policies={policies} <div className="py-12">
isOpen={popUp.requestAccess.isOpen} <EmptyState
onOpenChange={() => { title={`No ${statusFilter === "open" ? "Pending" : "Closed"} Access Requests`}
queryClient.invalidateQueries({ />
queryKey: accessApprovalKeys.getAccessApprovalRequests( </div>
projectSlug, )}
envFilter, {Boolean(!filteredRequests?.length && isFiltered && !areRequestsPending) && (
requestedByFilter <div className="py-12">
) <EmptyState title="No Requests Match Filters" icon={faSearch} />
}); </div>
handlePopUpClose("requestAccess"); )}
}} {!!filteredRequests?.length &&
/> filteredRequests?.slice(offset, perPage * page).map((request) => {
)} const details = generateRequestDetails(request);
{!!selectedRequest && ( return (
<ReviewAccessRequestModal <div
selectedEnvSlug={envFilter} key={request.id}
policies={policies || []} 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"
selectedRequester={requestedByFilter} role="button"
projectSlug={projectSlug} tabIndex={0}
request={selectedRequest} onClick={() => handleSelectRequest(request)}
members={members || []} onKeyDown={(evt) => {
isOpen={popUp.reviewRequest.isOpen} if (evt.key === "Enter") {
onOpenChange={() => { handleSelectRequest(request);
handlePopUpClose("reviewRequest"); }
setSelectedRequest(null); }}
refetchRequests(); >
}} <div className="flex w-full items-center justify-between">
onUpdate={(request) => { <div className="flex w-full flex-col justify-between">
// scott: this isn't ideal but our current use of state makes this complicated... <div className="mb-1 flex w-full items-center">
// we shouldn't be using state like this... <FontAwesomeIcon
handleSelectRequest({ icon={faLock}
...selectedRequest, size="xs"
isTemporary: request.isTemporary, className="mr-1.5 text-mineshaft-300"
temporaryRange: request.temporaryRange, />
reviewers: [] {generateRequestText(request)}
}); </div>
}} <div className="flex items-center justify-between">
canBypass={generateRequestDetails(selectedRequest).canBypass} <div className="text-xs leading-3 text-gray-500">
/> {membersGroupById?.[request.requestedByUserId]?.user && (
)} <>
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
<UpgradePlanModal ago by{" "}
text="You need to upgrade your plan to access this feature" {membersGroupById?.[request.requestedByUserId]?.user?.firstName}{" "}
isOpen={popUp.upgradePlan.isOpen} {membersGroupById?.[request.requestedByUserId]?.user?.lastName} (
onOpenChange={() => handlePopUpClose("upgradePlan")} {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 faSearch
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -240,262 +239,253 @@ export const ApprovalPolicyList = ({ projectId }: IProps) => {
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
return ( return (
<AnimatePresence mode="wait"> <>
<motion.div <div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
key="approval-changes-list" <div className="mb-4 flex items-center justify-between">
transition={{ duration: 0.1 }} <div>
initial={{ opacity: 0, translateX: 30 }} <div className="flex items-start gap-1">
animate={{ opacity: 1, translateX: 0 }} <p className="text-xl font-medium text-mineshaft-100">Policies</p>
exit={{ opacity: 0, translateX: 30 }} <a
className="rounded-md text-gray-300" href="https://infisical.com/docs/documentation/platform/pr-workflows"
> target="_blank"
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> rel="noopener noreferrer"
<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"
> >
<DropdownMenuLabel>Policy Type</DropdownMenuLabel> <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">
<DropdownMenuItem <FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
onClick={() => <span>Docs</span>
setFilters((prev) => ({ <FontAwesomeIcon
...prev, icon={faArrowUpRightFromSquare}
type: null className="mb-[0.07rem] ml-1.5 text-[10px]"
}))
}
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>
{!isPoliciesLoading && !policies?.length && ( </a>
<Tr> </div>
<Td colSpan={5}> <p className="text-sm text-bunker-300">
<EmptyState title="No Policies Found" icon={faFileShield} /> Implement granular policies for access requests and secrets management
</Td> </p>
</Tr> </div>
)} <ProjectPermissionCan
{!!currentProject && I={ProjectPermissionActions.Create}
filteredPolicies a={ProjectPermissionSub.SecretApproval}
?.slice(offset, perPage * page) >
.map((policy) => ( {(isAllowed) => (
<ApprovalPolicyRow <Button
policy={policy} onClick={() => {
key={policy.id} if (subscription && !subscription?.secretApproval) {
members={members} handlePopUpOpen("upgradePlan");
groups={groups} return;
onEdit={() => handlePopUpOpen("policyForm", policy)} }
onDelete={() => handlePopUpOpen("deletePolicy", policy)} handlePopUpOpen("policyForm");
/> }}
))} colorSchema="secondary"
</TBody> leftIcon={<FontAwesomeIcon icon={faPlus} />}
</Table> isDisabled={!isAllowed}
{Boolean(!filteredPolicies.length && policies.length && !isPoliciesLoading) && ( >
<EmptyState title="No Policies Match Search" icon={faSearch} /> Create Policy
</Button>
)} )}
{Boolean(filteredPolicies.length) && ( </ProjectPermissionCan>
<Pagination
count={filteredPolicies.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
</TableContainer>
</div> </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 <AccessPolicyForm
projectId={currentProject.id} projectId={currentProject.id}
projectSlug={currentProject.slug} projectSlug={currentProject.slug}
@@ -517,6 +507,6 @@ export const ApprovalPolicyList = ({ projectId }: IProps) => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add secret approval policy if you switch to Infisical's Enterprise plan." 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSearch } from "@tanstack/react-router"; import { useSearch } from "@tanstack/react-router";
import { format, formatDistance } from "date-fns"; import { format, formatDistance } from "date-fns";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { import {
@@ -131,284 +130,250 @@ export const SecretApprovalRequest = () => {
const isFiltered = Boolean(searchFilter || envFilter || committerFilter); const isFiltered = Boolean(searchFilter || envFilter || committerFilter);
return ( return isSecretApprovalScreen ? (
<AnimatePresence mode="wait"> <SecretApprovalRequestChanges
{isSecretApprovalScreen ? ( approvalRequestId={selectedApprovalId || ""}
<motion.div onGoBack={handleGoBackSecretRequestDetail}
key="approval-changes-details" />
transition={{ duration: 0.1 }} ) : (
initial={{ opacity: 0, translateX: 30 }} <div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
animate={{ opacity: 1, translateX: 0 }} <div className="mb-4 flex items-center justify-between">
exit={{ opacity: 0, translateX: 30 }} <div>
> <div className="flex items-start gap-1">
<SecretApprovalRequestChanges <p className="text-xl font-medium text-mineshaft-100">Change Requests</p>
approvalRequestId={selectedApprovalId || ""} <a
onGoBack={handleGoBackSecretRequestDetail} href="https://infisical.com/docs/documentation/platform/pr-workflows"
/> target="_blank"
</motion.div> rel="noopener noreferrer"
) : ( >
<motion.div <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">
key="approval-changes-list" <FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
transition={{ duration: 0.1 }} <span>Docs</span>
initial={{ opacity: 0, translateX: 30 }} <FontAwesomeIcon
animate={{ opacity: 1, translateX: 0 }} icon={faArrowUpRightFromSquare}
exit={{ opacity: 0, translateX: 30 }} className="mb-[0.07rem] ml-1.5 text-[10px]"
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}
/> />
)} </div>
{isApprovalRequestLoading && ( </a>
<div> </div>
{Array.apply(0, Array(3)).map((_x, index) => ( <p className="text-sm text-bunker-300">Review pending and closed change requests</p>
<div </div>
key={`approval-request-loading-${index + 1}`} </div>
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700" <Input
> value={searchFilter}
<div className="mb-2 flex items-center"> onChange={(e) => setSearchFilter(e.target.value)}
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" /> leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
<Skeleton className="w-1/4 bg-mineshaft-600" /> placeholder="Search change requests by author, environment or policy path..."
</div> className="flex-1"
<Skeleton className="w-1/2 bg-mineshaft-600" /> containerClassName="mb-4"
</div> />
))} <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> </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> </div>
);
})}
{Boolean(!secretApprovalRequests.length && isFiltered && !isApprovalRequestLoading) && (
<div className="py-12">
<EmptyState title="No Requests Match Filters" icon={faSearch} />
</div> </div>
</motion.div> )}
)} {Boolean(totalApprovalCount) && (
</AnimatePresence> <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>
); );
}; };

Some files were not shown because too many files have changed in this diff Show More