mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 00:27:30 +00:00
style: implement new navigation
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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";
|
|
||||||
@@ -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">
|
||||||
|
|||||||
+2
-2
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+16
-4
@@ -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">
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+16
-4
@@ -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">
|
||||||
|
|||||||
+16
-4
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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
-13
@@ -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
-13
@@ -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
-13
@@ -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
-13
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -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")}
|
||||||
|
|||||||
+14
-18
@@ -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">
|
||||||
|
|||||||
+17
-4
@@ -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"
|
||||||
|
|||||||
+1
-1
@@ -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}>
|
||||||
|
|||||||
+1
-1
@@ -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,
|
||||||
|
|||||||
+276
-289
@@ -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
-13
@@ -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 }
|
||||||
);
|
);
|
||||||
|
|||||||
+4
-13
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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 />
|
||||||
|
|||||||
+2
-2
@@ -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 && (
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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 />
|
||||||
|
|||||||
+269
-284
@@ -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")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+244
-254
@@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+240
-275
@@ -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
Reference in New Issue
Block a user