feat: completed org breadcrumbs

This commit is contained in:
=
2025-01-20 22:58:59 +05:30
parent 338961480c
commit 1b1fe2a700
29 changed files with 514 additions and 295 deletions
@@ -0,0 +1,98 @@
import * as React from "react";
import { faChevronRight, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={twMerge(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-bunker-100 sm:gap-2.5",
className
)}
{...props}
/>
)
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={twMerge("inline-flex items-center gap-1.5", className)} {...props} />
)
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
return (
<li
ref={ref}
className={twMerge("transition-colors hover:text-bunker-200", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={twMerge("font-normal text-bunker-200 last:text-bunker-300", className)}
{...props}
/>
)
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={twMerge("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
{...props}
>
{children ?? <FontAwesomeIcon icon={faChevronRight} />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={twMerge("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<FontAwesomeIcon icon={faEllipsis} className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
};
@@ -0,0 +1 @@
export * from "./Breadcrumb";
@@ -0,0 +1,21 @@
import { ReactNode } from "@tanstack/react-router";
type Props = {
title: ReactNode;
description?: ReactNode;
children?: ReactNode;
};
export const PageHeader = ({ title, description, children }: Props) => (
<div className="mb-4">
<div className="flex w-full justify-between">
<div>
<h1 className="mr-4 text-3xl font-semibold capitalize text-white">{title}</h1>
</div>
<div>{children}</div>
</div>
<div>
<p className="mt-2 text-gray-400">{description}</p>
</div>
</div>
);
@@ -0,0 +1 @@
export { PageHeader } from "./PageHeader";
+2
View File
@@ -2,6 +2,7 @@
export * from "./Accordion"; export * from "./Accordion";
export * from "./Alert"; export * from "./Alert";
export * from "./Badge"; export * from "./Badge";
export * from "./Breadcrumb";
export * from "./Button"; export * from "./Button";
export * from "./Card"; export * from "./Card";
export * from "./Checkbox"; export * from "./Checkbox";
@@ -21,6 +22,7 @@ export * from "./Input";
export * from "./Menu"; export * from "./Menu";
export * from "./Modal"; export * from "./Modal";
export * from "./NoticeBanner"; export * from "./NoticeBanner";
export * from "./PageHeader";
export * from "./Pagination"; export * from "./Pagination";
export * from "./Popoverv2"; export * from "./Popoverv2";
export * from "./SecretInput"; export * from "./SecretInput";
@@ -3,14 +3,24 @@ import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons"; import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-router"; import { Link, Outlet, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Mfa } from "@app/components/auth/Mfa"; import { Mfa } from "@app/components/auth/Mfa";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { Menu, MenuGroup, MenuItem } from "@app/components/v2"; import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Menu,
MenuGroup,
MenuItem
} from "@app/components/v2";
import { useUser } from "@app/context"; import { useUser } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { useSelectOrganization, workspaceKeys } from "@app/hooks/api"; import { useSelectOrganization, workspaceKeys } from "@app/hooks/api";
@@ -32,6 +42,8 @@ export const OrganizationLayout = ({ children }: Props) => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false); const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
const { user } = useUser(); const { user } = useUser();
@@ -252,7 +264,34 @@ export const OrganizationLayout = ({ children }: Props) => {
isOpen={popUp?.createOrg?.isOpen} isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)} onClose={() => handlePopUpToggle("createOrg", false)}
/> />
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]"> <main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
{breadcrumbs && (
<div className="mx-auto max-w-7xl py-4 capitalize text-white">
<Breadcrumb>
<BreadcrumbList>
{breadcrumbs.map((el, index) => {
const isNotLastCrumb = index + 1 !== breadcrumbs.length;
return (
<>
{el.link && isNotLastCrumb && !("disabled" in el.link) ? (
<Link {...el.link}>
<BreadcrumbItem>
<BreadcrumbLink>{el.label}</BreadcrumbLink>
</BreadcrumbItem>
</Link>
) : (
<BreadcrumbItem>
<BreadcrumbPage>{el.label}</BreadcrumbPage>
</BreadcrumbItem>
)}
{isNotLastCrumb && <BreadcrumbSeparator />}
</>
);
})}
</BreadcrumbList>
</Breadcrumb>
</div>
)}
{children || <Outlet />} {children || <Outlet />}
</main> </main>
</div> </div>
@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgAccessControlTabSections } from "@app/types/org"; import { OrgAccessControlTabSections } from "@app/types/org";
@@ -59,8 +59,11 @@ export const AccessManagementPage = () => {
<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 px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<p className="mb-4 mr-4 text-3xl font-semibold text-white">Organization Access Control</p> <PageHeader
title="Organization Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
{tabSections {tabSections
@@ -1,4 +1,4 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -19,5 +19,16 @@ export const Route = createFileRoute(
search: { search: {
// strip default values // strip default values
middlewares: [stripSearchParams({ action: "" })] middlewares: [stripSearchParams({ action: "" })]
} },
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/" })
},
{
label: "access control"
}
]
})
}); });
@@ -2,7 +2,7 @@ import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgAdminProjects } from "./components/OrgAdminProjects"; import { OrgAdminProjects } from "./components/OrgAdminProjects";
@@ -17,13 +17,13 @@ export const AdminPage = () => {
<> <>
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-6xl px-6"> <div className="w-full max-w-7xl">
<div className="mb-4"> <PageHeader
<p className="text-3xl font-semibold text-gray-200">Organization Admin Console</p> title="Organization Admin Console"
</div> description="View and manage resources across your organization."
/>
<Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}> <Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
<TabList> <TabList>
<Tab value={TabSections.Projects}>Projects</Tab> <Tab value={TabSections.Projects}>Projects</Tab>
@@ -1,9 +1,20 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AdminPage } from "./AdminPage"; import { AdminPage } from "./AdminPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/admin" "/_authenticate/_inject-org-details/organization/_layout/admin"
)({ )({
component: AdminPage component: AdminPage,
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Admin Console"
}
]
})
}); });
@@ -1,6 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { LogsSection } from "./components"; import { LogsSection } from "./components";
import { PageHeader } from "@app/components/v2";
export const AuditLogsPage = () => { export const AuditLogsPage = () => {
return ( return (
@@ -11,11 +12,11 @@ export const AuditLogsPage = () => {
<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 px-6"> <div className="w-full max-w-7xl">
<div className="bg-bunker-800 py-6"> <PageHeader
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p> title="Audit logs"
<div /> description="Audit logs for security and compliance teams to monitor information access."
</div> />
<LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn /> <LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn />
</div> </div>
</div> </div>
@@ -1,9 +1,20 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage"; import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/audit-logs" "/_authenticate/_inject-org-details/organization/_layout/audit-logs"
)({ )({
component: AuditLogsPage component: AuditLogsPage,
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Audit Logs"
}
]
})
}); });
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
link: linkOptions({ disabled: true, to: "/" }) link: linkOptions({ disabled: true, to: "/" })
}, },
{ {
label: "Cert Managers", label: "Cert Management",
link: linkOptions({ to: "/organization/cert-manager/overview" }) link: linkOptions({ to: "/organization/cert-manager/overview" })
} }
] ]
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -15,6 +13,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Spinner, Spinner,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
@@ -83,34 +82,17 @@ const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container 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 px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.group.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: TabSections.Groups
}
});
}}
className="mb-4"
>
Groups
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.group.name}</p>
<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">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -153,7 +135,7 @@ const Page = () => {
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} /> <GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,24 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/groups/$groupId" "/_authenticate/_inject-org-details/organization/_layout/groups/$groupId"
)({ )({
component: GroupDetailsByIDPage component: GroupDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "groups"
}
]
})
}); });
@@ -1,8 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -16,6 +14,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -165,30 +164,13 @@ const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container 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 px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.identity.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management",
search: {
selectedTab: OrgAccessControlTabSections.Identities
}
});
}}
className="mb-4"
>
Identities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.identity.name}</p>
<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">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -257,7 +239,7 @@ const Page = () => {
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<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} />
@@ -1,9 +1,24 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/identities/$identityId" "/_authenticate/_inject-org-details/organization/_layout/identities/$identityId"
)({ )({
component: IdentityDetailsByIDPage component: IdentityDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "identities"
}
]
})
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -14,6 +12,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -78,31 +77,14 @@ export const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container 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 px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: OrgAccessControlTabSections.Roles
}
});
}}
className="mb-4"
>
Roles
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
{isCustomRole && ( {isCustomRole && (
<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">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -144,7 +126,7 @@ export const Page = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} /> <RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,24 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleByIDPage } from "./RoleByIDPage"; import { RoleByIDPage } from "./RoleByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId" "/_authenticate/_inject-org-details/organization/_layout/roles/$roleId"
)({ )({
component: RoleByIDPage component: RoleByIDPage,
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "RBAC"
}
]
})
}); });
@@ -22,7 +22,15 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { NewProjectModal } from "@app/components/projects"; import { NewProjectModal } from "@app/components/projects";
import { Button, IconButton, Input, Pagination, Skeleton, Tooltip } from "@app/components/v2"; import {
Button,
IconButton,
Input,
PageHeader,
Pagination,
Skeleton,
Tooltip
} from "@app/components/v2";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
@@ -50,13 +58,6 @@ enum ProjectOrderBy {
Name = "name" Name = "name"
} }
const formatTitle = (type: ProjectType) => {
if (type === ProjectType.SecretManager) return "Secret Management";
if (type === ProjectType.CertificateManager) return "Cert Management";
if (type === ProjectType.KMS) return "Key Management";
return "SSH";
};
const formatDescription = (type: ProjectType) => { const formatDescription = (type: ProjectType) => {
if (type === ProjectType.SecretManager) if (type === ProjectType.SecretManager)
return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc."; return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.";
@@ -371,7 +372,7 @@ export const ProductOverviewPage = ({ type }: Props) => {
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
{!serverDetails?.redisConfigured && ( {!serverDetails?.redisConfigured && (
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl"> <div className="mb-4 flex flex-col items-start justify-start text-3xl">
<p className="mb-4 mr-4 font-semibold text-white">Announcements</p> <p className="mb-4 mr-4 font-semibold text-white">Announcements</p>
<div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100"> <div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100">
<FontAwesomeIcon <FontAwesomeIcon
@@ -393,14 +394,9 @@ export const ProductOverviewPage = ({ type }: Props) => {
</div> </div>
</div> </div>
)} )}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0"> <div className="mb-4 flex flex-col items-start justify-start">
<div className="flex w-full justify-between"> <PageHeader title="Projects" description={formatDescription(type)} />
<p className="mr-4 text-3xl font-semibold text-white">{formatTitle(type)}</p> <div className="flex w-full flex-row">
</div>
<div>
<p className="mr-4 mt-2 text-gray-400">{formatDescription(type)}</p>
</div>
<div className="mt-6 flex w-full flex-row">
<Input <Input
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80" className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by project name..." placeholder="Search by project name..."
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
link: linkOptions({ disabled: true, to: "/" }) link: linkOptions({ disabled: true, to: "/" })
}, },
{ {
label: "Secret Managers", label: "Secret Management",
link: linkOptions({ to: "/organization/secret-manager/overview" }) link: linkOptions({ to: "/organization/secret-manager/overview" })
} }
] ]
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router"; import { useSearch } from "@tanstack/react-router";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, NoticeBanner, Pagination } from "@app/components/v2"; import { Button, NoticeBanner, PageHeader, Pagination } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { import {
OrgPermissionActions, OrgPermissionActions,
@@ -111,11 +111,11 @@ export const SecretScanningPage = withPermission(
<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 px-6"> <div className="w-full max-w-7xl">
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div> <PageHeader
<div className="mb-6 text-lg text-mineshaft-300"> title="Secret Scanning"
Automatically monitor your GitHub activity and prevent secret leaks description="Automatically monitor your GitHub activity and prevent secret leaks"
</div> />
{config.isSecretScanningDisabled && ( {config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4"> <NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased We are working on improving the performance of secret scanning due to increased
@@ -210,4 +210,4 @@ export const SecretScanningPage = withPermission(
action: OrgPermissionActions.Read, action: OrgPermissionActions.Read,
subject: OrgPermissionSubjects.SecretScanning subject: OrgPermissionSubjects.SecretScanning
} }
); );
@@ -1,4 +1,4 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -12,8 +12,8 @@ const SecretScanningQueryParams = z.object({
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-scanning" "/_authenticate/_inject-org-details/organization/_layout/secret-scanning"
)({ )({
component: SecretScanningPage,
validateSearch: zodValidator(SecretScanningQueryParams), validateSearch: zodValidator(SecretScanningQueryParams),
component: SecretScanningPage,
search: { search: {
middlewares: [ middlewares: [
stripSearchParams({ stripSearchParams({
@@ -21,5 +21,16 @@ export const Route = createFileRoute(
state: "" state: ""
}) })
] ]
} },
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/" })
},
{
label: "secret scanning"
}
]
})
}); });
@@ -4,6 +4,7 @@ import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ShareSecretSection } from "./components"; import { ShareSecretSection } from "./components";
import { PageHeader } from "@app/components/v2";
export const SecretSharingPage = () => { export const SecretSharingPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -18,28 +19,25 @@ 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 px-6 text-white"> <div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
<div className="flex items-center justify-between py-6"> <PageHeader
<div className="flex w-full flex-col"> title="Secret Sharing"
<h2 className="text-3xl font-semibold text-gray-200">Secret Sharing</h2> description="Share secrets securely using a shareable link"
<p className="text-bunker-300">Share secrets securely using a shareable link</p> >
</div> <a
<div className="flex w-max justify-center"> target="_blank"
<a rel="noopener noreferrer"
target="_blank" href="https://infisical.com/docs/documentation/platform/secret-sharing"
rel="noopener noreferrer" >
href="https://infisical.com/docs/documentation/platform/secret-sharing" <div className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
> Documentation{" "}
<div className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white"> <FontAwesomeIcon
Documentation{" "} icon={faArrowUpRightFromSquare}
<FontAwesomeIcon className="mb-[0.06rem] ml-1 text-xs"
icon={faArrowUpRightFromSquare} />
className="mb-[0.06rem] ml-1 text-xs" </div>
/> </a>
</div> </PageHeader>
</a>
</div>
</div>
<ShareSecretSection /> <ShareSecretSection />
</div> </div>
</div> </div>
@@ -1,9 +1,20 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretSharingPage } from "./SecretSharingPage"; import { SecretSharingPage } from "./SecretSharingPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-sharing" "/_authenticate/_inject-org-details/organization/_layout/secret-sharing"
)({ )({
component: SecretSharingPage component: SecretSharingPage,
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/" })
},
{
label: "secret sharing"
}
]
})
}); });
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { OrgTabGroup } from "./components"; import { OrgTabGroup } from "./components";
import { PageHeader } from "@app/components/v2";
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -11,11 +12,9 @@ export const SettingsPage = () => {
<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="flex w-full justify-center bg-bunker-800 py-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="mb-4"> <PageHeader title={t("settings.org.title")} />
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
</div>
<OrgTabGroup /> <OrgTabGroup />
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -15,5 +15,16 @@ export const Route = createFileRoute(
validateSearch: zodValidator(SettingsPageQueryParams), validateSearch: zodValidator(SettingsPageQueryParams),
search: { search: {
middlewares: [stripSearchParams({ selectedTab: "" })] middlewares: [stripSearchParams({ selectedTab: "" })]
} },
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/" })
},
{
label: "Settings"
}
]
})
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -15,6 +13,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -118,143 +117,132 @@ const Page = withPermission(
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container 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 px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader
variant="link" title={
type="submit" membership.user.firstName || membership.user.lastName
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: OrgAccessControlTabSections.Member
}
});
}}
className="mb-4"
>
Users
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">
{membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim() ? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
: "-"} : "-"
</p> }
{userId !== membership.user.id && ( >
<DropdownMenu> <div>
<DropdownMenuTrigger asChild className="rounded-lg"> {userId !== membership.user.id && (
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <DropdownMenu>
<Tooltip content="More options"> <DropdownMenuTrigger asChild className="rounded-lg">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
</Tooltip> <Tooltip content="More options">
</div> <Button variant="outline_bg" size="sm">
</DropdownMenuTrigger> More
<DropdownMenuContent align="start" className="p-1"> </Button>
<OrgPermissionCan </Tooltip>
I={OrgPermissionActions.Edit} </div>
a={OrgPermissionSubjects.Identity} </DropdownMenuTrigger>
> <DropdownMenuContent align="end" className="p-1">
{(isAllowed) => ( <OrgPermissionCan
<DropdownMenuItem I={OrgPermissionActions.Edit}
className={twMerge( a={OrgPermissionSubjects.Identity}
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" >
)} {(isAllowed) => (
onClick={() => <DropdownMenuItem
handlePopUpOpen("orgMembership", { className={twMerge(
membershipId: membership.id, !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
role: membership.role, )}
roleId: membership.roleId onClick={() =>
}) handlePopUpOpen("orgMembership", {
} membershipId: membership.id,
disabled={!isAllowed} role: membership.role,
> roleId: membership.roleId
Edit User })
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={
membership.isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
}
onClick={async () => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
} }
disabled={!isAllowed}
if (!membership.isActive) { >
// activate user Edit User
await updateOrgMembership({ </DropdownMenuItem>
organizationId: orgId, )}
membershipId, </OrgPermissionCan>
isActive: true <OrgPermissionCan
}); I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
return; >
{(isAllowed) => (
<DropdownMenuItem
className={
membership.isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
} }
onClick={async () => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
// deactivate user if (!membership.isActive) {
handlePopUpOpen("deactivateMember", { // activate user
orgMembershipId: membershipId, await updateOrgMembership({
username: membership.user.username organizationId: orgId,
}); membershipId,
}} isActive: true
disabled={!isAllowed} });
>
{`${membership.isActive ? "Deactivate" : "Activate"} User`} return;
</DropdownMenuItem> }
)}
</OrgPermissionCan> // deactivate user
<OrgPermissionCan handlePopUpOpen("deactivateMember", {
I={OrgPermissionActions.Delete} orgMembershipId: membershipId,
a={OrgPermissionSubjects.Member} username: membership.user.username
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
}); });
return; }}
} disabled={!isAllowed}
>
{`${membership.isActive ? "Deactivate" : "Activate"} User`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
handlePopUpOpen("removeMember", { handlePopUpOpen("removeMember", {
orgMembershipId: membershipId, orgMembershipId: membershipId,
username: membership.user.username username: membership.user.username
}); });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
Remove User Remove User
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
</div> </div>
</PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} /> <UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,24 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { UserDetailsByIDPage } from "./UserDetailsByIDPage"; import { UserDetailsByIDPage } from "./UserDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/members/$membershipId" "/_authenticate/_inject-org-details/organization/_layout/members/$membershipId"
)({ )({
component: UserDetailsByIDPage component: UserDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "home",
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "Users"
}
]
})
}); });