Merge pull request #3003 from akhilmhdh/feat/nav-restructure

Navigation restructure
This commit is contained in:
Maidul Islam
2025-01-20 15:07:39 -05:00
committed by GitHub
206 changed files with 6029 additions and 3761 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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>
);
};
@@ -0,0 +1,217 @@
/* eslint-disable react/prop-types */
import React from "react";
import { faCaretDown, faChevronRight, faEllipsis } from "@fortawesome/free-solid-svg-icons";
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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger
} from "../Dropdown";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={twMerge(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-bunker-100 sm:gap-2.5",
className
)}
{...props}
/>
)
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li
ref={ref}
className={twMerge("inline-flex items-center gap-1.5 font-medium", className)}
{...props}
/>
)
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<"div"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
return (
<div
ref={ref}
className={twMerge("transition-colors hover:text-primary-400", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={twMerge("font-normal text-bunker-200 last:text-bunker-300", className)}
{...props}
/>
)
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={twMerge("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
{...props}
>
{children ?? <FontAwesomeIcon icon={faChevronRight} />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={twMerge("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<FontAwesomeIcon icon={faEllipsis} className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
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 {
Breadcrumb,
BreadcrumbContainer,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbTypes
};
@@ -0,0 +1 @@
export * from "./Breadcrumb";
+36 -43
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 = {
@@ -30,7 +29,7 @@ export const MenuItem = <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,
@@ -38,46 +37,40 @@ export const MenuItem = <T extends ElementType = "button">({
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => { }: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null); const iconRef = useRef<DotLottie | null>(null);
return ( return (
<div onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}> <Item
<li type="button"
className={twMerge( role="menuitem"
"duration-50 group mt-0.5 flex cursor-pointer flex-col rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700", className={twMerge(
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600", "duration-50 group relative mt-0.5 flex w-full cursor-pointer items-center rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
isDisabled && "cursor-not-allowed hover:bg-transparent", isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
className isDisabled && "cursor-not-allowed hover:bg-transparent",
)} className
> )}
<motion.span className="flex w-full flex-row items-center justify-start rounded-sm"> ref={inputRef}
<Item onMouseEnter={() => iconRef.current?.play()}
type="button" onMouseLeave={() => iconRef.current?.stop()}
role="menuitem" {...props}
className="relative flex items-center" >
ref={inputRef} <div
{...props} className={`${
> isSelected ? "visisble" : "invisible"
<div } absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`}
className={`${ />
isSelected ? "visisble" : "invisible" {icon && (
} absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`} <div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3">
/> <DotLottieReact
{icon && ( dotLottieRefCallback={(el) => {
<div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3"> iconRef.current = el;
<DotLottieReact }}
dotLottieRefCallback={(el) => { src={`/lotties/${icon}.json`}
iconRef.current = el; loop
}} className="h-full w-full"
src={`/lotties/${icon}.json`} />
loop </div>
className="h-full w-full" )}
/> <span className="flex-grow text-left">{children}</span>
</div> {description && <span className="mt-2 text-xs">{description}</span>}
)} </Item>
<span className="flex-grow text-left">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}
</motion.span>
</li>
</div>
); );
}; };
@@ -90,7 +83,7 @@ export type MenuGroupProps = {
export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => ( export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => (
<> <>
<li className="p-2 text-xs text-gray-400">{title}</li> <li className="px-2 pt-3 text-xs uppercase text-gray-400">{title}</li>
{children} {children}
</> </>
); );
@@ -0,0 +1,19 @@
import { ReactNode } from "@tanstack/react-router";
type Props = {
title: ReactNode;
description?: ReactNode;
children?: ReactNode;
};
export const PageHeader = ({ title, description, children }: Props) => (
<div className="mb-4">
<div className="flex w-full justify-between">
<div>
<h1 className="mr-4 text-3xl font-semibold capitalize text-white">{title}</h1>
</div>
<div>{children}</div>
</div>
<div className="mt-2 text-gray-400">{description}</div>
</div>
);
@@ -0,0 +1 @@
export { PageHeader } from "./PageHeader";
+2
View File
@@ -2,6 +2,7 @@
export * from "./Accordion"; export * from "./Accordion";
export * from "./Alert"; export * from "./Alert";
export * from "./Badge"; export * from "./Badge";
export * from "./Breadcrumb";
export * from "./Button"; export * from "./Button";
export * from "./Card"; export * from "./Card";
export * from "./Checkbox"; export * from "./Checkbox";
@@ -21,6 +22,7 @@ export * from "./Input";
export * from "./Menu"; export * from "./Menu";
export * from "./Modal"; export * from "./Modal";
export * from "./NoticeBanner"; export * from "./NoticeBanner";
export * from "./PageHeader";
export * from "./Pagination"; export * from "./Pagination";
export * from "./Popoverv2"; export * from "./Popoverv2";
export * from "./SecretInput"; export * from "./SecretInput";
+62 -62
View File
@@ -18,261 +18,261 @@ export const ROUTE_PATHS = Object.freeze({
Organization: { Organization: {
SecretScanning: setRoute( SecretScanning: setRoute(
"/organization/secret-scanning", "/organization/secret-scanning",
"/_authenticate/_inject-org-details/organization/_layout/secret-scanning" "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
), ),
SettingsPage: setRoute( SettingsPage: setRoute(
"/organization/settings", "/organization/settings",
"/_authenticate/_inject-org-details/organization/_layout/settings" "/_authenticate/_inject-org-details/_org-layout/organization/settings"
), ),
GroupDetailsByIDPage: setRoute( GroupDetailsByIDPage: setRoute(
"/organization/groups/$groupId", "/organization/groups/$groupId",
"/_authenticate/_inject-org-details/organization/_layout/groups/$groupId" "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId"
), ),
IdentityDetailsByIDPage: setRoute( IdentityDetailsByIDPage: setRoute(
"/organization/identities/$identityId", "/organization/identities/$identityId",
"/_authenticate/_inject-org-details/organization/_layout/identities/$identityId" "/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId"
), ),
UserDetailsByIDPage: setRoute( UserDetailsByIDPage: setRoute(
"/organization/members/$membershipId", "/organization/members/$membershipId",
"/_authenticate/_inject-org-details/organization/_layout/members/$membershipId" "/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId"
), ),
AccessControlPage: setRoute( AccessControlPage: setRoute(
"/organization/access-management", "/organization/access-management",
"/_authenticate/_inject-org-details/organization/_layout/access-management" "/_authenticate/_inject-org-details/_org-layout/organization/access-management"
), ),
RoleByIDPage: setRoute( RoleByIDPage: setRoute(
"/organization/roles/$roleId", "/organization/roles/$roleId",
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId" "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId"
), ),
AppConnections: { AppConnections: {
GithubOauthCallbackPage: setRoute( GithubOauthCallbackPage: setRoute(
"/organization/app-connections/github/oauth/callback", "/organization/app-connections/github/oauth/callback",
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback" "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback"
) )
} }
}, },
SecretManager: { SecretManager: {
ApprovalPage: setRoute( ApprovalPage: setRoute(
"/secret-manager/$projectId/approval", "/secret-manager/$projectId/approval",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/approval" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval"
), ),
SecretDashboardPage: setRoute( SecretDashboardPage: setRoute(
"/secret-manager/$projectId/secrets/$envSlug", "/secret-manager/$projectId/secrets/$envSlug",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
), ),
OverviewPage: setRoute( OverviewPage: setRoute(
"/secret-manager/$projectId/overview", "/secret-manager/$projectId/overview",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/overview" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview"
), ),
IntegrationDetailsByIDPage: setRoute( IntegrationDetailsByIDPage: setRoute(
"/secret-manager/$projectId/integrations/$integrationId", "/secret-manager/$projectId/integrations/$integrationId",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
), ),
Integratons: { Integratons: {
SelectIntegrationAuth: setRoute( SelectIntegrationAuth: setRoute(
"/secret-manager/$projectId/integrations/select-integration-auth", "/secret-manager/$projectId/integrations/select-integration-auth",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth"
), ),
HerokuOauthCallbackPage: setRoute( HerokuOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/heroku/oauth2/callback", "/secret-manager/$projectId/integrations/heroku/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
), ),
HerokuConfigurePage: setRoute( HerokuConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/heroku/create", "/secret-manager/$projectId/integrations/heroku/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create"
), ),
AwsParameterStoreConfigurePage: setRoute( AwsParameterStoreConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/aws-parameter-store/create", "/secret-manager/$projectId/integrations/aws-parameter-store/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
), ),
AwsSecretManagerConfigurePage: setRoute( AwsSecretManagerConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/aws-secret-manager/create", "/secret-manager/$projectId/integrations/aws-secret-manager/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
), ),
AzureAppConfigurationsOauthCallbackPage: setRoute( AzureAppConfigurationsOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback", "/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
), ),
AzureAppConfigurationsConfigurePage: setRoute( AzureAppConfigurationsConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-app-configuration/create", "/secret-manager/$projectId/integrations/azure-app-configuration/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
), ),
AzureDevopsConfigurePage: setRoute( AzureDevopsConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-devops/create", "/secret-manager/$projectId/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
), ),
AzureKeyVaultAuthorizePage: setRoute( AzureKeyVaultAuthorizePage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/authorize", "/secret-manager/$projectId/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
), ),
AzureKeyVaultOauthCallbackPage: setRoute( AzureKeyVaultOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback", "/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
), ),
AzureKeyVaultConfigurePage: setRoute( AzureKeyVaultConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/create", "/secret-manager/$projectId/integrations/azure-key-vault/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
), ),
BitbucketOauthCallbackPage: setRoute( BitbucketOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/bitbucket/oauth2/callback", "/secret-manager/$projectId/integrations/bitbucket/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
), ),
BitbucketConfigurePage: setRoute( BitbucketConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/bitbucket/create", "/secret-manager/$projectId/integrations/bitbucket/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create"
), ),
ChecklyConfigurePage: setRoute( ChecklyConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/checkly/create", "/secret-manager/$projectId/integrations/checkly/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create"
), ),
CircleConfigurePage: setRoute( CircleConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/circleci/create", "/secret-manager/$projectId/integrations/circleci/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create"
), ),
CloudflarePagesConfigurePage: setRoute( CloudflarePagesConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloudflare-pages/create", "/secret-manager/$projectId/integrations/cloudflare-pages/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
), ),
DigitalOceanAppPlatformConfigurePage: setRoute( DigitalOceanAppPlatformConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/digital-ocean-app-platform/create", "/secret-manager/$projectId/integrations/digital-ocean-app-platform/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
), ),
CloudflareWorkersConfigurePage: setRoute( CloudflareWorkersConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloudflare-workers/create", "/secret-manager/$projectId/integrations/cloudflare-workers/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
), ),
CodefreshConfigurePage: setRoute( CodefreshConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/codefresh/create", "/secret-manager/$projectId/integrations/codefresh/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create"
), ),
GcpSecretManagerConfigurePage: setRoute( GcpSecretManagerConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/gcp-secret-manager/create", "/secret-manager/$projectId/integrations/gcp-secret-manager/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
), ),
GcpSecretManagerOauthCallbackPage: setRoute( GcpSecretManagerOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/gcp-secret-manager/oauth2/callback", "/secret-manager/$projectId/integrations/gcp-secret-manager/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
), ),
GithubConfigurePage: setRoute( GithubConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/github/create", "/secret-manager/$projectId/integrations/github/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/create"
), ),
GithubOauthCallbackPage: setRoute( GithubOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/github/oauth2/callback", "/secret-manager/$projectId/integrations/github/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
), ),
GitlabConfigurePage: setRoute( GitlabConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/gitlab/create", "/secret-manager/$projectId/integrations/gitlab/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create"
), ),
GitlabOauthCallbackPage: setRoute( GitlabOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/gitlab/oauth2/callback", "/secret-manager/$projectId/integrations/gitlab/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
), ),
VercelOauthCallbackPage: setRoute( VercelOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/vercel/oauth2/callback", "/secret-manager/$projectId/integrations/vercel/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
), ),
VercelConfigurePage: setRoute( VercelConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/vercel/create", "/secret-manager/$projectId/integrations/vercel/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create"
), ),
FlyioConfigurePage: setRoute( FlyioConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/flyio/create", "/secret-manager/$projectId/integrations/flyio/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create"
), ),
HashicorpVaultConfigurePage: setRoute( HashicorpVaultConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/hashicorp-vault/create", "/secret-manager/$projectId/integrations/hashicorp-vault/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
), ),
HasuraCloudConfigurePage: setRoute( HasuraCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/hasura-cloud/create", "/secret-manager/$projectId/integrations/hasura-cloud/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
), ),
LaravelForgeConfigurePage: setRoute( LaravelForgeConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/laravel-forge/create", "/secret-manager/$projectId/integrations/laravel-forge/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
), ),
NorthflankConfigurePage: setRoute( NorthflankConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/northflank/create", "/secret-manager/$projectId/integrations/northflank/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/northflank/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/northflank/create"
), ),
RailwayConfigurePage: setRoute( RailwayConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/railway/create", "/secret-manager/$projectId/integrations/railway/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/railway/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/railway/create"
), ),
RenderConfigurePage: setRoute( RenderConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/render/create", "/secret-manager/$projectId/integrations/render/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/render/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/render/create"
), ),
RundeckConfigurePage: setRoute( RundeckConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/rundeck/create", "/secret-manager/$projectId/integrations/rundeck/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/rundeck/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/rundeck/create"
), ),
WindmillConfigurePage: setRoute( WindmillConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/windmill/create", "/secret-manager/$projectId/integrations/windmill/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create"
), ),
TravisCIConfigurePage: setRoute( TravisCIConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/travisci/create", "/secret-manager/$projectId/integrations/travisci/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/travisci/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/travisci/create"
), ),
TerraformCloudConfigurePage: setRoute( TerraformCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/terraform-cloud/create", "/secret-manager/$projectId/integrations/terraform-cloud/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/terraform-cloud/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
), ),
TeamcityConfigurePage: setRoute( TeamcityConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/teamcity/create", "/secret-manager/$projectId/integrations/teamcity/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/teamcity/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/teamcity/create"
), ),
SupabaseConfigurePage: setRoute( SupabaseConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/supabase/create", "/secret-manager/$projectId/integrations/supabase/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/supabase/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/supabase/create"
), ),
OctopusDeployCloudConfigurePage: setRoute( OctopusDeployCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/octopus-deploy/create", "/secret-manager/$projectId/integrations/octopus-deploy/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/octopus-deploy/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
), ),
DatabricksConfigurePage: setRoute( DatabricksConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/databricks/create", "/secret-manager/$projectId/integrations/databricks/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create"
), ),
QoveryConfigurePage: setRoute( QoveryConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/qovery/create", "/secret-manager/$projectId/integrations/qovery/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/qovery/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/qovery/create"
), ),
Cloud66ConfigurePage: setRoute( Cloud66ConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloud-66/create", "/secret-manager/$projectId/integrations/cloud-66/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create"
), ),
NetlifyConfigurePage: setRoute( NetlifyConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/netlify/create", "/secret-manager/$projectId/integrations/netlify/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/create"
), ),
NetlifyOuathCallbackPage: setRoute( NetlifyOuathCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/netlify/oauth2/callback", "/secret-manager/$projectId/integrations/netlify/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
) )
} }
}, },
CertManager: { CertManager: {
CertAuthDetailsByIDPage: setRoute( CertAuthDetailsByIDPage: setRoute(
"/cert-manager/$projectId/ca/$caId", "/cert-manager/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
), ),
OverviewPage: setRoute( OverviewPage: setRoute(
"/cert-manager/$projectId/overview", "/cert-manager/$projectId/overview",
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview"
), ),
PkiCollectionDetailsByIDPage: setRoute( PkiCollectionDetailsByIDPage: setRoute(
"/cert-manager/$projectId/pki-collections/$collectionId", "/cert-manager/$projectId/pki-collections/$collectionId",
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
) )
}, },
Ssh: { Ssh: {
SshCaByIDPage: setRoute( SshCaByIDPage: setRoute(
"/ssh/$projectId/ca/$caId", "/ssh/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId" "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId"
) )
}, },
Public: { Public: {
+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);
}, []); }, []);
@@ -14,7 +14,7 @@ import { envConfig } from "@app/config/env";
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 { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/SidebarFooter/SidebarFooter"; import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
export const AdminLayout = () => { export const AdminLayout = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,174 +1,124 @@
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 { Link, linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-router"; import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
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 {
import { Menu, MenuItem } from "@app/components/v2"; BreadcrumbContainer,
import { useUser } from "@app/context"; Menu,
import { usePopUp, useToggle } from "@app/hooks"; MenuGroup,
import { useSelectOrganization, workspaceKeys } from "@app/hooks/api"; MenuItem,
import { authKeys } from "@app/hooks/api/auth/queries"; TBreadcrumbFormat
import { MfaMethod } from "@app/hooks/api/auth/types"; } from "@app/components/v2";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner"; import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { SidebarFooter } from "./components/SidebarFooter"; import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
import { SidebarHeader } from "./components/SidebarHeader"; import { SidebarHeader } from "./components/SidebarHeader";
export const OrganizationLayout = () => { export const OrganizationLayout = () => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false); const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); const location = useLocation();
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const isOrganizationSpecificPage = location.pathname.startsWith("/organization");
const breadcrumbs =
const { user } = useUser(); isOrganizationSpecificPage && matches && "breadcrumbs" in matches
? matches.breadcrumbs
: undefined;
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { mutateAsync: selectOrganization } = useSelectOrganization();
const navigate = useNavigate();
const router = useRouter();
const queryClient = useQueryClient();
const { t } = useTranslation(); const { t } = useTranslation();
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ const shouldShowOrgSidebar = (
organizationId: orgId [
}); linkOptions({ to: "/organization/access-management" }).to,
linkOptions({ to: "/organization/settings" }).to,
if (isMfaEnabled) { linkOptions({ to: "/organization/audit-logs" }).to
SecurityClient.setMfaToken(token); ] as string[]
if (mfaMethod) { ).includes(location.pathname);
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
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 ( return (
<> <>
<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 bg-bunker-800 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 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"> <MinimizedOrgSidebar />
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]"> <AnimatePresence mode="popLayout">
<div> {shouldShowOrgSidebar && (
<SidebarHeader onChangeOrg={handleOrgChange} /> <motion.div
<div className="px-1"> key="menu-list-items"
<Menu className="mt-4"> initial={{ x: -150 }}
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}> animate={{ x: 0 }}
{({ isActive }) => ( exit={{ x: -150 }}
<MenuItem isSelected={isActive} icon="sliding-carousel"> transition={{ duration: 0.2 }}
Secret Management className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900"
</MenuItem> >
)} <nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
</Link> <div className="p-2 pt-3">
<Link to={`/organization/${ProjectType.CertificateManager}/overview` as const}> <SidebarHeader />
{({ isActive }) => ( </div>
<MenuItem isSelected={isActive} icon="note"> <Menu>
Cert Management <MenuGroup title="Organization Control">
</MenuItem> <Link to="/organization/audit-logs">
)}
</Link>
<Link to={`/organization/${ProjectType.KMS}/overview` as const}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="unlock">
Key Management
</MenuItem>
)}
</Link>
<Link to={`/organization/${ProjectType.SSH}/overview` as const}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="verified">
SSH
</MenuItem>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)}
</Link>
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="secret-scan" className="text-white">
Secret Scanning
</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 }) => ( {({ isActive }) => (
<MenuItem isSelected={isActive} icon="spinning-coin"> <MenuItem isSelected={isActive} icon="moving-block">
Usage & Billing Audit Logs
</MenuItem> </MenuItem>
)} )}
</Link> </Link>
)} {(window.location.origin.includes("https://app.infisical.com") ||
<Link to="/organization/audit-logs"> window.location.origin.includes("https://eu.infisical.com") ||
{({ isActive }) => ( window.location.origin.includes("https://gamma.infisical.com")) && (
<MenuItem isSelected={isActive} icon="moving-block"> <Link to="/organization/billing">
Audit Logs {({ isActive }) => (
</MenuItem> <MenuItem isSelected={isActive} icon="spinning-coin">
Usage & Billing
</MenuItem>
)}
</Link>
)} )}
</Link> </MenuGroup>
<Link to="/organization/settings"> <MenuGroup title="Others">
{({ isActive }) => ( <Link to="/organization/access-management">
<MenuItem isSelected={isActive} icon="toggle-settings"> {({ isActive }) => (
Organization Settings <MenuItem isSelected={isActive} icon="groups">
</MenuItem> Access Control
)} </MenuItem>
</Link> )}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
Organization Settings
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu> </Menu>
</div> </nav>
</div> </motion.div>
<SidebarFooter /> )}
</nav> </AnimatePresence>
</aside> <main
<CreateOrgModal className={twMerge(
isOpen={popUp?.createOrg?.isOpen} "flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]",
onClose={() => handlePopUpToggle("createOrg", false)} !isOrganizationSpecificPage && "overflow-hidden p-0"
/> )}
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]"> >
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
<Outlet /> <Outlet />
</main> </main>
</div> </div>
</div> </div>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden"> <div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" /> <FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200"> <p className="max-w-sm px-6 text-center text-lg text-gray-200">
@@ -0,0 +1,63 @@
import { ComponentPropsWithRef, ElementType, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
import { MenuItemProps } from "@app/components/v2";
export const MenuIconButton = <T extends ElementType = "button">({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = "div",
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef,
lottieIconMode = "forward",
...props
}: MenuItemProps<T> &
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-800 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "opacity-100" : "opacity-0"
} absolute -left-[0.28rem] h-full w-1 rounded-md bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
</div>
</Item>
);
};
@@ -0,0 +1 @@
export { MenuIconButton } from "./MenuIconButton";
@@ -0,0 +1,488 @@
import { useState } from "react";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import {
faArrowUpRightFromSquare,
faBook,
faCheck,
faCog,
faEnvelope,
faInfinity,
faInfo,
faInfoCircle,
faMoneyBill,
faSignOut,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { Link, linkOptions, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
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 { SubscriptionPlan } from "@app/hooks/api/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { MenuIconButton } from "../MenuIconButton";
const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.dynamicSecret) return "Enterprise Plan";
if (subscription.pitRecovery) return "Pro Plan";
return "Free Plan";
};
export const INFISICAL_SUPPORT_OPTIONS = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:[email protected]"
]
];
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 location = useLocation();
const queryClient = useQueryClient();
const isMoreSelected = (
[
linkOptions({ to: "/organization/access-management" }).to,
linkOptions({ to: "/organization/settings" }).to,
linkOptions({ to: "/organization/audit-logs" }).to
] as string[]
).includes(location.pathname);
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 z-10 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 transition-all duration-150"
style={{ width: "72px" }}
>
<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 modal>
<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"
side="right"
className="p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "320px" }}
>
<div className="px-2 py-1">
<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">
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
{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">{getPlan(subscription)}</div>
</div>
</div>
</div>
<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" />
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faSignOut} />}
onClick={logOutUser}
>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-1 px-1">
<Link to="/organization/secret-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/${ProjectType.SecretManager}`)
}
icon="sliding-carousel"
>
Secret Manager
</MenuIconButton>
)}
</Link>
<Link to="/organization/cert-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/${ProjectType.CertificateManager}`)
}
icon="note"
>
Cert Manager
</MenuIconButton>
)}
</Link>
<Link to="/organization/kms/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive || window.location.pathname.startsWith(`/${ProjectType.KMS}`)
}
icon="unlock"
>
KMS
</MenuIconButton>
)}
</Link>
<Link to="/organization/ssh/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive || window.location.pathname.startsWith(`/${ProjectType.SSH}`)
}
icon="verified"
>
SSH
</MenuIconButton>
)}
</Link>
<div className="w-full bg-mineshaft-500" style={{ height: "1px" }} />
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="secret-scan">
Secret Scanning
</MenuIconButton>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="lock-closed">
Secret Sharing
</MenuIconButton>
)}
</Link>
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="w-full">
<MenuIconButton
lottieIconMode="reverse"
icon="three-ellipsis"
isSelected={isMoreSelected}
>
More
</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>
{(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>
</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 icon="user">User</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1">
<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">
<div className="p-2">
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400" />
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{user?.firstName} {user?.lastName}
</div>
<div className="text-xs text-mineshaft-300">{user.email}</div>
</div>
</div>
</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 @@
export { MinimizedOrgSidebar } from "./MinimizedOrgSidebar";
@@ -1,130 +0,0 @@
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import {
faBook,
faEnvelope,
faInfinity,
faInfo,
faPlus,
faQuestion
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { WishForm } from "@app/components/features/WishForm";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription } from "@app/context";
import { useGetOrgTrialUrl } from "@app/hooks/api";
export const INFISICAL_SUPPORT_OPTIONS = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:[email protected]"
]
];
export const SidebarFooter = () => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { mutateAsync } = useGetOrgTrialUrl();
return (
<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-3 text-sm text-mineshaft-400`}
>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
<Link
to="/organization/access-management"
search={{
action: "invite"
}}
className="w-full"
>
<div className="mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faPlus} className="mr-3" />
Invite people
</div>
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support
</div>
</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>
)}
</div>
);
};
@@ -1 +0,0 @@
export { SidebarFooter } from "./SidebarFooter";
@@ -1,169 +1,27 @@
import { faAngleDown, faArrowUpRightFromSquare, faCheck } from "@fortawesome/free-solid-svg-icons"; import { useOrganization, useSubscription } from "@app/context";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SubscriptionPlan } from "@app/hooks/api/types";
import { Link, useNavigate } from "@tanstack/react-router";
import { const getPlan = (subscription: SubscriptionPlan) => {
Button, if (subscription.dynamicSecret) return "Enterprise Plan";
DropdownMenu, if (subscription.pitRecovery) return "Pro Plan";
DropdownMenuContent, return "Free Plan";
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { useOrganization, useUser } from "@app/context";
import { useGetOrganizations, useLogoutUser } from "@app/hooks/api";
import { AuthMethod } from "@app/hooks/api/users/types";
type Prop = {
onChangeOrg: (orgId: string) => void;
}; };
export const SidebarHeader = ({ onChangeOrg }: Prop) => { export const SidebarHeader = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { user } = useUser(); const { subscription } = useSubscription();
const navigate = useNavigate();
const { data: orgs } = useGetOrganizations();
const logout = useLogoutUser();
const logOutUser = async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
navigate({ to: "/login" });
} catch (error) {
console.error(error);
}
};
return ( return (
<div className="flex h-12 cursor-default items-center px-3 pt-6"> <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">
<DropdownMenu> <div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary">
<DropdownMenuTrigger asChild className="max-w-[160px] data-[state=open]:bg-mineshaft-600"> {currentOrg?.name.charAt(0)}
<div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600"> </div>
<div className="flex h-5 w-5 min-w-[20px] items-center justify-center rounded-md bg-primary text-sm"> <div className="flex flex-grow flex-col text-white">
{currentOrg?.name.charAt(0)} <div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
</div> {currentOrg?.name}
<div </div>
className="overflow-hidden truncate text-ellipsis pl-2 text-sm text-mineshaft-100" <div className="text-xs text-mineshaft-400">{getPlan(subscription)}</div>
style={{ maxWidth: "140px" }} </div>
>
{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>
</div> </div>
); );
}; };
@@ -14,7 +14,7 @@ import { envConfig } from "@app/config/env";
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 { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/SidebarFooter/SidebarFooter"; import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
export const PersonalSettingsLayout = () => { export const PersonalSettingsLayout = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,48 +1,30 @@
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 { Link, Outlet, useRouterState } from "@tanstack/react-router";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-router"; import { motion } from "framer-motion";
import { Mfa } from "@app/components/auth/Mfa";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Menu, MenuItem } from "@app/components/v2";
import { useUser, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { import {
useGetAccessRequestsCount, BreadcrumbContainer,
useGetSecretApprovalRequestCount, Menu,
useSelectOrganization, MenuGroup,
workspaceKeys MenuItem,
} from "@app/hooks/api"; TBreadcrumbFormat
import { authKeys } from "@app/hooks/api/auth/queries"; } from "@app/components/v2";
import { MfaMethod } from "@app/hooks/api/auth/types"; import { useWorkspace } from "@app/context";
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { SidebarFooter } from "../OrganizationLayout/components/SidebarFooter";
import { ProjectSelect } from "./components/ProjectSelect"; import { ProjectSelect } from "./components/ProjectSelect";
import { SidebarHeader } from "./components/SidebarHeader";
// This is a generic layout shared by all types of projects. // This is a generic layout shared by all types of projects.
// If the product layout differs significantly, create a new layout as needed. // If the product layout differs significantly, create a new layout as needed.
export const ProjectLayout = () => { export const ProjectLayout = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const navigate = useNavigate(); const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { user } = useUser();
const { mutateAsync: selectOrganization } = useSelectOrganization();
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter();
const queryClient = useQueryClient();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || ""; const projectSlug = currentWorkspace?.slug || "";
@@ -63,191 +45,168 @@ export const ProjectLayout = () => {
const pendingRequestsCount = const pendingRequestsCount =
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
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);
};
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 ( return (
<> <>
<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 />}
<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 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"> <motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
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>
<SidebarHeader onChangeOrg={handleOrgChange} />
<ProjectSelect /> <ProjectSelect />
<div className="px-1"> <div className="px-1">
<Menu> <Menu>
{isSecretManager && ( <MenuGroup title="Main Menu">
<Link {isSecretManager && (
to={`/${ProjectType.SecretManager}/$projectId/overview` as const} <Link
params={{ to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
projectId: currentWorkspace.id params={{
}} projectId: currentWorkspace.id
> }}
{({ isActive }) => ( >
<MenuItem isSelected={isActive} icon="lock-closed"> {({ isActive }) => (
{t("nav.menu.secrets")} <MenuItem isSelected={isActive} icon="lock-closed">
</MenuItem> {t("nav.menu.secrets")}
)} </MenuItem>
</Link> )}
)} </Link>
{isCertManager && (
<Link
to={`/${ProjectType.CertificateManager}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
<Link
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)} )}
</Link> {isCertManager && (
{isSecretManager && ( <Link
<Link to={`/${ProjectType.CertificateManager}/$projectId/overview` as const}
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const} params={{
params={{ projectId: currentWorkspace.id
projectId: currentWorkspace.id }}
}} >
> {({ isActive }) => (
{({ isActive }) => ( <MenuItem isSelected={isActive} icon="lock-closed">
<MenuItem isSelected={isActive} icon="jigsaw-puzzle"> Overview
{t("nav.menu.integrations")} </MenuItem>
</MenuItem> )}
)} </Link>
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="rotation">
Secret Rotation
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="circular-check">
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
{pendingRequestsCount}
</span>
)}
</MenuItem>
)}
</Link>
)}
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
{t("nav.menu.project-settings")}
</MenuItem>
)} )}
</Link> {isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
{t("nav.menu.integrations")}
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="rotation">
Secret Rotation
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="circular-check">
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
{pendingRequestsCount}
</span>
)}
</MenuItem>
)}
</Link>
)}
</MenuGroup>
<MenuGroup title="Others">
<Link
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)}
</Link>
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
{t("nav.menu.project-settings")}
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu> </Menu>
</div> </div>
</div> </div>
<SidebarFooter />
</nav> </nav>
</aside> </motion.div>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]"> <div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
<Outlet /> <Outlet />
</main> </div>
</div> </div>
</div> </div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden"> <div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
@@ -167,7 +167,7 @@ export const ProjectSelect = () => {
}, [workspaces, projectFavorites, currentWorkspace]); }, [workspaces, projectFavorites, currentWorkspace]);
return ( return (
<div className="mb-4 mt-5 w-full p-3"> <div className="mt-2 w-full p-3">
<p className="mb-1 ml-1.5 text-xs font-semibold uppercase text-gray-400"> <p className="mb-1 ml-1.5 text-xs font-semibold uppercase text-gray-400">
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"} {currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"}
</p> </p>
@@ -80,7 +80,7 @@ export const LoginPage = () => {
/> />
</div> </div>
</Link> </Link>
<div className="pb-28">{renderView()}</div>; <div className="pb-28">{renderView()}</div>
</div> </div>
); );
}; };
@@ -59,7 +59,7 @@ export const LoginSsoPage = () => {
/> />
</div> </div>
</Link> </Link>
<div>{renderView()}</div>; <div>{renderView()}</div>
</div> </div>
); );
}; };
@@ -89,7 +89,7 @@ export const SignupSsoPage = () => {
alt="Infisical Logo" alt="Infisical Logo"
/> />
</div> </div>
<div>{renderView()}</div>; <div>{renderView()}</div>
</div> </div>
); );
}; };
@@ -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";
@@ -13,6 +11,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -80,34 +79,17 @@ const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.friendlyName}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() =>
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
params: {
projectId
}
})
}
className="mb-4"
>
Certificate Authorities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.friendlyName}</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities} a={ProjectPermissionSub.CertificateAuthorities}
@@ -133,7 +115,7 @@ const Page = () => {
</ProjectPermissionCan> </ProjectPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} /> <CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,25 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage"; import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
)({ )({
component: CertAuthDetailsByIDPage component: CertAuthDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificate Authorities",
link: linkOptions({
to: "/cert-manager/$projectId/overview",
params: {
projectId: params.projectId
}
})
}
]
};
}
}); });
@@ -2,7 +2,7 @@ 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 { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { CaTab, CertificatesTab, PkiAlertsTab } from "./components"; import { CaTab, CertificatesTab, PkiAlertsTab } from "./components";
@@ -19,11 +19,9 @@ export const CertificatesPage = () => {
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title> <title>{t("common.head-title", { title: "Certificates" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<p className="mb-4 mr-4 text-3xl font-semibold text-white">Internal PKI</p> <PageHeader title="Overview" />
<Tabs defaultValue={TabSections.Certificates}> <Tabs defaultValue={TabSections.Certificates}>
<TabList> <TabList>
<Tab value={TabSections.Certificates}>Certificates</Tab> <Tab value={TabSections.Certificates}>Certificates</Tab>
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { CertificatesPage } from "./CertificatesPage"; import { CertificatesPage } from "./CertificatesPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview"
)({ )({
component: CertificatesPage component: CertificatesPage
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -14,6 +12,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -74,34 +73,17 @@ export const PkiCollectionPage = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
params: {
projectId
}
});
}}
className="mb-4"
>
Certificate Collections
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.PkiCollections} a={ProjectPermissionSub.PkiCollections}
@@ -147,7 +129,7 @@ export const PkiCollectionPage = () => {
</ProjectPermissionCan> </ProjectPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<PkiCollectionDetailsSection <PkiCollectionDetailsSection
@@ -1,9 +1,25 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage"; import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
)({ )({
component: PkiCollectionDetailsByIDPage component: PkiCollectionDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificate Collections",
link: linkOptions({
to: "/cert-manager/$projectId/overview",
params: {
projectId: params.projectId
}
})
}
]
};
}
}); });
@@ -1,7 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
@@ -14,12 +14,9 @@ export const SettingsPage = () => {
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<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" />
</Helmet> </Helmet>
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="my-6"> <PageHeader title={t("settings.project.title")} />
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tabs defaultValue={tabs[0].key}> <Tabs defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage"; import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/settings" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings"
)({ )({
component: SettingsPage component: SettingsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Settings"
}
]
};
}
}); });
+19 -3
View File
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api"; import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
@@ -6,11 +6,11 @@ import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout"; import { ProjectLayout } from "@app/layouts/ProjectLayout";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
)({ )({
component: ProjectLayout, component: ProjectLayout,
beforeLoad: async ({ params, context }) => { beforeLoad: async ({ params, context }) => {
await context.queryClient.ensureQueryData({ const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId), queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId) queryFn: () => fetchWorkspaceById(params.projectId)
}); });
@@ -21,5 +21,21 @@ export const Route = createFileRoute(
}), }),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
}); });
return {
breadcrumbs: [
{
label: "Cert Managers",
link: linkOptions({ to: "/organization/cert-manager/overview" })
},
{
label: project.name,
link: linkOptions({
to: "/cert-manager/$projectId/overview",
params: { projectId: project.id }
})
}
]
};
} }
}); });
@@ -2,6 +2,7 @@ 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";
@@ -13,15 +14,13 @@ export const OverviewPage = () => {
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title> <title>{t("common.head-title", { title: "KMS" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<p className="mr-4 text-3xl font-semibold text-white">Key Management System</p> <PageHeader
<p className="text-md mb-4 text-bunker-300"> title="OverviewPage"
Manage keys and perform cryptographic operations. description="Manage keys and perform cryptographic operations."
</p> />
<ProjectPermissionCan <ProjectPermissionCan
passThrough={false} passThrough={false}
renderGuardBanner renderGuardBanner
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage"; import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/overview" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview"
)({ )({
component: OverviewPage component: OverviewPage
}); });
@@ -1,7 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
@@ -14,12 +14,9 @@ export const SettingsPage = () => {
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<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" />
</Helmet> </Helmet>
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="my-6"> <PageHeader title={t("settings.project.title")} />
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tabs defaultValue={tabs[0].key}> <Tabs defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
+12 -2
View File
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage"; import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/settings" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings"
)({ )({
component: SettingsPage component: SettingsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Settings"
}
]
};
}
}); });
+19 -3
View File
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api"; import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
@@ -6,11 +6,11 @@ import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout"; import { ProjectLayout } from "@app/layouts/ProjectLayout";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout"
)({ )({
component: ProjectLayout, component: ProjectLayout,
beforeLoad: async ({ params, context }) => { beforeLoad: async ({ params, context }) => {
await context.queryClient.ensureQueryData({ const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId), queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId) queryFn: () => fetchWorkspaceById(params.projectId)
}); });
@@ -21,5 +21,21 @@ export const Route = createFileRoute(
}), }),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
}); });
return {
breadcrumbs: [
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
},
{
label: project.name,
link: linkOptions({
to: "/kms/$projectId/overview",
params: { projectId: project.id }
})
}
]
};
} }
}); });
@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgAccessControlTabSections } from "@app/types/org"; import { OrgAccessControlTabSections } from "@app/types/org";
@@ -59,8 +59,11 @@ export const AccessManagementPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet> </Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<p className="mb-4 mr-4 text-3xl font-semibold text-white">Organization Access Control</p> <PageHeader
title="Organization Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
{tabSections {tabSections
@@ -1,4 +1,6 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -12,12 +14,24 @@ const AccessControlPageQuerySchema = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/access-management" "/_authenticate/_inject-org-details/_org-layout/organization/access-management"
)({ )({
component: AccessManagementPage, component: AccessManagementPage,
validateSearch: zodValidator(AccessControlPageQuerySchema), validateSearch: zodValidator(AccessControlPageQuerySchema),
search: { search: {
// strip default values // strip default values
middlewares: [stripSearchParams({ action: "" })] middlewares: [stripSearchParams({ action: "" })]
} },
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "access control"
}
]
})
}); });
@@ -2,7 +2,7 @@ import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgAdminProjects } from "./components/OrgAdminProjects"; import { OrgAdminProjects } from "./components/OrgAdminProjects";
@@ -17,13 +17,13 @@ export const AdminPage = () => {
<> <>
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-6xl px-6"> <div className="w-full max-w-7xl">
<div className="mb-4"> <PageHeader
<p className="text-3xl font-semibold text-gray-200">Organization Admin Console</p> title="Organization Admin Console"
</div> description="View and manage resources across your organization."
/>
<Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}> <Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
<TabList> <TabList>
<Tab value={TabSections.Projects}>Projects</Tab> <Tab value={TabSections.Projects}>Projects</Tab>
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AdminPage } from "./AdminPage"; import { AdminPage } from "./AdminPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/admin" "/_authenticate/_inject-org-details/_org-layout/organization/admin"
)({ )({
component: AdminPage component: AdminPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Admin Console"
}
]
})
}); });
@@ -11,7 +11,7 @@ const GitHubOAuthCallbackPageQueryParamsSchema = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback" "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback"
)({ )({
component: GitHubOAuthCallbackPage, component: GitHubOAuthCallbackPage,
validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema), validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema),
@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { LogsSection } from "./components"; import { LogsSection } from "./components";
export const AuditLogsPage = () => { export const AuditLogsPage = () => {
@@ -11,11 +13,11 @@ export const AuditLogsPage = () => {
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="bg-bunker-800 py-6"> <PageHeader
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p> title="Audit logs"
<div /> description="Audit logs for security and compliance teams to monitor information access."
</div> />
<LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn /> <LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn />
</div> </div>
</div> </div>
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage"; import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/audit-logs" "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs"
)({ )({
component: AuditLogsPage component: AuditLogsPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Audit Logs"
}
]
})
}); });
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { BillingPage } from "./BillingPage"; import { BillingPage } from "./BillingPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/billing" "/_authenticate/_inject-org-details/_org-layout/organization/billing"
)({ )({
component: BillingPage component: BillingPage
}); });
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertManagerOverviewPage } from "./CertManagerOverviewPage"; import { CertManagerOverviewPage } from "./CertManagerOverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/cert-manager/overview" "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview"
)({ )({
component: CertManagerOverviewPage component: CertManagerOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Cert Management",
link: linkOptions({ to: "/organization/cert-manager/overview" })
}
]
})
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -15,6 +13,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Spinner, Spinner,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
@@ -83,34 +82,17 @@ const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.group.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: TabSections.Groups
}
});
}}
className="mb-4"
>
Groups
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.group.name}</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -153,7 +135,7 @@ const Page = () => {
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} /> <GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/groups/$groupId" "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId"
)({ )({
component: GroupDetailsByIDPage component: GroupDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "groups"
}
]
})
}); });
@@ -1,8 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -16,6 +14,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -165,30 +164,13 @@ const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.identity.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management",
search: {
selectedTab: OrgAccessControlTabSections.Identities
}
});
}}
className="mb-4"
>
Identities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.identity.name}</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -257,7 +239,7 @@ const Page = () => {
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} /> <IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/identities/$identityId" "/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId"
)({ )({
component: IdentityDetailsByIDPage component: IdentityDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "identities"
}
]
})
}); });
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { KmsOverviewPage } from "./KmsOverviewPage"; import { KmsOverviewPage } from "./KmsOverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/kms/overview" "/_authenticate/_inject-org-details/_org-layout/organization/kms/overview"
)({ )({
component: KmsOverviewPage component: KmsOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
}
]
})
}); });
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { NoOrgPage } from "./NoOrgPage"; import { NoOrgPage } from "./NoOrgPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/none" "/_authenticate/_inject-org-details/_org-layout/organization/none"
)({ )({
component: NoOrgPage component: NoOrgPage
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -14,6 +12,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -78,31 +77,14 @@ export const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: OrgAccessControlTabSections.Roles
}
});
}}
className="mb-4"
>
Roles
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
{isCustomRole && ( {isCustomRole && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -144,7 +126,7 @@ export const Page = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} /> <RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleByIDPage } from "./RoleByIDPage"; import { RoleByIDPage } from "./RoleByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId" "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId"
)({ )({
component: RoleByIDPage component: RoleByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "Roles"
}
]
})
}); });
@@ -22,7 +22,15 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { NewProjectModal } from "@app/components/projects"; import { NewProjectModal } from "@app/components/projects";
import { Button, IconButton, Input, Pagination, Skeleton, Tooltip } from "@app/components/v2"; import {
Button,
IconButton,
Input,
PageHeader,
Pagination,
Skeleton,
Tooltip
} from "@app/components/v2";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
@@ -50,13 +58,6 @@ enum ProjectOrderBy {
Name = "name" Name = "name"
} }
const formatTitle = (type: ProjectType) => {
if (type === ProjectType.SecretManager) return "Secret Management";
if (type === ProjectType.CertificateManager) return "Cert Management";
if (type === ProjectType.KMS) return "Key Management";
return "SSH";
};
const formatDescription = (type: ProjectType) => { const formatDescription = (type: ProjectType) => {
if (type === ProjectType.SecretManager) if (type === ProjectType.SecretManager)
return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc."; return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.";
@@ -365,13 +366,13 @@ 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" />
</Helmet> </Helmet>
{!serverDetails?.redisConfigured && ( {!serverDetails?.redisConfigured && (
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl"> <div className="mb-4 flex flex-col items-start justify-start text-3xl">
<p className="mb-4 mr-4 font-semibold text-white">Announcements</p> <p className="mb-4 mr-4 font-semibold text-white">Announcements</p>
<div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100"> <div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100">
<FontAwesomeIcon <FontAwesomeIcon
@@ -393,14 +394,9 @@ export const ProductOverviewPage = ({ type }: Props) => {
</div> </div>
</div> </div>
)} )}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0"> <div className="mb-4 flex flex-col items-start justify-start">
<div className="flex w-full justify-between"> <PageHeader title="Projects" description={formatDescription(type)} />
<p className="mr-4 text-3xl font-semibold text-white">{formatTitle(type)}</p> <div className="flex w-full flex-row">
</div>
<div>
<p className="mr-4 mt-2 text-gray-400">{formatDescription(type)}</p>
</div>
<div className="mt-6 flex w-full flex-row">
<Input <Input
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80" className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by project name..." placeholder="Search by project name..."
@@ -1,9 +1,22 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { SecretManagerOverviewPage } from "./SecretManagerOverviewPage"; import { SecretManagerOverviewPage } from "./SecretManagerOverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-manager/overview" "/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview"
)({ )({
component: SecretManagerOverviewPage component: SecretManagerOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Management"
}
]
})
}); });
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router"; import { useSearch } from "@tanstack/react-router";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, NoticeBanner, Pagination } from "@app/components/v2"; import { Button, NoticeBanner, PageHeader, Pagination } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { import {
OrgPermissionActions, OrgPermissionActions,
@@ -111,11 +111,11 @@ export const SecretScanningPage = withPermission(
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div> <PageHeader
<div className="mb-6 text-lg text-mineshaft-300"> title="Secret Scanning"
Automatically monitor your GitHub activity and prevent secret leaks description="Automatically monitor your GitHub activity and prevent secret leaks"
</div> />
{config.isSecretScanningDisabled && ( {config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4"> <NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased We are working on improving the performance of secret scanning due to increased
@@ -1,4 +1,6 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
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";
@@ -10,10 +12,10 @@ const SecretScanningQueryParams = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-scanning" "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
)({ )({
component: SecretScanningPage,
validateSearch: zodValidator(SecretScanningQueryParams), validateSearch: zodValidator(SecretScanningQueryParams),
component: SecretScanningPage,
search: { search: {
middlewares: [ middlewares: [
stripSearchParams({ stripSearchParams({
@@ -21,5 +23,17 @@ export const Route = createFileRoute(
state: "" state: ""
}) })
] ]
} },
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Secret Scanning"
}
]
})
}); });
@@ -3,6 +3,8 @@ 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 { PageHeader } from "@app/components/v2";
import { ShareSecretSection } from "./components"; import { ShareSecretSection } from "./components";
export const SecretSharingPage = () => { export const SecretSharingPage = () => {
@@ -18,28 +20,25 @@ export const SecretSharingPage = () => {
<meta name="og:description" content={String(t("approval.og-description"))} /> <meta name="og:description" content={String(t("approval.og-description"))} />
</Helmet> </Helmet>
<div className="h-full"> <div className="h-full">
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 px-6 text-white"> <div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
<div className="flex items-center justify-between py-6"> <PageHeader
<div className="flex w-full flex-col"> title="Secret Sharing"
<h2 className="text-3xl font-semibold text-gray-200">Secret Sharing</h2> description="Share secrets securely using a shareable link"
<p className="text-bunker-300">Share secrets securely using a shareable link</p> >
</div> <a
<div className="flex w-max justify-center"> target="_blank"
<a rel="noopener noreferrer"
target="_blank" href="https://infisical.com/docs/documentation/platform/secret-sharing"
rel="noopener noreferrer" >
href="https://infisical.com/docs/documentation/platform/secret-sharing" <div className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
> Documentation{" "}
<div className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white"> <FontAwesomeIcon
Documentation{" "} icon={faArrowUpRightFromSquare}
<FontAwesomeIcon className="mb-[0.06rem] ml-1 text-xs"
icon={faArrowUpRightFromSquare} />
className="mb-[0.06rem] ml-1 text-xs" </div>
/> </a>
</div> </PageHeader>
</a>
</div>
</div>
<ShareSecretSection /> <ShareSecretSection />
</div> </div>
</div> </div>
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretSharingPage } from "./SecretSharingPage"; import { SecretSharingPage } from "./SecretSharingPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-sharing" "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
)({ )({
component: SecretSharingPage component: SecretSharingPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "secret sharing"
}
]
})
}); });
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { OrgTabGroup } from "./components"; import { OrgTabGroup } from "./components";
export const SettingsPage = () => { export const SettingsPage = () => {
@@ -11,11 +13,9 @@ export const SettingsPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="mb-4"> <PageHeader title={t("settings.org.title")} />
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
</div>
<OrgTabGroup /> <OrgTabGroup />
</div> </div>
</div> </div>
@@ -1,4 +1,6 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
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";
@@ -9,11 +11,23 @@ const SettingsPageQueryParams = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/settings" "/_authenticate/_inject-org-details/_org-layout/organization/settings"
)({ )({
component: SettingsPage, component: SettingsPage,
validateSearch: zodValidator(SettingsPageQueryParams), validateSearch: zodValidator(SettingsPageQueryParams),
search: { search: {
middlewares: [stripSearchParams({ selectedTab: "" })] middlewares: [stripSearchParams({ selectedTab: "" })]
} },
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Settings"
}
]
})
}); });
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SshOverviewPage } from "./SshOverviewPage"; import { SshOverviewPage } from "./SshOverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/ssh/overview" "/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview"
)({ )({
component: SshOverviewPage component: SshOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "SSH",
link: linkOptions({ to: "/organization/ssh/overview" })
}
]
})
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -15,6 +13,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -118,143 +117,132 @@ const Page = withPermission(
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{membership && ( {membership && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader
variant="link" title={
type="submit" membership.user.firstName || membership.user.lastName
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: OrgAccessControlTabSections.Member
}
});
}}
className="mb-4"
>
Users
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">
{membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim() ? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
: "-"} : "-"
</p> }
{userId !== membership.user.id && ( >
<DropdownMenu> <div>
<DropdownMenuTrigger asChild className="rounded-lg"> {userId !== membership.user.id && (
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <DropdownMenu>
<Tooltip content="More options"> <DropdownMenuTrigger asChild className="rounded-lg">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
</Tooltip> <Tooltip content="More options">
</div> <Button variant="outline_bg" size="sm">
</DropdownMenuTrigger> More
<DropdownMenuContent align="start" className="p-1"> </Button>
<OrgPermissionCan </Tooltip>
I={OrgPermissionActions.Edit} </div>
a={OrgPermissionSubjects.Identity} </DropdownMenuTrigger>
> <DropdownMenuContent align="end" className="p-1">
{(isAllowed) => ( <OrgPermissionCan
<DropdownMenuItem I={OrgPermissionActions.Edit}
className={twMerge( a={OrgPermissionSubjects.Identity}
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" >
)} {(isAllowed) => (
onClick={() => <DropdownMenuItem
handlePopUpOpen("orgMembership", { className={twMerge(
membershipId: membership.id, !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
role: membership.role, )}
roleId: membership.roleId onClick={() =>
}) handlePopUpOpen("orgMembership", {
} membershipId: membership.id,
disabled={!isAllowed} role: membership.role,
> roleId: membership.roleId
Edit User })
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={
membership.isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
}
onClick={async () => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
} }
disabled={!isAllowed}
if (!membership.isActive) { >
// activate user Edit User
await updateOrgMembership({ </DropdownMenuItem>
organizationId: orgId, )}
membershipId, </OrgPermissionCan>
isActive: true <OrgPermissionCan
}); I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
return; >
{(isAllowed) => (
<DropdownMenuItem
className={
membership.isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
} }
onClick={async () => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
// deactivate user if (!membership.isActive) {
handlePopUpOpen("deactivateMember", { // activate user
orgMembershipId: membershipId, await updateOrgMembership({
username: membership.user.username organizationId: orgId,
}); membershipId,
}} isActive: true
disabled={!isAllowed} });
>
{`${membership.isActive ? "Deactivate" : "Activate"} User`} return;
</DropdownMenuItem> }
)}
</OrgPermissionCan> // deactivate user
<OrgPermissionCan handlePopUpOpen("deactivateMember", {
I={OrgPermissionActions.Delete} orgMembershipId: membershipId,
a={OrgPermissionSubjects.Member} username: membership.user.username
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
}); });
return; }}
} disabled={!isAllowed}
>
{`${membership.isActive ? "Deactivate" : "Activate"} User`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
handlePopUpOpen("removeMember", { handlePopUpOpen("removeMember", {
orgMembershipId: membershipId, orgMembershipId: membershipId,
username: membership.user.username username: membership.user.username
}); });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
Remove User Remove User
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
</div> </div>
</PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} /> <UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { UserDetailsByIDPage } from "./UserDetailsByIDPage"; import { UserDetailsByIDPage } from "./UserDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/members/$membershipId" "/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId"
)({ )({
component: UserDetailsByIDPage component: UserDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "Users"
}
]
})
}); });
+1 -1
View File
@@ -2,6 +2,6 @@ import { createFileRoute } from "@tanstack/react-router";
import { OrganizationLayout } from "@app/layouts/OrganizationLayout"; import { OrganizationLayout } from "@app/layouts/OrganizationLayout";
export const Route = createFileRoute("/_authenticate/_inject-org-details/organization/_layout")({ export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({
component: OrganizationLayout component: OrganizationLayout
}); });
@@ -2,9 +2,8 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { 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";
@@ -38,11 +37,11 @@ const Page = withProjectPermission(
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<p className="mb-4 mr-4 text-3xl font-semibold text-white"> <PageHeader
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"} Access title="Access Control"
Control description="Manage fine-grained access for users, groups, roles, and identities within your project resources."
</p> />
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab value={ProjectAccessControlTabs.Member}>Users</Tab> <Tab value={ProjectAccessControlTabs.Member}>Users</Tab>
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -11,8 +11,24 @@ const AccessControlPageQuerySchema = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/access-management" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management"
)({ )({
component: AccessControlPage, component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema) validateSearch: zodValidator(AccessControlPageQuerySchema),
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/cert-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
}
]
};
}
}); });
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -11,8 +11,24 @@ const AccessControlPageQuerySchema = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/access-management" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/access-management"
)({ )({
component: AccessControlPage, component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema) validateSearch: zodValidator(AccessControlPageQuerySchema),
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/kms/$projectId/access-management",
params: {
projectId: params.projectId
}
})
}
]
};
}
}); });
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -11,8 +11,24 @@ const AccessControlPageQuerySchema = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/access-management" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management"
)({ )({
component: AccessControlPage, component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema) validateSearch: zodValidator(AccessControlPageQuerySchema),
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/secret-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
}
]
};
}
}); });
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -11,8 +11,24 @@ const AccessControlPageQuerySchema = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management" "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management"
)({ )({
component: AccessControlPage, component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema) validateSearch: zodValidator(AccessControlPageQuerySchema),
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/ssh/$projectId/access-management",
params: {
projectId: params.projectId
}
})
}
]
};
}
}); });
@@ -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 } 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, 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,
@@ -82,78 +79,37 @@ 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">
<div className="mb-4">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: `/${currentWorkspace.type}/$projectId/access-management` as const,
params: {
projectId: workspaceId
},
search: {
selectedTab: "identities"
}
});
}}
className="mb-4"
>
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"} Access
Control
</Button>
</div>
{identityMembershipDetails ? ( {identityMembershipDetails ? (
<> <>
<div className="mb-4"> <PageHeader
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> title={identityMembershipDetails?.identity?.name}
<div className="mb-4 flex items-center justify-between"> description={`Identity joined on ${identityMembershipDetails?.createdAt && formatRelative(new Date(identityMembershipDetails?.createdAt || ""), new Date())}`}
<h3 className="text-xl font-semibold text-mineshaft-100"> >
Project Identity Access <div>
</h3> <ProjectPermissionCan
<div> I={ProjectPermissionActions.Delete}
<ProjectPermissionCan a={subject(ProjectPermissionSub.Identity, {
I={ProjectPermissionActions.Delete} identityId: identityMembershipDetails?.identity?.id
a={subject(ProjectPermissionSub.Identity, { })}
identityId: identityMembershipDetails?.identity?.id renderTooltip
})} allowedLabel="Remove from project"
renderTooltip >
allowedLabel="Remove from project" {(isAllowed) => (
<Button
colorSchema="danger"
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
isLoading={isDeletingIdentity}
onClick={() => handlePopUpOpen("deleteIdentity")}
> >
{(isAllowed) => ( Remove Identity
<Button </Button>
colorSchema="danger" )}
variant="outline_bg" </ProjectPermissionCan>
size="xs"
isDisabled={!isAllowed}
isLoading={isDeletingIdentity}
onClick={() => handlePopUpOpen("deleteIdentity")}
>
Remove Identity
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
<div className="flex gap-12">
<div>
<span className="text-xs font-semibold text-gray-400">Name</span>
{identityMembershipDetails && (
<p className="text-lg capitalize">
{identityMembershipDetails?.identity?.name}
</p>
)}
</div>
</div>
<div className="mt-4 text-sm text-gray-400">
Joined on{" "}
{identityMembershipDetails?.createdAt &&
format(new Date(identityMembershipDetails?.createdAt || ""), "yyyy-MM-dd")}
</div>
</div> </div>
</div> </PageHeader>
<IdentityRoleDetailsSection <IdentityRoleDetailsSection
identityMembershipDetails={identityMembershipDetails} identityMembershipDetails={identityMembershipDetails}
isMembershipDetailsLoading={isMembershipDetailsLoading} isMembershipDetailsLoading={isMembershipDetailsLoading}
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/identities/$identityId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId"
)({ )({
component: IdentityDetailsByIDPage component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/cert-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Identities"
}
]
};
}
}); });
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/identities/$identityId" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId"
)({ )({
component: IdentityDetailsByIDPage component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/kms/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Identities"
}
]
};
}
}); });
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/identities/$identityId" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId"
)({ )({
component: IdentityDetailsByIDPage component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/secret-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Identities"
}
]
};
}
}); });
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId" "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId"
)({ )({
component: IdentityDetailsByIDPage component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/ssh/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Identities"
}
]
};
}
}); });
@@ -1,21 +1,18 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
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 } from "date-fns"; import { formatRelative } from "date-fns";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, EmptyState, Spinner } from "@app/components/v2"; import { Button, DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
useOrganization, useOrganization,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteUserFromWorkspace, useGetWorkspaceUserDetails } from "@app/hooks/api"; import { useDeleteUserFromWorkspace, useGetWorkspaceUserDetails } from "@app/hooks/api";
@@ -82,77 +79,37 @@ 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">
<div className="mb-4">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: `/${currentWorkspace.type}/$projectId/access-management` as const,
params: {
projectId: currentWorkspace.id
}
});
}}
className="mb-4"
>
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"} Access
Control
</Button>
</div>
{membershipDetails ? ( {membershipDetails ? (
<> <>
<div className="mb-4"> <PageHeader
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> title={
<div className="mb-4 flex items-center justify-between"> membershipDetails.user.firstName || membershipDetails.user.lastName
<h3 className="text-xl font-semibold text-mineshaft-100">Project User Access</h3> ? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
<div> : "-"
<ProjectPermissionCan }
I={ProjectPermissionActions.Delete} description={`User joined on ${membershipDetails?.createdAt && formatRelative(new Date(membershipDetails?.createdAt || ""), new Date())}`}
a={ProjectPermissionSub.Member} >
renderTooltip <ProjectPermissionCan
allowedLabel="Remove from project" I={ProjectPermissionActions.Delete}
> a={ProjectPermissionSub.Member}
{(isAllowed) => ( renderTooltip
<Button allowedLabel="Remove from project"
colorSchema="danger" >
variant="outline_bg" {(isAllowed) => (
size="xs" <Button
isDisabled={!isAllowed} colorSchema="danger"
isLoading={isRemovingUserFromWorkspace} variant="outline_bg"
onClick={() => handlePopUpOpen("removeMember")} size="xs"
> isDisabled={!isAllowed}
Remove User isLoading={isRemovingUserFromWorkspace}
</Button> onClick={() => handlePopUpOpen("removeMember")}
)} >
</ProjectPermissionCan> Remove User
</div> </Button>
</div> )}
<div className="flex gap-12"> </ProjectPermissionCan>
<div> </PageHeader>
<span className="text-xs font-semibold text-gray-400">Name</span>
{membershipDetails && (
<p className="text-lg capitalize">
{membershipDetails.user.firstName || membershipDetails.user.lastName
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
: "-"}
</p>
)}
</div>
<div>
<span className="text-xs font-semibold text-gray-400">Email</span>
{membershipDetails && <p className="text-lg">{membershipDetails?.user?.email}</p>}
</div>
</div>
<div className="mt-4 text-sm text-gray-400">
Joined on{" "}
{membershipDetails?.createdAt &&
format(new Date(membershipDetails?.createdAt || ""), "yyyy-MM-dd")}
</div>
</div>
</div>
<MemberRoleDetailsSection <MemberRoleDetailsSection
membershipDetails={membershipDetails} membershipDetails={membershipDetails}
isMembershipDetailsLoading={isMembershipDetailsLoading} isMembershipDetailsLoading={isMembershipDetailsLoading}
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/members/$membershipId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId"
)({ )({
component: MemberDetailsByIDPage component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/cert-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Users"
}
]
};
}
}); });
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/members/$membershipId" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/members/$membershipId"
)({ )({
component: MemberDetailsByIDPage component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/kms/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Users"
}
]
};
}
}); });
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/members/$membershipId" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/members/$membershipId"
)({ )({
component: MemberDetailsByIDPage component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/secret-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Users"
}
]
};
}
}); });
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId" "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/members/$membershipId"
)({ )({
component: MemberDetailsByIDPage component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/ssh/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Users"
}
]
};
}
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -14,6 +12,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
@@ -83,38 +82,18 @@ const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() =>
navigate({
to: `/${currentWorkspace?.type}/$projectId/access-management` as const,
params: {
projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Roles
}
})
}
className="mb-4"
>
Roles
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
{isCustomRole && ( {isCustomRole && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role} a={ProjectPermissionSub.Role}
@@ -156,7 +135,7 @@ const Page = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} /> <RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/roles/$roleSlug" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/roles/$roleSlug"
)({ )({
component: RoleDetailsBySlugPage component: RoleDetailsBySlugPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/cert-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Roles"
}
]
};
}
}); });
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/roles/$roleSlug" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/roles/$roleSlug"
)({ )({
component: RoleDetailsBySlugPage component: RoleDetailsBySlugPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/kms/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Roles"
}
]
};
}
}); });
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug"
)({ )({
component: RoleDetailsBySlugPage component: RoleDetailsBySlugPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/secret-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Roles"
}
]
};
}
}); });
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/roles/$roleSlug" "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/roles/$roleSlug"
)({ )({
component: RoleDetailsBySlugPage component: RoleDetailsBySlugPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/ssh/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Roles"
}
]
};
}
}); });
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { IPAllowListPage } from "./IPAllowlistPage"; import { IPAllowListPage } from "./IPAllowlistPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/allowlist" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist"
)({ )({
component: () => IPAllowListPage component: () => IPAllowListPage
}); });
@@ -1,8 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
@@ -13,6 +13,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyState, EmptyState,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -28,7 +29,6 @@ import { integrationSlugNameMapping } from "./IntegrationsDetailsByIDPage.utils"
export const IntegrationDetailsByIDPage = () => { export const IntegrationDetailsByIDPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const integrationId = useParams({ const integrationId = useParams({
from: ROUTE_PATHS.SecretManager.IntegrationDetailsByIDPage.id, from: ROUTE_PATHS.SecretManager.IntegrationDetailsByIDPage.id,
select: (el) => el.integrationId select: (el) => el.integrationId
@@ -50,38 +50,21 @@ export const IntegrationDetailsByIDPage = () => {
<meta property="og:title" content="Manage your .env files in seconds" /> <meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} /> <meta name="og:description" content={t("integrations.description") as string} />
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 text-white">
{integration ? ( {integration ? (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader
variant="link" title={`${integrationSlugNameMapping[integration.integration]} Integration`}
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/secret-manager/$projectId/integrations",
params: {
projectId
}
});
}}
className="mb-4"
> >
Integrations
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">
{integrationSlugNameMapping[integration.integration]} Integration
</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<DropdownMenuItem <DropdownMenuItem
onClick={async () => { onClick={async () => {
await syncIntegration({ await syncIntegration({
@@ -119,13 +102,13 @@ export const IntegrationDetailsByIDPage = () => {
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex justify-center"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<IntegrationDetailsSection integration={integration} /> <IntegrationDetailsSection integration={integration} />
<IntegrationConnectionSection integration={integration} /> <IntegrationConnectionSection integration={integration} />
</div> </div>
<div className="space-y-4"> <div className="flex-grow space-y-4">
<IntegrationSettingsSection integration={integration} /> <IntegrationSettingsSection integration={integration} />
<IntegrationAuditLogsSection integration={integration} /> <IntegrationAuditLogsSection integration={integration} />
</div> </div>
@@ -1,9 +1,26 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IntegrationDetailsByIDPage } from "./IntegrationsDetailsByIDPage"; import { IntegrationDetailsByIDPage } from "./IntegrationsDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
)({ )({
component: IntegrationDetailsByIDPage component: IntegrationDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Integrations",
link: linkOptions({
to: "/secret-manager/$projectId/integrations",
params
})
},
{
label: "Details"
}
]
};
}
}); });
@@ -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}`}
@@ -1,7 +1,7 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Checkbox, DeleteActionModal } from "@app/components/v2"; import { Button, Checkbox, DeleteActionModal, PageHeader } from "@app/components/v2";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { TCloudIntegration, TIntegration } from "@app/hooks/api/types"; import { TCloudIntegration, TIntegration } from "@app/hooks/api/types";
@@ -38,11 +38,11 @@ export const IntegrationsSection = ({
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false); const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
return ( return (
<div className="mx-6 mb-8"> <div className="mb-8">
<div className="mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl"> <PageHeader
<h1 className="text-3xl font-semibold">Integrations</h1> title="Integrations"
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p> description="Manage integrations with third-party services."
</div> />
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Active Integrations</p> <p className="text-xl font-semibold text-mineshaft-100">Active Integrations</p>
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { IntegrationsListPage } from "./IntegrationsListPage"; 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/_org-layout/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

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