feat: completed all nav restructing and breadcrumbs cleanup

This commit is contained in:
=
2025-01-20 22:58:59 +05:30
parent 9df8cf60ef
commit 01ae19fa2b
55 changed files with 1249 additions and 780 deletions
@@ -0,0 +1,87 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { useTimedReset } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { createNotification } from "../notifications";
import { IconButton, Tooltip } from "../v2";
type Props = {
secretPathSegments: string[];
selectedPathSegmentIndex: number;
environmentSlug: string;
projectId: string;
};
export const SecretDashboardPathBreadcrumb = ({
secretPathSegments,
selectedPathSegmentIndex,
environmentSlug,
projectId
}: Props) => {
const [, isCopying, setIsCopying] = useTimedReset({
initialState: false
});
const newSecretPath = `/${secretPathSegments.slice(0, selectedPathSegmentIndex + 1).join("/")}`;
const isLastItem = secretPathSegments.length === selectedPathSegmentIndex + 1;
const folderName = secretPathSegments.at(selectedPathSegmentIndex);
return (
<div className="flex items-center space-x-3">
{isLastItem ? (
<div className="group flex items-center space-x-2">
<span
className={twMerge(
"text-sm font-semibold transition-all",
isCopying ? "text-bunker-200" : "text-bunker-300"
)}
>
{folderName}
</span>
<Tooltip className="relative right-2" position="bottom" content="Copy secret path">
<IconButton
variant="plain"
ariaLabel="copy"
onClick={() => {
if (isCopying) return;
setIsCopying(true);
navigator.clipboard.writeText(newSecretPath);
createNotification({
text: "Copied secret path to clipboard",
type: "info"
});
}}
className="opacity-0 transition duration-75 hover:bg-bunker-100/10 group-hover:opacity-100"
>
<FontAwesomeIcon
icon={!isCopying ? faCopy : faCheck}
size="sm"
className="cursor-pointer"
/>
</IconButton>
</Tooltip>
</div>
) : (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
params={{
projectId,
envSlug: environmentSlug
}}
search={(query) => ({ ...query, secretPath: newSecretPath })}
className={twMerge(
"text-sm font-semibold transition-all hover:text-primary",
isCopying && "text-primary"
)}
>
{folderName}
</Link>
)}
</div>
);
};
@@ -1,8 +1,19 @@
import * as React from "react"; /* eslint-disable react/prop-types */
import { faChevronRight, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import React from "react";
import { faCaretDown, faChevronRight, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, ReactNode } from "@tanstack/react-router";
import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger
} from "../Dropdown";
const Breadcrumb = React.forwardRef< const Breadcrumb = React.forwardRef<
HTMLElement, HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & { React.ComponentPropsWithoutRef<"nav"> & {
@@ -27,21 +38,25 @@ BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>( const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<li ref={ref} className={twMerge("inline-flex items-center gap-1.5", className)} {...props} /> <li
ref={ref}
className={twMerge("inline-flex items-center gap-1.5 font-medium", className)}
{...props}
/>
) )
); );
BreadcrumbItem.displayName = "BreadcrumbItem"; BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef< const BreadcrumbLink = React.forwardRef<
HTMLLIElement, HTMLDivElement,
React.ComponentPropsWithoutRef<"li"> & { React.ComponentPropsWithoutRef<"div"> & {
asChild?: boolean; asChild?: boolean;
} }
>(({ asChild, className, ...props }, ref) => { >(({ asChild, className, ...props }, ref) => {
return ( return (
<li <div
ref={ref} ref={ref}
className={twMerge("transition-colors hover:text-bunker-200", className)} className={twMerge("transition-colors hover:text-primary-400", className)}
{...props} {...props}
/> />
); );
@@ -87,12 +102,116 @@ const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span"
); );
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"; BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
enum BreadcrumbTypes {
Dropdown = "dropdown",
Component = "component"
}
export type TBreadcrumbFormat =
| {
type: BreadcrumbTypes.Dropdown;
label: string;
dropdownTitle?: string;
links: { label: string; link: LinkComponentProps }[];
}
| {
type: BreadcrumbTypes.Component;
component: ReactNode;
}
| {
type: undefined;
link?: LinkComponentProps;
label: string;
icon?: ReactNode;
};
const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => (
<div className="mx-auto max-w-7xl py-4 capitalize text-white">
<Breadcrumb>
<BreadcrumbList>
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => {
const isNotLastCrumb = index + 1 !== breadcrumbs.length;
const BreadcrumbSegment = isNotLastCrumb ? BreadcrumbLink : BreadcrumbPage;
if (el.type === BreadcrumbTypes.Dropdown) {
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
<DropdownMenu>
<DropdownMenuTrigger>
<BreadcrumbItem>
<BreadcrumbSegment>
{el.label} <FontAwesomeIcon icon={faCaretDown} size="sm" />
</BreadcrumbSegment>
</BreadcrumbItem>
</DropdownMenuTrigger>
<DropdownMenuContent>
{el?.dropdownTitle && <DropdownMenuLabel>{el.dropdownTitle}</DropdownMenuLabel>}
{el.links.map((i, dropIndex) => (
<Link
{...i.link}
key={`breadcrumb-group-${index + 1}-dropdown-${dropIndex + 1}`}
>
<DropdownMenuItem>{i.label}</DropdownMenuItem>
</Link>
))}
</DropdownMenuContent>
</DropdownMenu>
{isNotLastCrumb && <BreadcrumbSeparator />}
</React.Fragment>
);
}
if (el.type === BreadcrumbTypes.Component) {
const Component = el.component;
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
<BreadcrumbItem>
<BreadcrumbSegment>
<Component />
</BreadcrumbSegment>
</BreadcrumbItem>
{isNotLastCrumb && <BreadcrumbSeparator />}
</React.Fragment>
);
}
const Icon = el?.icon;
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
{"link" in el && isNotLastCrumb ? (
<Link {...el.link}>
<BreadcrumbItem>
<BreadcrumbLink className="inline-flex items-center gap-1.5">
{Icon && <Icon />}
{el.label}
</BreadcrumbLink>
</BreadcrumbItem>
</Link>
) : (
<BreadcrumbItem>
<BreadcrumbPage className="inline-flex items-center gap-1.5">
{Icon && <Icon />}
{el.label}
</BreadcrumbPage>
</BreadcrumbItem>
)}
{isNotLastCrumb && <BreadcrumbSeparator />}
</React.Fragment>
);
})}
</BreadcrumbList>
</Breadcrumb>
</div>
);
export { export {
Breadcrumb, Breadcrumb,
BreadcrumbContainer,
BreadcrumbEllipsis, BreadcrumbEllipsis,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
BreadcrumbList, BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator BreadcrumbSeparator,
BreadcrumbTypes
}; };
-1
View File
@@ -1,6 +1,5 @@
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react"; import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react"; import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export type MenuProps = { export type MenuProps = {
@@ -14,8 +14,6 @@ export const PageHeader = ({ title, description, children }: Props) => (
</div> </div>
<div>{children}</div> <div>{children}</div>
</div> </div>
<div> <div className="mt-2 text-gray-400">{description}</div>
<p className="mt-2 text-gray-400">{description}</p>
</div>
</div> </div>
); );
+2 -1
View File
@@ -30,8 +30,9 @@ export const useToggle = (initialState = false): UseToggleReturn => {
const timedToggle = useCallback((timeout = 2000) => { const timedToggle = useCallback((timeout = 2000) => {
setValue((prev) => !prev); setValue((prev) => !prev);
setTimeout(() => { const timeoutRef = setTimeout(() => {
setValue(false); setValue(false);
clearTimeout(timeoutRef);
}, timeout); }, timeout);
}, []); }, []);
@@ -1,25 +1,20 @@
import { ReactNode, useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; 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, useRouterState } from "@tanstack/react-router"; import { Link, Outlet, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion"; import { motion } from "framer-motion";
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 { import {
Breadcrumb, BreadcrumbContainer,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Menu, Menu,
MenuGroup, MenuGroup,
MenuItem MenuItem,
TBreadcrumbFormat
} from "@app/components/v2"; } 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";
@@ -30,15 +25,10 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner"; import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { MenuIconButton } from "./components/MenuIconButton";
import { SidebarFooter } from "./components/SidebarFooter"; import { SidebarFooter } from "./components/SidebarFooter";
import { SidebarHeader } from "./components/SidebarHeader"; import { SidebarHeader } from "./components/SidebarHeader";
type Props = { export const OrganizationLayout = () => {
children?: ReactNode;
};
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>(() => {});
@@ -53,9 +43,6 @@ export const OrganizationLayout = ({ children }: Props) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isNestedLayout = Boolean(children);
const [isCollapsed, setIsCollapsed] = useToggle(isNestedLayout);
const { t } = useTranslation(); const { t } = useTranslation();
const handleOrgChange = async (orgId: string) => { const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
@@ -96,168 +83,108 @@ export const OrganizationLayout = ({ children }: Props) => {
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden transition-all md:flex"> <div className="dark hidden h-screen w-full flex-col overflow-x-hidden transition-all md:flex">
{!window.isSecureContext && <InsecureConnectionBanner />} {!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row"> <div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside <aside className="dark w-60 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 transition-all duration-150">
className={twMerge(
"dark w-60 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 transition-all duration-150",
isCollapsed && "w-16"
)}
>
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]"> <nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div> <div>
<SidebarHeader onChangeOrg={handleOrgChange} isCollapsed={isCollapsed} /> <SidebarHeader onChangeOrg={handleOrgChange} />
<div className="px-1"> <div className="px-1">
<AnimatePresence mode="wait"> <motion.div
{isCollapsed ? ( key="menu-list-items"
<motion.div initial={{ x: 300, opacity: 0 }}
key="menu-icons" animate={{ x: 0, opacity: 1 }}
className="space-y-1" exit={{ x: -300, opacity: 0 }}
animate={{ x: 0, opacity: 1 }} transition={{ duration: 0.1 }}
exit={{ x: -300, opacity: 0 }} >
transition={{ duration: 0.1 }} <Menu>
> <MenuGroup title="PRODUCTS">
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}> <Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
<MenuIconButton {({ isActive }) => (
isSelected={window.location.pathname.startsWith( <MenuItem isSelected={isActive} icon="sliding-carousel">
`/${ProjectType.SecretManager}` Secret Management
)} </MenuItem>
icon="sliding-carousel" )}
>
Secret Manager
</MenuIconButton>
</Link> </Link>
<Link <Link
to={`/organization/${ProjectType.CertificateManager}/overview` as const} to={`/organization/${ProjectType.CertificateManager}/overview` as const}
> >
<MenuIconButton {({ isActive }) => (
isSelected={window.location.pathname.startsWith( <MenuItem isSelected={isActive} icon="note">
`/${ProjectType.CertificateManager}` Cert Management
)} </MenuItem>
icon="note" )}
>
Cert Manager
</MenuIconButton>
</Link> </Link>
<Link to={`/organization/${ProjectType.KMS}/overview` as const}> <Link to={`/organization/${ProjectType.KMS}/overview` as const}>
<MenuIconButton {({ isActive }) => (
isSelected={window.location.pathname.startsWith(`/${ProjectType.KMS}`)} <MenuItem isSelected={isActive} icon="unlock">
icon="unlock" Key Management
> </MenuItem>
KMS )}
</MenuIconButton>
</Link> </Link>
<Link to={`/organization/${ProjectType.SSH}/overview` as const}> <Link to={`/organization/${ProjectType.SSH}/overview` as const}>
<MenuIconButton {({ isActive }) => (
isSelected={window.location.pathname.startsWith(`/${ProjectType.SSH}`)} <MenuItem isSelected={isActive} icon="verified">
icon="verified" SSH
> </MenuItem>
SSH )}
</MenuIconButton>
</Link> </Link>
</motion.div> <Link to="/organization/secret-scanning">
) : ( {({ isActive }) => (
<motion.div <MenuItem
key="menu-list-items" isSelected={isActive}
initial={{ x: 300, opacity: 0 }} icon="secret-scan"
animate={{ x: 0, opacity: 1 }} className="text-white"
exit={{ x: -300, opacity: 0 }}
transition={{ duration: 0.1 }}
>
<Menu>
<MenuGroup title="PRODUCTS">
<Link
to={`/organization/${ProjectType.SecretManager}/overview` as const}
> >
{({ isActive }) => ( Secret Scanning
<MenuItem isSelected={isActive} icon="sliding-carousel"> </MenuItem>
Secret Management )}
</MenuItem> </Link>
)} </MenuGroup>
</Link> <MenuGroup title="Others">
<Link <Link to="/organization/access-management">
to={ {({ isActive }) => (
`/organization/${ProjectType.CertificateManager}/overview` as const <MenuItem isSelected={isActive} icon="groups">
} Access Control
> </MenuItem>
{({ isActive }) => ( )}
<MenuItem isSelected={isActive} icon="note"> </Link>
Cert Management <Link to="/organization/secret-sharing">
</MenuItem> {({ isActive }) => (
)} <MenuItem isSelected={isActive} icon="lock-closed">
</Link> Secret Sharing
<Link to={`/organization/${ProjectType.KMS}/overview` as const}> </MenuItem>
{({ isActive }) => ( )}
<MenuItem isSelected={isActive} icon="unlock"> </Link>
Key Management {(window.location.origin.includes("https://app.infisical.com") ||
</MenuItem> window.location.origin.includes("https://eu.infisical.com") ||
)} window.location.origin.includes("https://gamma.infisical.com")) && (
</Link> <Link to="/organization/billing">
<Link to={`/organization/${ProjectType.SSH}/overview` as const}> {({ isActive }) => (
{({ isActive }) => ( <MenuItem isSelected={isActive} icon="spinning-coin">
<MenuItem isSelected={isActive} icon="verified"> Usage & Billing
SSH </MenuItem>
</MenuItem>
)}
</Link>
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuItem
isSelected={isActive}
icon="secret-scan"
className="text-white"
>
Secret Scanning
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Secret Sharing
</MenuItem>
)}
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
<Link to="/organization/billing">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="spinning-coin">
Usage & Billing
</MenuItem>
)}
</Link>
)} )}
<Link to="/organization/audit-logs"> </Link>
{({ isActive }) => ( )}
<MenuItem isSelected={isActive} icon="moving-block"> <Link to="/organization/audit-logs">
Audit Logs {({ isActive }) => (
</MenuItem> <MenuItem isSelected={isActive} icon="moving-block">
)} Audit Logs
</Link> </MenuItem>
<Link to="/organization/settings"> )}
{({ isActive }) => ( </Link>
<MenuItem isSelected={isActive} icon="toggle-settings"> <Link to="/organization/settings">
Organization Settings {({ isActive }) => (
</MenuItem> <MenuItem isSelected={isActive} icon="toggle-settings">
)} Organization Settings
</Link> </MenuItem>
</MenuGroup> )}
</Menu> </Link>
</motion.div> </MenuGroup>
)} </Menu>
</AnimatePresence> </motion.div>
</div> </div>
</div> </div>
<SidebarFooter isCollapsed={isCollapsed} onToggleSidebar={setIsCollapsed.toggle} /> <SidebarFooter />
</nav> </nav>
</aside> </aside>
<CreateOrgModal <CreateOrgModal
@@ -265,34 +192,10 @@ export const OrganizationLayout = ({ children }: Props) => {
onClose={() => handlePopUpToggle("createOrg", false)} onClose={() => handlePopUpToggle("createOrg", false)}
/> />
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 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 && ( {breadcrumbs ? (
<div className="mx-auto max-w-7xl py-4 capitalize text-white"> <BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
<Breadcrumb> ) : null}
<BreadcrumbList> <Outlet />
{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 />}
</main> </main>
</div> </div>
</div> </div>
@@ -1,13 +1,10 @@
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import { import {
faAnglesLeft,
faAnglesRight,
faArrowUpRightFromSquare, faArrowUpRightFromSquare,
faBook, faBook,
faEnvelope, faEnvelope,
faInfinity, faInfinity,
faInfo, faInfo,
faInfoCircle,
faPlus, faPlus,
faQuestion, faQuestion,
faUser faUser
@@ -20,15 +17,12 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger
MenuItem
} from "@app/components/v2"; } from "@app/components/v2";
import { envConfig } from "@app/config/env"; import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context"; import { useOrganization, useSubscription, useUser } from "@app/context";
import { useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api"; import { useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api";
import { MenuIconButton } from "../MenuIconButton";
export const INFISICAL_SUPPORT_OPTIONS = [ export const INFISICAL_SUPPORT_OPTIONS = [
[ [
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />, <FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
@@ -52,12 +46,7 @@ export const INFISICAL_SUPPORT_OPTIONS = [
] ]
]; ];
type Props = { export const SidebarFooter = () => {
isCollapsed?: boolean;
onToggleSidebar: () => void;
};
export const SidebarFooter = ({ isCollapsed, onToggleSidebar }: Props) => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -82,38 +71,28 @@ export const SidebarFooter = ({ isCollapsed, onToggleSidebar }: Props) => {
subscription && subscription.slug === "starter" && !subscription.has_used_trial subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2" ? "mb-2"
: "mb-4" : "mb-4"
} flex w-full cursor-default flex-col items-center ${isCollapsed ? "px-1" : "px-2"} text-sm text-mineshaft-400`} } flex w-full cursor-default flex-col items-center px-2 text-sm text-mineshaft-400`}
> >
{(window.location.origin.includes("https://app.infisical.com") || {(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
!isCollapsed && <WishForm />} <Link
{!isCollapsed && ( to="/organization/access-management"
<Link search={{
to="/organization/access-management" action: "invite"
search={{ }}
action: "invite" className="w-full"
}} >
className="w-full" <div className="mb-3 w-full pl-2 duration-200 hover:text-mineshaft-200">
> <FontAwesomeIcon icon={faPlus} className="mr-3" />
<div className="mb-3 w-full pl-2 duration-200 hover:text-mineshaft-200"> Invite people
<FontAwesomeIcon icon={faPlus} className="mr-3" /> </div>
Invite people </Link>
</div>
</Link>
)}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="w-full"> <DropdownMenuTrigger className="w-full">
{isCollapsed ? ( <div className="mb-4 flex w-full items-center pl-2 duration-200 hover:text-mineshaft-200">
<MenuIconButton> <FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" /> Help & Support
Support </div>
</MenuIconButton>
) : (
<div className="mb-4 flex w-full items-center pl-2 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support
</div>
)}
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => ( {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
@@ -161,39 +140,19 @@ export const SidebarFooter = ({ isCollapsed, onToggleSidebar }: Props) => {
</div> </div>
</button> </button>
)} )}
{isCollapsed ? (
<MenuIconButton onClick={onToggleSidebar} className="p-4">
<FontAwesomeIcon icon={faAnglesRight} className="text-lg" />
</MenuIconButton>
) : (
<MenuItem onClick={onToggleSidebar} className="mb-2 w-full px-2 text-mineshaft-400">
<FontAwesomeIcon icon={faAnglesLeft} className="mr-3" />
Collapse
</MenuItem>
)}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="w-full"> <DropdownMenuTrigger asChild className="w-full">
{isCollapsed ? ( <div className="flex w-full cursor-pointer items-center rounded-md border border-mineshaft-600 p-2 px-1">
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary text-sm uppercase">
{user?.firstName?.charAt(0)}
</div>
<div className="max-w-40 flex-grow truncate text-ellipsis text-left text-sm capitalize text-white">
{user?.firstName} {user?.lastName}
</div>
<div> <div>
<MenuIconButton> <FontAwesomeIcon icon={faUser} className="text-xs text-mineshaft-400" />
<div className="my-1 flex h-6 w-6 items-center justify-center rounded-md bg-primary text-sm uppercase text-black">
{user?.firstName?.charAt(0)}
</div>
</MenuIconButton>
</div> </div>
) : ( </div>
<div className="flex w-full cursor-pointer items-center rounded-md border border-mineshaft-600 p-2 px-1">
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary text-sm uppercase">
{user?.firstName?.charAt(0)}
</div>
<div className="max-w-40 flex-grow truncate text-ellipsis text-left text-sm capitalize text-white">
{user?.firstName} {user?.lastName}
</div>
<div>
<FontAwesomeIcon icon={faUser} className="text-xs text-mineshaft-400" />
</div>
</div>
)}
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div> <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
@@ -1,4 +1,4 @@
import { faCheck, faSort } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faSignOut, faSort } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
@@ -12,14 +12,12 @@ import {
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { useGetOrganizations, useLogoutUser } from "@app/hooks/api"; import { useGetOrganizations, useLogoutUser } from "@app/hooks/api";
import { AuthMethod } from "@app/hooks/api/users/types"; import { AuthMethod } from "@app/hooks/api/users/types";
import { twMerge } from "tailwind-merge";
type Prop = { type Prop = {
onChangeOrg: (orgId: string) => void; onChangeOrg: (orgId: string) => void;
isCollapsed?: boolean;
}; };
export const SidebarHeader = ({ onChangeOrg, isCollapsed }: Prop) => { export const SidebarHeader = ({ onChangeOrg }: Prop) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const navigate = useNavigate(); const navigate = useNavigate();
const { data: orgs } = useGetOrganizations(); const { data: orgs } = useGetOrganizations();
@@ -36,38 +34,20 @@ export const SidebarHeader = ({ onChangeOrg, isCollapsed }: Prop) => {
}; };
return ( return (
<div <div className="flex cursor-pointer items-center p-2 pt-4">
className={twMerge(
"flex cursor-pointer items-center p-2 pt-4",
isCollapsed && "transition-all duration-150 hover:bg-mineshaft-700"
)}
>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div <div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
className={twMerge( <div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary">
"flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all", {currentOrg?.name.charAt(0)}
isCollapsed ? "border-none" : "transition-all duration-150 hover:bg-mineshaft-700" </div>
)} <div className="flex flex-grow flex-col text-white">
> <div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{isCollapsed ? ( {currentOrg?.name}
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
{currentOrg?.name.charAt(0)}
</div> </div>
) : ( <div className="text-xs text-mineshaft-400">Free Plan</div>
<> </div>
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary"> <FontAwesomeIcon icon={faSort} className="text-xs text-mineshaft-400" />
{currentOrg?.name.charAt(0)}
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{currentOrg?.name}
</div>
<div className="text-xs text-mineshaft-400">Free Plan</div>
</div>
<FontAwesomeIcon icon={faSort} className="text-xs text-mineshaft-400" />
</>
)}
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
@@ -113,139 +93,11 @@ export const SidebarHeader = ({ onChangeOrg, isCollapsed }: Prop) => {
); );
})} })}
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full"> <DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
<DropdownMenuItem>Log Out</DropdownMenuItem> Log Out
</button> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
); );
}; };
// <DropdownMenu>
// <DropdownMenuTrigger asChild className="max-w-[160px] data-[state=open]:bg-mineshaft-600">
// <div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
// <div className="flex h-5 w-5 min-w-[20px] items-center justify-center rounded-md bg-primary text-sm">
// {currentOrg?.name.charAt(0)}
// </div>
// <div
// className="overflow-hidden truncate text-ellipsis pl-2 text-sm text-mineshaft-100"
// style={{ maxWidth: "140px" }}
// >
// {currentOrg?.name}
// </div>
// <FontAwesomeIcon icon={faAngleDown} className="pl-1 pt-1 text-xs text-mineshaft-300" />
// </div>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="start" className="p-1">
// <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</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;
// }
//
// onChangeOrg(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" />
// <button type="button" onClick={logOutUser} className="w-full">
// <DropdownMenuItem>Log Out</DropdownMenuItem>
// </button>
// </DropdownMenuContent>
// </DropdownMenu>
// <DropdownMenu>
// <DropdownMenuTrigger
// asChild
// className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
// >
// <div
// className="child flex items-center justify-center rounded-full bg-mineshaft pr-1 text-mineshaft-300 hover:bg-mineshaft-500"
// style={{ fontSize: "11px", width: "26px", height: "26px" }}
// >
// {user?.firstName?.charAt(0)}
// {user?.lastName && user?.lastName?.charAt(0)}
// </div>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="start" className="p-1">
// <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
// <Link to="/personal-settings">
// <DropdownMenuItem>Personal Settings</DropdownMenuItem>
// </Link>
// <a
// href="https://infisical.com/docs/documentation/getting-started/introduction"
// target="_blank"
// rel="noopener noreferrer"
// className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
// >
// <DropdownMenuItem>
// Documentation
// <FontAwesomeIcon
// icon={faArrowUpRightFromSquare}
// className="mb-[0.06rem] pl-1.5 text-xxs"
// />
// </DropdownMenuItem>
// </a>
// <a
// href="https://infisical.com/slack"
// target="_blank"
// rel="noopener noreferrer"
// className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
// >
// <DropdownMenuItem>
// Join Slack Community
// <FontAwesomeIcon
// icon={faArrowUpRightFromSquare}
// className="mb-[0.06rem] pl-1.5 text-xxs"
// />
// </DropdownMenuItem>
// </a>
// {user?.superAdmin && (
// <Link to="/admin">
// <DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
// Server Admin Console
// </DropdownMenuItem>
// </Link>
// )}
// <Link to="/organization/admin">
// <DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
// Organization Admin Console
// </DropdownMenuItem>
// </Link>
// <div className="mt-1 h-1 border-t border-mineshaft-600" />
// <button type="button" onClick={logOutUser} className="w-full">
// <DropdownMenuItem>Log Out</DropdownMenuItem>
// </button>
// </DropdownMenuContent>
// </DropdownMenu>
@@ -4,21 +4,18 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet, useRouterState } from "@tanstack/react-router"; import { Link, Outlet, useRouterState } from "@tanstack/react-router";
import { import {
Breadcrumb, BreadcrumbContainer,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Menu, Menu,
MenuGroup, MenuGroup,
MenuItem MenuItem,
TBreadcrumbFormat
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api"; import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner"; import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
import { ProjectSelect } from "./components/ProjectSelect"; import { ProjectSelect } from "./components/ProjectSelect";
// This is a generic layout shared by all types of projects. // This is a generic layout shared by all types of projects.
@@ -54,6 +51,7 @@ export const ProjectLayout = () => {
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
{!window.isSecureContext && <InsecureConnectionBanner />} {!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row"> <div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<MinimizedOrgSidebar />
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"> <aside className="dark w-full border-r border-mineshaft-600 bg-gradient-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:[color-scheme:dark]"> <nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div> <div>
@@ -199,34 +197,10 @@ export const ProjectLayout = () => {
</div> </div>
</nav> </nav>
</aside> </aside>
<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 && ( {breadcrumbs ? (
<div className="mx-auto max-w-7xl py-4 capitalize text-white"> <BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
<Breadcrumb> ) : null}
<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>
)}
<Outlet /> <Outlet />
</main> </main>
</div> </div>
@@ -1,8 +1,8 @@
import { ComponentPropsWithRef, ElementType, useRef } from "react"; import { ComponentPropsWithRef, ElementType, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react"; import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
import { MenuItemProps } from "@app/components/v2"; import { MenuItemProps } from "@app/components/v2";
import { twMerge } from "tailwind-merge";
export const MenuIconButton = <T extends ElementType = "button">({ export const MenuIconButton = <T extends ElementType = "button">({
children, children,
@@ -10,7 +10,7 @@ export const MenuIconButton = <T extends ElementType = "button">({
className, className,
isDisabled, isDisabled,
isSelected, isSelected,
as: Item = "button", as: Item = "div",
description, description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props // wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef, inputRef,
@@ -35,7 +35,7 @@ export const MenuIconButton = <T extends ElementType = "button">({
<div <div
className={`${ className={`${
isSelected ? "visisble" : "invisible" isSelected ? "visisble" : "invisible"
} absolute -left-[0.28rem] h-full w-[0.07rem] rounded-md bg-primary`} } absolute -left-[0.28rem] h-full w-0.5 rounded-md bg-primary`}
/> />
{icon && ( {icon && (
<div className="my-auto mb-2 h-6 w-6"> <div className="my-auto mb-2 h-6 w-6">
@@ -49,7 +49,7 @@ export const MenuIconButton = <T extends ElementType = "button">({
/> />
</div> </div>
)} )}
<span className="flex-grow break-words text-center text-xxs">{children}</span> <div className="flex-grow justify-center break-words text-center text-xxs">{children}</div>
</Item> </Item>
); );
}; };
@@ -0,0 +1,447 @@
import { useState } from "react";
import {
faArrowUpRightFromSquare,
faBook,
faCheck,
faCog,
faEllipsis,
faInfinity,
faInfo,
faInfoCircle,
faMoneyBill,
faReply,
faShare,
faSignOut,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Mfa } from "@app/components/auth/Mfa";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import SecurityClient from "@app/components/utilities/SecurityClient";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useGetOrganizations,
useGetOrgTrialUrl,
useLogoutUser,
useSelectOrganization,
workspaceKeys
} from "@app/hooks/api";
import { authKeys } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { INFISICAL_SUPPORT_OPTIONS } from "@app/layouts/OrganizationLayout/components/SidebarFooter/SidebarFooter";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { MenuIconButton } from "../MenuIconButton";
import { ProjectSwitcher } from "./ProjectSwitcher";
export const MinimizedOrgSidebar = () => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { subscription } = useSubscription();
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
const { currentOrg } = useOrganization();
const { data: orgs } = useGetOrganizations();
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { mutateAsync: selectOrganization } = useSelectOrganization();
const navigate = useNavigate();
const router = useRouter();
const queryClient = useQueryClient();
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
const logout = useLogoutUser();
const logOutUser = async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
navigate({ to: "/login" });
} catch (error) {
console.error(error);
}
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
return (
<>
<aside className="dark w-16 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 transition-all duration-150">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<div className="flex cursor-pointer items-center p-2 pt-4 hover:bg-mineshaft-700">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-1 transition-all">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
{currentOrg?.name.charAt(0)}
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs capitalize text-mineshaft-400">
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;
}
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" />
<Link to="/organization/secret-manager/overview">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faReply} />}>
Home
</DropdownMenuItem>
</Link>
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faSignOut} />}
onClick={logOutUser}
>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="px-1">
<motion.div
key="menu-icons"
className="space-y-1"
initial={{ x: 300, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -300, opacity: 0 }}
transition={{ duration: 0.1 }}
>
<DropdownMenu modal>
<DropdownMenuTrigger>
<MenuIconButton
isSelected={window.location.pathname.startsWith(
`/${ProjectType.SecretManager}`
)}
icon="sliding-carousel"
>
Secret Manager
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
className="px-3 pb-2"
style={{ minWidth: "320px" }}
>
<ProjectSwitcher type={ProjectType.SecretManager} />
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu modal>
<DropdownMenuTrigger>
<MenuIconButton
isSelected={window.location.pathname.startsWith(
`/${ProjectType.CertificateManager}`
)}
icon="note"
>
Cert Manager
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
className="px-3 pb-2"
style={{ minWidth: "320px" }}
>
<ProjectSwitcher type={ProjectType.CertificateManager} />
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu modal>
<DropdownMenuTrigger className="w-full">
<MenuIconButton
isSelected={window.location.pathname.startsWith(`/${ProjectType.KMS}`)}
icon="unlock"
>
KMS
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
className="px-3 pb-2"
style={{ minWidth: "320px" }}
>
<ProjectSwitcher type={ProjectType.KMS} />
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu modal>
<DropdownMenuTrigger className="w-full">
<MenuIconButton
isSelected={window.location.pathname.startsWith(`/${ProjectType.SSH}`)}
icon="verified"
>
SSH
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
className="px-3 pb-2"
style={{ minWidth: "320px" }}
>
<ProjectSwitcher type={ProjectType.SSH} />
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="w-full">
<MenuIconButton>
<div className="flex flex-col items-center justify-center">
<FontAwesomeIcon icon={faEllipsis} className="mb-3 text-lg" />
<span>More</span>
</div>
</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="right" className="p-1">
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
<Link to="/organization/access-management">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUsers} />}>
Access Control
</DropdownMenuItem>
</Link>
<Link to="/organization/secret-sharing">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faShare} />}>
Secret Sharing
</DropdownMenuItem>
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
<Link to="/organization/billing">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faMoneyBill} />}>
Usage & Billing
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/audit-logs">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faBook} />}>
Audit Logs
</DropdownMenuItem>
</Link>
<Link to="/organization/settings">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faCog} />}>
Organization Settings
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
</motion.div>
</div>
</div>
<div
className={`relative mt-10 ${
subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2"
: "mb-4"
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
>
<DropdownMenu>
<DropdownMenuTrigger className="w-full">
<MenuIconButton>
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
Support
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
</DropdownMenuItem>
))}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<button
type="button"
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;
}}
className="mt-1.5 w-full"
>
<div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
<FontAwesomeIcon icon={faInfinity} className="ml-0.5 mr-3 py-2 text-primary" />
Start Free Pro Trial
</div>
</button>
)}
<DropdownMenu>
<DropdownMenuTrigger className="w-full" asChild>
<div>
<MenuIconButton>
<div className="my-1 flex h-6 w-6 items-center justify-center rounded-md bg-primary text-sm uppercase text-black">
{user?.firstName?.charAt(0)}
</div>
</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
<Link to="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Documentation
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<a
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
{user?.superAdmin && (
<Link to="/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Server Admin Console
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console
</DropdownMenuItem>
</Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</nav>
</aside>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};
@@ -0,0 +1,59 @@
import { useState } from "react";
import { faExternalLink, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { DropdownMenuItem, EmptyState, Input } from "@app/components/v2";
import { getProjectTitle } from "@app/helpers/project";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
type Props = {
type: ProjectType;
};
export const ProjectSwitcher = ({ type }: Props) => {
const { data: workspaces, isPending } = useGetUserWorkspaces({ type });
const [search, setSearch] = useState("");
const filteredWorkspaces = workspaces?.filter((el) =>
el.name.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<Link to={`/organization/${type}/overview` as const}>
<div className="py-2 text-xs capitalize text-bunker-300">
{getProjectTitle(type)} projects
<FontAwesomeIcon icon={faExternalLink} size="xs" className="ml-1" />
</div>
</Link>
<div className="w-full pb-2">
<Input
leftIcon={<FontAwesomeIcon icon={faSearch} />}
value={search}
onChange={(evt) => setSearch(evt.target.value)}
size="xs"
placeholder="Search by name"
className=""
/>
</div>
<div className="thin-scrollbar max-h-64 overflow-auto">
{filteredWorkspaces?.map((el) => (
<Link
to={`/${type}/$projectId/overview` as const}
params={{ projectId: el.id }}
key={el.id}
>
<DropdownMenuItem>
<span className="capitalize">{el.name}</span>
</DropdownMenuItem>
</Link>
))}
{!isPending && !filteredWorkspaces?.length && (
<EmptyState title="No project found" iconSize="1x" />
)}
</div>
</>
);
};
@@ -0,0 +1 @@
export { MinimizedOrgSidebar } from "./MinimizedOrgSidebar";
@@ -1,6 +1,4 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
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";
@@ -91,7 +89,7 @@ const Page = () => {
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="emd" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities} a={ProjectPermissionSub.CertificateAuthorities}
@@ -2,10 +2,10 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { CmekTable } from "./components"; import { CmekTable } from "./components";
import { PageHeader } from "@app/components/v2";
export const OverviewPage = () => { export const OverviewPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, 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";
@@ -23,7 +25,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" }) link: linkOptions({ to: "/" })
}, },
{ {
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AdminPage } from "./AdminPage"; import { AdminPage } from "./AdminPage";
@@ -9,7 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" }) link: linkOptions({ to: "/organization/secret-manager/overview" })
}, },
{ {
@@ -1,8 +1,9 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { LogsSection } from "./components";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { LogsSection } from "./components";
export const AuditLogsPage = () => { export const AuditLogsPage = () => {
return ( return (
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage"; import { AuditLogsPage } from "./AuditLogsPage";
@@ -9,7 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" }) link: linkOptions({ to: "/organization/secret-manager/overview" })
}, },
{ {
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertManagerOverviewPage } from "./CertManagerOverviewPage"; import { CertManagerOverviewPage } from "./CertManagerOverviewPage";
@@ -9,8 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "products", label: "Products",
link: linkOptions({ disabled: true, to: "/" }) icon: () => <FontAwesomeIcon icon={faHome} />
}, },
{ {
label: "Cert Management", label: "Cert Management",
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
@@ -9,7 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" }) link: linkOptions({ to: "/organization/secret-manager/overview" })
}, },
{ {
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
@@ -9,7 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" }) link: linkOptions({ to: "/organization/secret-manager/overview" })
}, },
{ {
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { KmsOverviewPage } from "./KmsOverviewPage"; import { KmsOverviewPage } from "./KmsOverviewPage";
@@ -9,8 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "products", label: "Products",
link: linkOptions({ disabled: true, to: "/" }) icon: () => <FontAwesomeIcon icon={faHome} />
}, },
{ {
label: "KMS", label: "KMS",
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleByIDPage } from "./RoleByIDPage"; import { RoleByIDPage } from "./RoleByIDPage";
@@ -9,7 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" }) link: linkOptions({ to: "/organization/secret-manager/overview" })
}, },
{ {
@@ -366,7 +366,7 @@ export const ProductOverviewPage = ({ type }: Props) => {
} }
return ( return (
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen"> <div className="mx-auto flex max-w-7xl 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" />
@@ -1,4 +1,6 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute } from "@tanstack/react-router";
import { SecretManagerOverviewPage } from "./SecretManagerOverviewPage"; import { SecretManagerOverviewPage } from "./SecretManagerOverviewPage";
@@ -9,12 +11,11 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "products", label: "Products",
link: linkOptions({ disabled: true, to: "/" }) icon: () => <FontAwesomeIcon icon={faHome} />
}, },
{ {
label: "Secret Management", label: "Secret Management"
link: linkOptions({ to: "/organization/secret-manager/overview" })
} }
] ]
}) })
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, 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";
@@ -25,11 +27,12 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" }) link: linkOptions({ to: "/" })
}, },
{ {
label: "secret scanning" label: "Secret Scanning"
} }
] ]
}) })
@@ -3,9 +3,10 @@ import { useTranslation } from "react-i18next";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; 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 { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { ShareSecretSection } from "./components";
export const SecretSharingPage = () => { export const SecretSharingPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretSharingPage } from "./SecretSharingPage"; import { SecretSharingPage } from "./SecretSharingPage";
@@ -9,7 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" }) link: linkOptions({ to: "/" })
}, },
{ {
@@ -1,9 +1,10 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { OrgTabGroup } from "./components";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { OrgTabGroup } from "./components";
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, 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,7 +21,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" }) link: linkOptions({ to: "/" })
}, },
{ {
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SshOverviewPage } from "./SshOverviewPage"; import { SshOverviewPage } from "./SshOverviewPage";
@@ -9,8 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "products", label: "Products",
link: linkOptions({ disabled: true, to: "/" }) icon: () => <FontAwesomeIcon icon={faHome} />
}, },
{ {
label: "SSH", label: "SSH",
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { UserDetailsByIDPage } from "./UserDetailsByIDPage"; import { UserDetailsByIDPage } from "./UserDetailsByIDPage";
@@ -9,7 +11,8 @@ export const Route = createFileRoute(
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
{ {
label: "home", label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" }) link: linkOptions({ to: "/organization/secret-manager/overview" })
}, },
{ {
@@ -4,7 +4,6 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectAccessControlTabs } from "@app/types/project"; import { ProjectAccessControlTabs } from "@app/types/project";
@@ -1,16 +1,13 @@
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 { faChevronLeft } 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 { format, formatRelative } from "date-fns"; import { formatRelative } from "date-fns";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2"; import { Button, DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import {
useDeleteIdentityFromWorkspace, useDeleteIdentityFromWorkspace,
@@ -79,7 +79,7 @@ export const Page = () => {
} }
return ( return (
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 p-6 text-white"> <div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 text-white">
{membershipDetails ? ( {membershipDetails ? (
<> <>
<PageHeader <PageHeader
@@ -163,7 +163,7 @@ const Page = () => {
); );
return ( return (
<div className="container relative mx-auto max-w-7xl pb-12 text-white"> <div className="container relative mx-auto max-w-7xl text-white">
<div className="relative"> <div className="relative">
{view === IntegrationView.List ? ( {view === IntegrationView.List ? (
<motion.div <motion.div
@@ -87,7 +87,7 @@ export const redirectForProviderAuth = (
}, },
search: { search: {
clientId: integrationOption.clientId, clientId: integrationOption.clientId,
state, state
} }
}); });
break; break;
@@ -80,7 +80,7 @@ export const CloudIntegrationSection = ({
<NoEnvironmentsBanner projectId={currentWorkspace.id} /> <NoEnvironmentsBanner projectId={currentWorkspace.id} />
)} )}
</div> </div>
<div className="m-4 mt-7 flex flex-col items-start justify-between px-2 text-xl"> <div className="flex flex-col items-start justify-between py-4 text-xl">
{onViewActiveIntegrations && ( {onViewActiveIntegrations && (
<Button <Button
variant="link" variant="link"
@@ -104,7 +104,7 @@ export const CloudIntegrationSection = ({
/> />
</div> </div>
</div> </div>
<div className="mx-6 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7"> <div className="mx-2 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
{isLoading && {isLoading &&
Array.from({ length: 12 }).map((_, index) => ( Array.from({ length: 12 }).map((_, index) => (
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} /> <Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
@@ -12,11 +12,11 @@ export const FrameworkIntegrationSection = () => {
return ( return (
<> <>
<div className="mx-4 mb-4 mt-12 flex flex-col items-start justify-between px-2 text-xl"> <div className="mb-4 mt-12 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1> <h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p> <p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p>
</div> </div>
<div className="mx-6 mt-4 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7"> <div className="mx-2 mt-4 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
{sortedFrameworks.map((framework) => ( {sortedFrameworks.map((framework) => (
<a <a
key={`framework-integration-${framework.slug}`} key={`framework-integration-${framework.slug}`}
@@ -5,13 +5,13 @@ export const InfrastructureIntegrationSection = () => {
return ( return (
<> <>
<div className="mx-4 mb-4 mt-12 flex flex-col items-start justify-between px-2 text-xl"> <div className="mb-4 mt-12 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Infrastructure Integrations</h1> <h1 className="text-3xl font-semibold">Infrastructure Integrations</h1>
<p className="text-base text-gray-400"> <p className="text-base text-gray-400">
Click on of the integration to read the documentation. Click on of the integration to read the documentation.
</p> </p>
</div> </div>
<div className="mx-6 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4"> <div className="mx-2 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
{sortedIntegrations.map((integration) => ( {sortedIntegrations.map((integration) => (
<a <a
key={`framework-integration-${integration.slug}`} key={`framework-integration-${integration.slug}`}
@@ -5,5 +5,15 @@ import { IntegrationsListPage } from "./IntegrationsListPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/" "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/"
)({ )({
component: IntegrationsListPage component: IntegrationsListPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Integrations"
}
]
};
}
}); });
@@ -19,7 +19,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router"; import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import NavHeader from "@app/components/navigation/NavHeader";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
@@ -34,6 +33,7 @@ import {
IconButton, IconButton,
Modal, Modal,
ModalContent, ModalContent,
PageHeader,
Pagination, Pagination,
Table, Table,
TableContainer, TableContainer,
@@ -661,22 +661,17 @@ export const OverviewPage = () => {
); );
return ( return (
<div className="h-full"> <div className="">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("dashboard.title") })}</title> <title>{t("common.head-title", { title: t("dashboard.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<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="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]"> <div className="mx-auto max-w-7xl text-mineshaft-50 dark:[color-scheme:dark]">
<div className="relative right-5 ml-4"> <div className="flex w-full items-baseline justify-between">
<NavHeader pageName={t("dashboard.title")} isProjectRelated /> <PageHeader
</div> title="Secrets Overview"
<div className="space-y-8"> description={
<div className="flex w-full items-baseline justify-between">
<div className="mt-6">
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
<p className="text-md text-bunker-300"> <p className="text-md text-bunker-300">
Inject your secrets using Inject your secrets using
<a <a
@@ -714,32 +709,33 @@ export const OverviewPage = () => {
> >
more more
</a> </a>
. . Click the Explore button to view the secret details section.
</p> </p>
</div> }
</div> />
<div className="flex items-center justify-between"> </div>
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} /> <div className="mt-4 flex items-center justify-between">
<div className="flex flex-row items-center justify-center space-x-2"> <FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
{userAvailableEnvs.length > 0 && ( <div className="flex flex-row items-center justify-center space-x-2">
<DropdownMenu> {userAvailableEnvs.length > 0 && (
<DropdownMenuTrigger asChild> <DropdownMenu>
<IconButton <DropdownMenuTrigger asChild>
ariaLabel="Environments" <IconButton
variant="plain" ariaLabel="Environments"
size="sm" variant="plain"
className={twMerge( size="sm"
"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", className={twMerge(
isTableFiltered && "border-primary/50 text-primary" "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"
> )}
<Tooltip content="Choose visible environments" className="mb-2"> >
<FontAwesomeIcon icon={faList} /> <Tooltip content="Choose visible environments" className="mb-2">
</Tooltip> <FontAwesomeIcon icon={faList} />
</IconButton> </Tooltip>
</DropdownMenuTrigger> </IconButton>
<DropdownMenuContent align="end"> </DropdownMenuTrigger>
{/* <DropdownMenuItem className="px-1.5" asChild> <DropdownMenuContent align="end">
{/* <DropdownMenuItem className="px-1.5" asChild>
<Button <Button
size="xs" size="xs"
className="w-full" className="w-full"
@@ -751,129 +747,126 @@ export const OverviewPage = () => {
Create an environment Create an environment
</Button> </Button>
</DropdownMenuItem> */} </DropdownMenuItem> */}
<DropdownMenuLabel>Filter project resources</DropdownMenuLabel> <DropdownMenuLabel>Filter project resources</DropdownMenuLabel>
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleToggleRowType(RowType.Folder); handleToggleRowType(RowType.Folder);
}} }}
icon={filter[RowType.Folder] && <FontAwesomeIcon icon={faCheckCircle} />} icon={filter[RowType.Folder] && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right" iconPos="right"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FontAwesomeIcon icon={faFolder} className="text-yellow-700" /> <FontAwesomeIcon icon={faFolder} className="text-yellow-700" />
<span>Folders</span> <span>Folders</span>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleToggleRowType(RowType.DynamicSecret); handleToggleRowType(RowType.DynamicSecret);
}} }}
icon={ icon={filter[RowType.DynamicSecret] && <FontAwesomeIcon icon={faCheckCircle} />}
filter[RowType.DynamicSecret] && <FontAwesomeIcon icon={faCheckCircle} /> iconPos="right"
} >
iconPos="right" <div className="flex items-center gap-2">
> <FontAwesomeIcon icon={faFingerprint} className="text-yellow-700" />
<div className="flex items-center gap-2"> <span>Dynamic Secrets</span>
<FontAwesomeIcon icon={faFingerprint} className="text-yellow-700" /> </div>
<span>Dynamic Secrets</span> </DropdownMenuItem>
</div> <DropdownMenuItem
</DropdownMenuItem> onClick={(e) => {
<DropdownMenuItem e.preventDefault();
onClick={(e) => { handleToggleRowType(RowType.Secret);
e.preventDefault(); }}
handleToggleRowType(RowType.Secret); icon={filter[RowType.Secret] && <FontAwesomeIcon icon={faCheckCircle} />}
}} iconPos="right"
icon={filter[RowType.Secret] && <FontAwesomeIcon icon={faCheckCircle} />} >
iconPos="right" <div className="flex items-center gap-2">
> <FontAwesomeIcon icon={faKey} className="text-bunker-300" />
<div className="flex items-center gap-2"> <span>Secrets</span>
<FontAwesomeIcon icon={faKey} className="text-bunker-300" /> </div>
<span>Secrets</span> </DropdownMenuItem>
</div> <DropdownMenuLabel>Choose visible environments</DropdownMenuLabel>
</DropdownMenuItem> {userAvailableEnvs.map((availableEnv) => {
<DropdownMenuLabel>Choose visible environments</DropdownMenuLabel> const { id: envId, name } = availableEnv;
{userAvailableEnvs.map((availableEnv) => {
const { id: envId, name } = availableEnv;
const isEnvSelected = visibleEnvs.map((env) => env.id).includes(envId); const isEnvSelected = visibleEnvs.map((env) => env.id).includes(envId);
return ( return (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleEnvSelect(envId); handleEnvSelect(envId);
}} }}
key={envId} key={envId}
disabled={visibleEnvs?.length === 1} disabled={visibleEnvs?.length === 1}
icon={isEnvSelected && <FontAwesomeIcon icon={faCheckCircle} />} icon={isEnvSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right" iconPos="right"
> >
<div className="flex items-center">{name}</div> <div className="flex items-center">{name}</div>
</DropdownMenuItem> </DropdownMenuItem>
); );
})} })}
</DropdownMenuContent>
</DropdownMenu>
)}
<SecretSearchInput
value={searchFilter}
tags={tags}
onChange={setSearchFilter}
environments={userAvailableEnvs}
projectId={currentWorkspace?.id}
/>
{userAvailableEnvs.length > 0 && (
<div>
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
className="h-10 rounded-r-none"
>
Add Secret
</Button>
<DropdownMenu
open={popUp.misc.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add-folder-or-import"
variant="outline_bg"
className="rounded-l-none bg-mineshaft-600 p-3"
>
<FontAwesomeIcon icon={faAngleDown} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SecretFolders}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => {
handlePopUpOpen("addFolder");
handlePopUpClose("misc");
}}
isDisabled={!isAllowed}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Folder
</Button>
)}
</ProjectPermissionCan>
</div>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} </div>
<SecretSearchInput )}
value={searchFilter}
tags={tags}
onChange={setSearchFilter}
environments={userAvailableEnvs}
projectId={currentWorkspace?.id}
/>
{userAvailableEnvs.length > 0 && (
<div>
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
className="h-10 rounded-r-none"
>
Add Secret
</Button>
<DropdownMenu
open={popUp.misc.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add-folder-or-import"
variant="outline_bg"
className="rounded-l-none bg-mineshaft-600 p-3"
>
<FontAwesomeIcon icon={faAngleDown} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SecretFolders}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => {
handlePopUpOpen("addFolder");
handlePopUpClose("misc");
}}
isDisabled={!isAllowed}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Folder
</Button>
)}
</ProjectPermissionCan>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
</div> </div>
</div> </div>
<SelectionPanel <SelectionPanel
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge"; import { Badge } from "@app/components/v2/Badge";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api"; import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
@@ -35,39 +35,32 @@ export const SecretApprovalsPage = () => {
: TabSection.SecretApprovalRequests; : TabSection.SecretApprovalRequests;
return ( return (
<div className="h-full"> <div>
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("approval.title") })}</title> <title>{t("common.head-title", { title: t("approval.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<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 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="Approval Workflows"
<h2 className="text-3xl font-semibold text-gray-200">Approval Workflows</h2> description="Create approval policies for any modifications to secrets in sensitive environments and folders.
<p className="text-bunker-300"> "
Create approval policies for any modifications to secrets in sensitive environments >
and folders. <a
</p> href="https://infisical.com/docs/documentation/platform/pr-workflows"
</div> target="_blank"
<div className="flex w-max justify-center"> rel="noopener noreferrer"
<a >
href="https://infisical.com/docs/documentation/platform/pr-workflows" <span 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">
target="_blank" Documentation
rel="noopener noreferrer" <FontAwesomeIcon
> icon={faArrowUpRightFromSquare}
<span 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"> className="mb-[0.06rem] ml-1 text-xs"
Documentation />
<FontAwesomeIcon </span>
icon={faArrowUpRightFromSquare} </a>
className="mb-[0.06rem] ml-1 text-xs" </PageHeader>
/>
</span>
</a>
</div>
</div>
<Tabs defaultValue={defaultTab}> <Tabs defaultValue={defaultTab}>
<TabList> <TabList>
<Tab value={TabSection.SecretApprovalRequests}> <Tab value={TabSection.SecretApprovalRequests}>
@@ -12,5 +12,15 @@ export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/approval" "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/approval"
)({ )({
component: SecretApprovalsPage, component: SecretApprovalsPage,
validateSearch: zodValidator(SecretApprovalPageQueryParams) validateSearch: zodValidator(SecretApprovalPageQueryParams),
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Approvals"
}
]
};
}
}); });
@@ -7,7 +7,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import NavHeader from "@app/components/navigation/NavHeader";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { PermissionDeniedBanner } from "@app/components/permissions"; import { PermissionDeniedBanner } from "@app/components/permissions";
import { import {
@@ -67,7 +66,6 @@ const LOADER_TEXT = [
]; ];
const Page = () => { const Page = () => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const navigate = useNavigate({ const navigate = useNavigate({
from: ROUTE_PATHS.SecretManager.SecretDashboardPage.path from: ROUTE_PATHS.SecretManager.SecretDashboardPage.path
@@ -264,18 +262,6 @@ const Page = () => {
state === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC state === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
); );
const handleEnvChange = (slug: string) => {
navigate({
params: {
envSlug: slug
},
search: (state) => {
const newState = { ...state, secretPath: undefined };
return newState;
}
});
};
const handleTagToggle = useCallback( const handleTagToggle = useCallback(
(tagSlug: string) => (tagSlug: string) =>
setFilter((state) => { setFilter((state) => {
@@ -396,21 +382,8 @@ const Page = () => {
setDebouncedSearchFilter(""); setDebouncedSearchFilter("");
}; };
return ( return (
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]"> <div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:[color-scheme:dark]">
<SecretV2MigrationSection /> <SecretV2MigrationSection />
<div className="relative -top-2 right-6 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
currentEnv={environment}
userAvailableEnvs={currentWorkspace?.environments}
isFolderMode
secretPath={secretPath}
isProjectRelated
onEnvChange={handleEnvChange}
isProtectedBranch={isProtectedBranch}
protectionPolicyName={boardPolicy?.name}
/>
</div>
{!isRollbackMode ? ( {!isRollbackMode ? (
<> <>
<ActionBar <ActionBar
@@ -428,6 +401,7 @@ const Page = () => {
isSnapshotCountLoading={isSnapshotCountLoading} isSnapshotCountLoading={isSnapshotCountLoading}
onToggleRowType={handleToggleRowType} onToggleRowType={handleToggleRowType}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)} onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
protectedBranchPolicyName={boardPolicy?.name}
/> />
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300"> <div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col" id="dashboard"> <div className="flex flex-col" id="dashboard">
@@ -15,6 +15,7 @@ import {
faFolder, faFolder,
faFolderPlus, faFolderPlus,
faKey, faKey,
faLock,
faMinusSquare, faMinusSquare,
faPlus, faPlus,
faTrash faTrash
@@ -80,6 +81,7 @@ type Props = {
isVisible?: boolean; isVisible?: boolean;
snapshotCount: number; snapshotCount: number;
isSnapshotCountLoading?: boolean; isSnapshotCountLoading?: boolean;
protectedBranchPolicyName?: string;
onSearchChange: (term: string) => void; onSearchChange: (term: string) => void;
onToggleTagFilter: (tagId: string) => void; onToggleTagFilter: (tagId: string) => void;
onVisibilityToggle: () => void; onVisibilityToggle: () => void;
@@ -101,7 +103,8 @@ export const ActionBar = ({
onToggleTagFilter, onToggleTagFilter,
onVisibilityToggle, onVisibilityToggle,
onClickRollbackMode, onClickRollbackMode,
onToggleRowType onToggleRowType,
protectedBranchPolicyName
}: Props) => { }: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"addFolder", "addFolder",
@@ -112,9 +115,9 @@ export const ActionBar = ({
"misc", "misc",
"upgradePlan" "upgradePlan"
] as const); ] as const);
const isProtectedBranch = Boolean(protectedBranchPolicyName);
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { openPopUp } = usePopUpAction(); const { openPopUp } = usePopUpAction();
const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch();
const { mutateAsync: moveSecrets } = useMoveSecrets(); const { mutateAsync: moveSecrets } = useMoveSecrets();
@@ -387,6 +390,15 @@ export const ActionBar = ({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
<div>
{isProtectedBranch && (
<Tooltip content={`Protected by policy ${protectedBranchPolicyName}`}>
<IconButton variant="outline_bg" ariaLabel="protected">
<FontAwesomeIcon icon={faLock} className="text-primary" />
</IconButton>
</Tooltip>
)}
</div>
<div className="flex-grow" /> <div className="flex-grow" />
<div> <div>
<IconButton variant="outline_bg" ariaLabel="Download" onClick={handleSecretDownload}> <IconButton variant="outline_bg" ariaLabel="Download" onClick={handleSecretDownload}>
@@ -1,7 +1,10 @@
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";
import { SecretDashboardPathBreadcrumb } from "@app/components/navigation/SecretDashboardPathBreadcrumb";
import { BreadcrumbTypes } from "@app/components/v2";
import { SecretDashboardPage } from "./SecretDashboardPage"; import { SecretDashboardPage } from "./SecretDashboardPage";
const SecretDashboardPageQueryParamsSchema = z.object({ const SecretDashboardPageQueryParamsSchema = z.object({
@@ -9,7 +12,6 @@ const SecretDashboardPageQueryParamsSchema = z.object({
search: z.string().catch(""), search: z.string().catch(""),
tags: z.string().catch("") tags: z.string().catch("")
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug" "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
)({ )({
@@ -17,5 +19,39 @@ export const Route = createFileRoute(
validateSearch: zodValidator(SecretDashboardPageQueryParamsSchema), validateSearch: zodValidator(SecretDashboardPageQueryParamsSchema),
search: { search: {
middlewares: [stripSearchParams({ secretPath: "/", search: "", tags: "" })] middlewares: [stripSearchParams({ secretPath: "/", search: "", tags: "" })]
},
beforeLoad: ({ context, params, search }) => {
const secretPathSegments = search.secretPath.split("/").filter(Boolean);
return {
breadcrumbs: [
...context.breadcrumbs,
{
type: BreadcrumbTypes.Dropdown,
label: context.project.environments.find((el) => el.slug === params.envSlug)?.name || "",
dropdownTitle: "Environments",
links: context.project.environments.map((el) => ({
label: el.name,
link: linkOptions({
to: "/secret-manager/$projectId/secrets/$envSlug",
params: {
projectId: params.projectId,
envSlug: el.slug
}
})
}))
},
...secretPathSegments.map((_, index) => ({
type: BreadcrumbTypes.Component,
component: () => (
<SecretDashboardPathBreadcrumb
secretPathSegments={secretPathSegments}
selectedPathSegmentIndex={index}
environmentSlug={params.envSlug}
projectId={params.projectId}
/>
)
}))
]
};
} }
}); });
@@ -20,6 +20,7 @@ import {
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
IconButton, IconButton,
PageHeader,
Skeleton, Skeleton,
Spinner, Spinner,
Table, Table,
@@ -137,30 +138,25 @@ const Page = () => {
}; };
return ( return (
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 px-6 text-white"> <div className="container mx-auto 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 Rotation"
<h2 className="text-3xl font-semibold text-gray-200">Secret Rotation</h2> description="Stop manually rotating secrets and automate credential rotation."
<p className="text-bunker-300"> >
Stop manually rotating secrets and automate credential rotation. <a
</p> target="_blank"
</div> rel="noopener noreferrer"
<div className="flex w-max justify-center"> href="https://infisical.com/docs/documentation/platform/secret-rotation/overview"
<a >
target="_blank" <span 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">
rel="noopener noreferrer" Documentation
href="https://infisical.com/docs/documentation/platform/secret-rotation/overview" <FontAwesomeIcon
> icon={faArrowUpRightFromSquare}
<span 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"> className="mb-[0.06rem] ml-1 text-xs"
Documentation />
<FontAwesomeIcon </span>
icon={faArrowUpRightFromSquare} </a>
className="mb-[0.06rem] ml-1 text-xs" </PageHeader>
/>
</span>
</a>
</div>
</div>
<div className="mb-6"> <div className="mb-6">
<div className="mb-2 mt-6 text-xl font-semibold text-gray-200">Rotated Secrets</div> <div className="mb-2 mt-6 text-xl font-semibold text-gray-200">Rotated Secrets</div>
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
@@ -374,7 +370,7 @@ export const SecretRotationPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="h-full bg-bunker-800"> <div className="bg-bunker-800">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title> <title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
@@ -5,5 +5,15 @@ import { SecretRotationPage } from "./SecretRotationPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secret-rotation" "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secret-rotation"
)({ )({
component: SecretRotationPage component: SecretRotationPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Secret Rotation"
}
]
};
}
}); });
@@ -1,13 +1,12 @@
import { Helmet } from "react-helmet";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router";
import z from "zod"; import z from "zod";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { Helmet } from "react-helmet";
import { useSearch } from "@tanstack/react-router";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
const schema = z.object({ const schema = z.object({
@@ -18,7 +17,7 @@ type FormData = z.infer<typeof schema>;
export function AzureKeyVaultAuthorizePage() { export function AzureKeyVaultAuthorizePage() {
const { state, clientId } = useSearch({ const { state, clientId } = useSearch({
from: ROUTE_PATHS.SecretManager.Integratons.AzureKeyVaultAuthorizePage.id, from: ROUTE_PATHS.SecretManager.Integratons.AzureKeyVaultAuthorizePage.id
}); });
const { control, handleSubmit } = useForm<FormData>({ const { control, handleSubmit } = useForm<FormData>({
@@ -38,7 +37,7 @@ export function AzureKeyVaultAuthorizePage() {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<Helmet> <Helmet>
<title>Authorize Azure Key Vault Integration</title> <title>Authorize Azure Key Vault Integration</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600"> <Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
@@ -56,12 +55,12 @@ export function AzureKeyVaultAuthorizePage() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100"> <div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" /> <FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs Docs
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowUpRightFromSquare} icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs" className="mb-[0.07rem] ml-1.5 text-xxs"
/> />
</div> </div>
</a> </a>
@@ -1,12 +1,11 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import z from 'zod' import z from "zod";
import { AzureKeyVaultAuthorizePage } from "./AzureKeyVaultAuthorizePage"; import { AzureKeyVaultAuthorizePage } from "./AzureKeyVaultAuthorizePage";
const PageQueryParamsSchema = z.object({ const PageQueryParamsSchema = z.object({
state: z.string(), state: z.string(),
clientId: z.string().optional(), clientId: z.string().optional()
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
@@ -187,7 +187,7 @@ export const CircleCIConfigurePage = () => {
name="secretPath" name="secretPath"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}> <FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
<SecretPathInput {...field} environment={selectedEnvironment.slug}/> <SecretPathInput {...field} environment={selectedEnvironment.slug} />
</FormControl> </FormControl>
)} )}
/> />
@@ -1,3 +1,5 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api"; import { workspaceKeys } from "@app/hooks/api";
@@ -23,9 +25,11 @@ export const Route = createFileRoute(
}); });
return { return {
project,
breadcrumbs: [ breadcrumbs: [
{ {
label: "Secret Managers", label: "Secret Managers",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" }) link: linkOptions({ to: "/organization/secret-manager/overview" })
}, },
{ {