Merge remote-tracking branch 'origin/main' into pki-revamp-v3

This commit is contained in:
Carlos Monastyrski
2025-10-20 14:27:19 -03:00
201 changed files with 7648 additions and 3401 deletions
@@ -63,14 +63,14 @@ export const WishForm = () => {
open={isOpen}
>
<PopoverTrigger asChild>
<div className="text-md mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
<div className="mb-3 w-full cursor-pointer pl-5 text-sm whitespace-nowrap text-mineshaft-400 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faRocketchat} className="mr-2" />
Request a feature
</div>
</PopoverTrigger>
<PopoverContent
hideCloseBtn
align="start"
align="end"
alignOffset={20}
className="mb-1 w-auto border border-mineshaft-600 bg-mineshaft-900 p-4 drop-shadow-2xl"
sticky="always"
+3 -3
View File
@@ -20,11 +20,11 @@ const badgeVariants = cva(
success: "bg-green/20 text-green",
org: "bg-org-v1/20 text-org-v1 [&_svg]:text-org-v1 flex items-center opacity-100 hover:bg-org-v1/10 [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
namespace:
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1 w-min whitespace-nowrap",
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1.5 w-min whitespace-nowrap",
project:
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 w-min gap-x-1.5 whitespace-nowrap",
instance:
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap"
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1.5 w-min whitespace-nowrap"
}
}
}
@@ -120,10 +120,16 @@ export type TBreadcrumbFormat =
icon?: ReactNode;
};
const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => (
<div className="mx-auto max-w-7xl text-white">
<Breadcrumb>
<BreadcrumbList>
const BreadcrumbContainer = ({
breadcrumbs,
className
}: {
breadcrumbs: TBreadcrumbFormat[];
className?: string;
}) => (
<div className={twMerge("mx-auto max-w-8xl overflow-hidden text-white", className)}>
<Breadcrumb className="overflow-hidden">
<BreadcrumbList className="overflow-hidden">
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => {
const isNotLastCrumb = index + 1 !== breadcrumbs.length;
const BreadcrumbSegment = isNotLastCrumb ? BreadcrumbLink : BreadcrumbPage;
@@ -165,8 +171,8 @@ const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[]
const Component = el.component;
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
<BreadcrumbItem>
<BreadcrumbSegment>
<BreadcrumbItem className="overflow-hidden">
<BreadcrumbSegment className="overflow-hidden">
<Component />
</BreadcrumbSegment>
</BreadcrumbItem>
@@ -5,29 +5,48 @@ import { ReactNode } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { Badge } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { ProjectType } from "@app/hooks/api/projects/types";
type Props = {
title: ReactNode;
description?: ReactNode;
children?: ReactNode;
className?: string;
scope: "org" | "project" | "namespace" | "instance";
scope: "org" | "namespace" | "instance" | ProjectType | null;
};
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: IconDefinition }> = {
org: { label: "Organization", icon: faGlobe },
project: { label: "Project", icon: faCube },
[ProjectType.SecretManager]: { label: "Project", icon: faCube },
[ProjectType.CertificateManager]: { label: "Project", icon: faCube },
[ProjectType.SSH]: { label: "Project", icon: faCube },
[ProjectType.KMS]: { label: "Project", icon: faCube },
[ProjectType.PAM]: { label: "Project", icon: faCube },
[ProjectType.SecretScanning]: { label: "Project", icon: faCube },
namespace: { label: "Namespace", icon: faCubes },
instance: { label: "Server", icon: faServer }
};
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]> = {
org: "org",
[ProjectType.SecretManager]: "project",
[ProjectType.CertificateManager]: "project",
[ProjectType.SSH]: "project",
[ProjectType.KMS]: "project",
[ProjectType.PAM]: "project",
[ProjectType.SecretScanning]: "project",
namespace: "namespace",
instance: "instance"
};
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
<div className={twMerge("mb-4 w-full", className)}>
<div className={twMerge("mb-10 w-full", className)}>
<div className="flex w-full justify-between">
<div className="mr-4 flex w-full items-center">
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
{scope && (
<Badge variant={scope} className="mt-1 ml-2.5">
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
<FontAwesomeIcon icon={SCOPE_NAME[scope].icon} />
{SCOPE_NAME[scope].label}
</Badge>
+32 -5
View File
@@ -1,10 +1,19 @@
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { twMerge } from "tailwind-merge";
export type TabsProps = TabsPrimitive.TabsProps;
export const Tabs = ({ className, children, ...props }: TabsProps) => (
<TabsPrimitive.Root className={twMerge("flex flex-col", className)} {...props}>
<TabsPrimitive.Root
className={twMerge(
"flex",
className,
props.orientation === "vertical" ? "flex-col xl:flex-row xl:gap-x-12" : "flex-col"
)}
{...props}
>
{children}
</TabsPrimitive.Root>
);
@@ -13,7 +22,11 @@ export type TabListProps = TabsPrimitive.TabsListProps;
export const TabList = ({ className, children, ...props }: TabListProps) => (
<TabsPrimitive.List
className={twMerge("flex shrink-0 border-b-2 border-mineshaft-800", className)}
className={twMerge(
"no-scrollbar flex shrink-0 overflow-auto border-b-2 border-mineshaft-800",
"data-[orientation=vertical]:xl:flex-col data-[orientation=vertical]:xl:items-start data-[orientation=vertical]:xl:gap-y-6 data-[orientation=vertical]:xl:border-b-0",
className
)}
{...props}
>
{children}
@@ -26,18 +39,29 @@ export const Tab = ({
className,
children,
variant = "project",
icon,
...props
}: TabProps & { variant?: "project" | "namespace" | "org" }) => (
}: TabProps & {
icon?: IconDefinition;
variant?: "project" | "namespace" | "org" | "instance";
}) => (
<TabsPrimitive.Trigger
className={twMerge(
"flex h-10 items-center justify-center px-3 text-sm font-medium text-mineshaft-400 transition-all select-none first:rounded-tl-md last:rounded-tr-md hover:text-mineshaft-200 data-[state=active]:border-b data-[state=active]:text-white",
"flex h-10 cursor-pointer items-center justify-center border-transparent",
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-400 transition-all select-none",
"data-[orientation=vertical]:xl:h-5 data-[orientation=vertical]:xl:border-b-0 data-[orientation=vertical]:xl:border-l",
"border-b hover:text-mineshaft-200",
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",
"hover:border-mineshaft-400",
variant === "project" && "data-[state=active]:border-primary",
variant === "namespace" && "data-[state=active]:border-namespace-v1",
variant === "org" && "data-[state=active]:border-org-v1",
variant === "instance" && "data-[state=active]:border-mineshaft-300",
className
)}
{...props}
>
{icon && <FontAwesomeIcon icon={icon} className="mr-2" size="xs" />}
{children}
</TabsPrimitive.Trigger>
);
@@ -46,7 +70,10 @@ export type TabPanelProps = TabsPrimitive.TabsContentProps;
export const TabPanel = ({ className, children, ...props }: TabPanelProps) => (
<TabsPrimitive.Content
className={twMerge("grow rounded-br-md rounded-bl-md py-5 outline-hidden", className)}
className={twMerge(
"grow rounded-br-md rounded-bl-md py-5 outline-hidden data-[orientation=vertical]:xl:overflow-x-hidden data-[orientation=vertical]:xl:py-0",
className
)}
{...props}
>
{children}
@@ -43,6 +43,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Revoke universal auth client secret",
[EventType.CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS]: "Clear universal auth lockouts",
[EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS]: "Get universal auth client secrets",
[EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID]:
"Get universal auth client secret by id",
[EventType.CREATE_ENVIRONMENT]: "Create environment",
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
[EventType.DELETE_ENVIRONMENT]: "Delete environment",
@@ -48,6 +48,7 @@ export enum EventType {
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS = "clear-identity-universal-auth-lockouts",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth",
ADD_IDENTITY_LDAP_AUTH = "add-identity-ldap-auth",
@@ -318,6 +318,14 @@ interface GetIdentityUniversalAuthClientSecretsEvent {
};
}
interface GetIdentityUniversalAuthClientSecretByIdEvent {
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID;
metadata: {
identityId: string;
clientSecretId: string;
};
}
interface RevokeIdentityUniversalAuthClientSecretEvent {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
metadata: {
@@ -906,6 +914,7 @@ export type Event =
| GetIdentityUniversalAuthEvent
| CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent
| GetIdentityUniversalAuthClientSecretByIdEvent
| RevokeIdentityUniversalAuthClientSecretEvent
| ClearIdentityUniversalAuthLockoutsEvent
| CreateEnvironmentEvent
@@ -1,2 +1,3 @@
export * from "./mutations";
export * from "./queries";
export * from "./types";
@@ -1,8 +1,12 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { projectKeys } from "../projects";
import { externalMigrationQueryKeys } from "./queries";
import { TImportVaultSecretsDTO, TVaultExternalMigrationConfig, VaultImportStatus } from "./types";
export const useImportEnvKey = () => {
const queryClient = useQueryClient();
@@ -65,3 +69,97 @@ export const useImportVault = () => {
}
});
};
export const useImportVaultSecrets = () => {
const queryClient = useQueryClient();
return useMutation<{ status: VaultImportStatus }, object, TImportVaultSecretsDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post<{ status: VaultImportStatus }>(
"/api/v3/external-migration/vault/import-secrets",
dto
);
return data;
},
onSuccess: (_, { projectId, environment, secretPath }) => {
queryClient.invalidateQueries({ queryKey: dashboardKeys.all() });
queryClient.invalidateQueries({
queryKey: secretKeys.getProjectSecret({
projectId,
environment,
secretPath
})
});
}
});
};
export const useCreateVaultExternalMigrationConfig = () => {
const queryClient = useQueryClient();
return useMutation<
TVaultExternalMigrationConfig,
Error,
{ connectionId: string; namespace: string }
>({
mutationFn: async ({ connectionId, namespace }) => {
const { data } = await apiRequest.post<{ config: TVaultExternalMigrationConfig }>(
"/api/v3/external-migration/vault/configs",
{
connectionId,
namespace
}
);
return data.config;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: externalMigrationQueryKeys.vaultConfigs()
});
}
});
};
export const useUpdateVaultExternalMigrationConfig = () => {
const queryClient = useQueryClient();
return useMutation<
TVaultExternalMigrationConfig,
Error,
{ id: string; connectionId: string; namespace: string }
>({
mutationFn: async ({ id, connectionId, namespace }) => {
const { data } = await apiRequest.put<{ config: TVaultExternalMigrationConfig }>(
`/api/v3/external-migration/vault/configs/${id}`,
{
connectionId,
namespace
}
);
return data.config;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: externalMigrationQueryKeys.vaultConfigs()
});
}
});
};
export const useDeleteVaultExternalMigrationConfig = () => {
const queryClient = useQueryClient();
return useMutation<TVaultExternalMigrationConfig, Error, { id: string }>({
mutationFn: async ({ id }) => {
const { data } = await apiRequest.delete<{ config: TVaultExternalMigrationConfig }>(
`/api/v3/external-migration/vault/configs/${id}`
);
return data.config;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: externalMigrationQueryKeys.vaultConfigs()
});
}
});
};
+154 -2
View File
@@ -2,12 +2,35 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { ExternalMigrationProviders } from "./types";
import {
ExternalMigrationProviders,
TVaultExternalMigrationConfig,
VaultKubernetesAuthRole
} from "./types";
const externalMigrationQueryKeys = {
export const externalMigrationQueryKeys = {
customMigrationAvailable: (provider: ExternalMigrationProviders) => [
"custom-migration-available",
provider
],
vaultConfigs: () => ["vault-external-migration-configs"],
vaultNamespaces: () => ["vault-namespaces"],
vaultPolicies: (namespace?: string) => ["vault-policies", namespace],
vaultMounts: (namespace?: string) => ["vault-mounts", namespace],
vaultAuthMounts: (namespace?: string, authType?: string) => [
"vault-auth-mounts",
namespace,
authType
],
vaultSecretPaths: (namespace?: string, mountPath?: string) => [
"vault-secret-paths",
namespace,
mountPath
],
vaultKubernetesAuthRoles: (namespace?: string, mountPath?: string) => [
"vault-kubernetes-auth-roles",
namespace,
mountPath
]
};
@@ -20,3 +43,132 @@ export const useHasCustomMigrationAvailable = (provider: ExternalMigrationProvid
)
});
};
export const useGetVaultExternalMigrationConfigs = () => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultConfigs(),
queryFn: async () => {
const { data } = await apiRequest.get<{ configs: TVaultExternalMigrationConfig[] }>(
"/api/v3/external-migration/vault/configs"
);
return data.configs;
}
});
};
export const useGetVaultNamespaces = () => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultNamespaces(),
queryFn: async () => {
const { data } = await apiRequest.get<{
namespaces: Array<{ id: string; name: string }>;
}>("/api/v3/external-migration/vault/namespaces");
return data.namespaces;
}
});
};
export const useGetVaultPolicies = (enabled = true, namespace?: string) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultPolicies(namespace),
queryFn: async () => {
const { data } = await apiRequest.get<{
policies: Array<{ name: string; rules: string }>;
}>("/api/v3/external-migration/vault/policies", {
params: {
namespace
}
});
return data.policies;
},
enabled
});
};
export const useGetVaultMounts = (enabled = true, namespace?: string) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultMounts(namespace),
queryFn: async () => {
const { data } = await apiRequest.get<{
mounts: Array<{ path: string; type: string; version: string | null }>;
}>("/api/v3/external-migration/vault/mounts", {
params: {
namespace
}
});
return data.mounts;
},
enabled
});
};
export const useGetVaultSecretPaths = (enabled = true, namespace?: string, mountPath?: string) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultSecretPaths(namespace, mountPath),
queryFn: async () => {
if (!namespace || !mountPath) {
throw new Error("Both namespace and mountPath are required");
}
const { data } = await apiRequest.get<{
secretPaths: string[];
}>("/api/v3/external-migration/vault/secret-paths", {
params: {
namespace,
mountPath
}
});
return data.secretPaths;
},
enabled: enabled && !!namespace && !!mountPath
});
};
export const useGetVaultAuthMounts = (enabled = true, namespace?: string, authType?: string) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultAuthMounts(namespace, authType),
queryFn: async () => {
const { data } = await apiRequest.get<{
mounts: Array<{ path: string; type: string }>;
}>("/api/v3/external-migration/vault/auth-mounts", {
params: {
namespace,
...(authType && { authType })
}
});
return data.mounts;
},
enabled
});
};
export const useGetVaultKubernetesAuthRoles = (
enabled = true,
namespace?: string,
mountPath?: string
) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultKubernetesAuthRoles(namespace, mountPath),
queryFn: async () => {
if (!namespace || !mountPath) {
throw new Error("Both namespace and mountPath are required");
}
const { data } = await apiRequest.get<{
roles: VaultKubernetesAuthRole[];
}>("/api/v3/external-migration/vault/auth-roles/kubernetes", {
params: {
namespace,
mountPath
}
});
return data.roles;
},
enabled: enabled && !!namespace && !!mountPath
});
};
+47
View File
@@ -2,3 +2,50 @@ export enum ExternalMigrationProviders {
Vault = "vault",
EnvKey = "env-key"
}
export enum VaultImportStatus {
Imported = "imported",
ApprovalRequired = "approval-required"
}
export type TVaultExternalMigrationConfig = {
id: string;
orgId: string;
namespace: string;
connectionId: string | null;
createdAt: string;
updatedAt: string;
};
export type TImportVaultSecretsDTO = {
projectId: string;
environment: string;
secretPath: string;
vaultNamespace: string;
vaultSecretPath: string;
};
export type VaultKubernetesAuthRole = {
name: string;
bound_service_account_names: string[];
bound_service_account_namespaces: string[];
token_ttl?: number;
token_max_ttl?: number;
token_policies?: string[];
token_bound_cidrs?: string[];
token_explicit_max_ttl?: number;
token_no_default_policy?: boolean;
token_num_uses?: number;
token_period?: number;
token_type?: string;
audience?: string;
alias_name_source?: string;
mountPath: string;
config: {
kubernetes_host: string;
kubernetes_ca_cert?: string;
issuer?: string;
disable_iss_validation?: boolean;
disable_local_ca_jwt?: boolean;
};
};
@@ -2,6 +2,7 @@ export {
useCreateFolder,
useDeleteFolder,
useGetFoldersByEnv,
useGetOrCreateFolder,
useGetProjectFolders,
useUpdateFolder
} from "./queries";
@@ -140,6 +140,59 @@ export const useGetFoldersByEnv = ({
return { folders, folderNames, isFolderPresentInEnv, getFolderByNameAndEnv };
};
export const useGetOrCreateFolder = () => {
const queryClient = useQueryClient();
return useMutation<TSecretFolder, object, TCreateFolderDTO>({
mutationFn: async (dto) => {
const { data: existingFolder } = await apiRequest.get<{ folders: TSecretFolder[] }>(
"/api/v2/folders",
{
params: {
projectId: dto.projectId,
environment: dto.environment,
path: dto.path || "/"
}
}
);
const folder = existingFolder.folders.find((f) => f.name === dto.name);
if (folder) return folder;
const { data } = await apiRequest.post("/api/v2/folders", {
...dto,
projectId: dto.projectId
});
return data;
},
onSuccess: (_, { projectId, environment, path }) => {
queryClient.invalidateQueries({
queryKey: dashboardKeys.getDashboardSecrets({
projectId,
secretPath: path ?? "/"
})
});
queryClient.invalidateQueries({
queryKey: folderQueryKeys.getSecretFolders({ projectId, environment, path })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.list({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ projectId, environment, directory: path })
});
}
});
};
export const useCreateFolder = () => {
const queryClient = useQueryClient();
@@ -170,6 +223,9 @@ export const useCreateFolder = () => {
queryClient.invalidateQueries({
queryKey: commitKeys.count({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ projectId, environment, directory: path })
});
}
});
};
@@ -218,12 +274,13 @@ export const useDeleteFolder = () => {
const queryClient = useQueryClient();
return useMutation<object, object, TDeleteFolderDTO>({
mutationFn: async ({ path = "/", folderId, environment, projectId }) => {
mutationFn: async ({ path = "/", folderId, environment, projectId, forceDelete = true }) => {
const { data } = await apiRequest.delete(`/api/v2/folders/${folderId}`, {
data: {
environment,
projectId,
path
path,
forceDelete
}
});
return data;
@@ -59,6 +59,7 @@ export type TDeleteFolderDTO = {
environment: string;
folderId: string;
path?: string;
forceDelete?: boolean;
};
export type TUpdateFolderBatchDTO = {
+1 -1
View File
@@ -42,7 +42,7 @@
--font-inter: "Inter", sans-serif;
--color-org-v1: #30B3FF;
--color-namespace-v1: #96ff59;
--max-width-8xl: 88rem; /* 1408px */
/* Primary */
--color-primary-50: #fffff5;
--color-primary-100: #fcfce8;
@@ -12,7 +12,7 @@ import { RedisBanner } from "@app/layouts/OrganizationLayout/components/RedisBan
import { SmtpBanner } from "@app/layouts/OrganizationLayout/components/SmtpBanner";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { AdminSidebar } from "./Sidebar";
import { AdminNavBar } from "./AdminNavBar";
export const AdminLayout = () => {
const { t } = useTranslation();
@@ -33,9 +33,9 @@ export const AdminLayout = () => {
{!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />}
{!isLoading && subscription.auditLogs && <AuditLogBanner />}
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<AdminSidebar />
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-4 pt-8 pb-4 dark:scheme-dark">
<div className="flex grow flex-col overflow-y-hidden">
<AdminNavBar />
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark">
<Outlet />
</div>
</div>
@@ -0,0 +1,99 @@
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faArrowLeft,
faBuilding,
faCog,
faDatabase,
faKey,
faLock,
faPlug,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Tab, TabList, Tabs, Tooltip } from "@app/components/v2";
const generalTabs = [
{
label: "General",
icon: faCog,
link: "/admin/"
},
{
label: "Resource Overview",
icon: faBuilding,
link: "/admin/resources/overview"
},
{
label: "Access Control",
icon: faUserTie,
link: "/admin/access-management"
},
{
label: "Encryption",
icon: faLock,
link: "/admin/encryption"
},
{
label: "Authentication",
icon: faCheckCircle,
link: "/admin/authentication"
},
{
label: "Integrations",
icon: faPlug,
link: "/admin/integrations"
},
{
label: "Caching",
icon: faDatabase,
link: "/admin/caching"
},
{
label: "Environment Variables",
icon: faKey,
link: "/admin/environment"
}
];
export const AdminNavBar = () => {
const matchRoute = useMatchRoute();
return (
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="px-4"
>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Tooltip position="bottom" content="Back to organization">
<Link to="/organization/projects">
<Tab value="back">
<FontAwesomeIcon icon={faArrowLeft} />
</Tab>
</Link>
</Tooltip>
{generalTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<Tab variant="instance" value={isActive ? "selected" : ""}>
{tab.label}
</Tab>
</Link>
);
})}
</TabList>
</Tabs>
</nav>
</motion.div>
</div>
);
};
@@ -1,126 +0,0 @@
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faBuilding,
faChevronLeft,
faCog,
faDatabase,
faKey,
faLock,
faPlug,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
const generalTabs = [
{
label: "General",
icon: faCog,
link: "/admin/"
},
{
label: "Encryption",
icon: faLock,
link: "/admin/encryption"
},
{
label: "Authentication",
icon: faCheckCircle,
link: "/admin/authentication"
},
{
label: "Integrations",
icon: faPlug,
link: "/admin/integrations"
},
{
label: "Caching",
icon: faDatabase,
link: "/admin/caching"
},
{
label: "Environment Variables",
icon: faKey,
link: "/admin/environment"
}
];
const othersTabs = [
{
label: "Access Controls",
icon: faUserTie,
link: "/admin/access-management"
},
{
label: "Resource Overview",
icon: faBuilding,
link: "/admin/resources/overview"
}
];
export const AdminSidebar = () => {
const matchRoute = useMatchRoute();
return (
<aside className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:scheme-dark">
<div className="flex-1">
<Menu>
<MenuGroup title="Configuration">
{generalTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={tab.icon} />
</div>
{tab.label}
</div>
</MenuItem>
</Link>
);
})}
</MenuGroup>
<MenuGroup title="Others">
{othersTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={tab.icon} />
</div>
{tab.label}
</div>
</MenuItem>
</Link>
);
})}
</MenuGroup>
</Menu>
</div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faChevronLeft}
flip="vertical"
/>
}
>
Back to Organization
</MenuItem>
</Link>
</Menu>
</nav>
</aside>
);
};
+65 -124
View File
@@ -1,9 +1,7 @@
import { faBook, faCog, faCube, faHome, faLock, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context";
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
@@ -12,138 +10,81 @@ export const KmsLayout = () => {
const { currentProject } = useProject();
const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
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-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="unlock" />
KMS
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/kms/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCube} />
</div>
Overview
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/kmip"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faLock} />
</div>
KMIP
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/kms/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
variant="project"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/kms/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Overview</Tab>}
</Link>
</Menu>
</div>
<Link
to="/projects/kms/$projectId/kmip"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>KMIP</Tab>}
</Link>
<Link
to="/projects/kms/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/kms/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/kms/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
);
@@ -13,7 +13,7 @@ import { useFetchServerStatus } from "@app/hooks/api";
import { AuditLogBanner } from "./components/AuditLogBanner";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { Navbar } from "./components/NavBar";
import { OrgSidebar } from "./components/OrgSidebar";
import { OrgNavBar } from "./components/OrgNavBar";
import { RedisBanner } from "./components/RedisBanner";
import { SmtpBanner } from "./components/SmtpBanner";
@@ -41,16 +41,16 @@ export const OrganizationLayout = () => {
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
>
<Navbar />
{!isLoading && !serverDetails?.redisConfigured && <RedisBanner />}
{!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />}
{!isLoading && subscription.auditLogs && <AuditLogBanner />}
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<OrgSidebar isHidden={isInsideProject} />
<div className="flex grow flex-col overflow-y-hidden">
<OrgNavBar isHidden={isInsideProject} />
{!isLoading && !isInsideProject && !serverDetails?.redisConfigured && <RedisBanner />}
{!isLoading && !isInsideProject && !serverDetails?.emailConfigured && <SmtpBanner />}
{!isLoading && !isInsideProject && subscription.auditLogs && <AuditLogBanner />}
{!window.isSecureContext && !isInsideProject && <InsecureConnectionBanner />}
<main
className={twMerge(
"flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-4 pt-8 pb-4 dark:scheme-dark",
isInsideProject && "p-0"
"flex-1 overflow-x-hidden bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark",
isInsideProject ? "overflow-y-hidden p-0" : "overflow-y-auto"
)}
>
<Outlet />
@@ -9,6 +9,7 @@ import {
faEnvelope,
faExclamationTriangle,
faGlobe,
faInfinity,
faInfo,
faInfoCircle,
faServer,
@@ -43,7 +44,7 @@ import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useToggle } from "@app/hooks";
import { projectKeys, useGetOrganizations, useLogoutUser } from "@app/hooks/api";
import { projectKeys, useGetOrganizations, useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api";
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery";
@@ -162,6 +163,8 @@ export const Navbar = () => {
await navigateUserToOrg(navigate, orgId);
};
const { mutateAsync } = useGetOrgTrialUrl();
const logout = useLogoutUser();
const logOutUser = async () => {
try {
@@ -201,144 +204,192 @@ export const Navbar = () => {
const isServerAdminPanel = location.pathname.startsWith("/admin");
const isOrgScope = breadcrumbs?.length === 1; // TODO: scott/akhil is this adequate?
const isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate?
return (
<div className="z-10 flex min-h-12 items-center border-b border-mineshaft-600 bg-mineshaft-800 px-4">
<div>
<Link to="/organization/projects">
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
</Link>
</div>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{isServerAdminPanel ? (
<>
<Link
to="/admin"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
>
<div>
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
</div>
<div className="whitespace-nowrap">Server Console</div>
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1">
<div className="mr-auto flex items-center overflow-hidden">
<div className="shrink-0">
<Link to="/organization/projects">
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
</Link>
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
// scott: remove /admin as we show server console above
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
) : null}
</>
) : (
<>
<div className="flex items-center">
<DropdownMenu modal={false}>
<Link to="/organization/projects">
<div className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary">
<Badge
variant="org"
className={twMerge("text-sm", !isOrgScope && "bg-transparent opacity-75")}
>
<FontAwesomeIcon icon={faGlobe} />
{currentOrg?.name}
</Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)}
</div>
{subscription.cardDeclined && (
<Tooltip
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
className="max-w-xs"
</div>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{isServerAdminPanel ? (
<>
<Link
to="/admin"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
>
<div>
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
</div>
<div className="whitespace-nowrap">Server Console</div>
</Link>
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
// scott: remove /admin as we show server console above
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
) : null}
</>
) : (
<>
<div className="flex items-center overflow-hidden">
<DropdownMenu modal={false}>
<Link className="overflow-hidden" to="/organization/projects">
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
<Badge
variant="org"
className={twMerge(
"max-w-full min-w-0 cursor-pointer text-sm",
!isOrgScope &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
)}
>
<div className="flex items-center">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</div>
</Tooltip>
)}
</div>
</Link>
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="px-2 py-1"
>
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">organizations</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
<FontAwesomeIcon icon={faGlobe} />
<p className="truncate">{currentOrg?.name}</p>
</Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)}
</div>
{subscription.cardDeclined && (
<Tooltip
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
className="max-w-xs"
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
<div className="flex items-center">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</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>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
</>
</Tooltip>
)}
</div>
</Link>
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="px-2 py-1"
>
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
organizations
</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faSignOut} />}
onClick={logOutUser}
>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{!isOrgScope && (
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
<BreadcrumbContainer
className="min-w-[15rem] flex-1"
breadcrumbs={[breadcrumbs[0]] as TBreadcrumbFormat[]}
/>
) : null}
</>
)}
</>
)}
</div>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<Tooltip content="Start Free Pro Trial">
<Button
variant="plain"
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
leftIcon={<FontAwesomeIcon icon={faInfinity} />}
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
>
Free Pro Trial
</Button>
</Tooltip>
)}
{user.superAdmin && !location.pathname.startsWith("/admin") && (
<Link
className="mr-2 rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
to="/admin"
>
<FontAwesomeIcon icon={faServer} className="mr-2" />
Server Console
</Link>
)}
<div className="grow" />
<DropdownMenu modal={false}>
<DropdownMenuTrigger>
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2.5 py-1 hover:bg-mineshaft-600">
@@ -16,15 +16,15 @@ export const Notification = ({ notification, onDelete }: Props) => {
return (
<div
className={twMerge(
"group relative flex cursor-pointer items-start border-b border-mineshaft-600 p-3 transition-colors",
"group relative flex cursor-pointer items-start border-b border-mineshaft-600 p-2 transition-colors",
notification.link ? "hover:bg-mineshaft-700" : "cursor-default",
!notification.isRead && "bg-mineshaft-800"
)}
>
<div className="flex w-full min-w-0 flex-col">
<div className="flex w-full min-w-0 flex-col p-1">
<div className="flex gap-2">
{!notification.isRead && (
<FontAwesomeIcon icon={faCircle} className="mt-1.5 size-2 text-yellow-400" />
<FontAwesomeIcon icon={faCircle} className="mt-0.5 size-2 text-yellow-400" />
)}
<Tooltip
content={<Markdown>{notification.title}</Markdown>}
@@ -45,7 +45,7 @@ export const Notification = ({ notification, onDelete }: Props) => {
</span>
)}
</div>
<div className="flex w-0 shrink-0 justify-end opacity-0 transition-all group-hover:w-[24px] group-hover:opacity-100">
<div className="mt-0.5 flex w-0 shrink-0 justify-end opacity-0 transition-all group-hover:w-[24px] group-hover:opacity-100">
<IconButton
ariaLabel="delete"
variant="plain"
@@ -0,0 +1,109 @@
import { Link, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
type Props = {
isHidden?: boolean;
};
export const OrgNavBar = ({ isHidden }: Props) => {
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { pathname } = useLocation();
return (
<>
{!isHidden && (
<div className="dark hidden w-full flex-col overflow-x-hidden border-b border-mineshaft-600 bg-mineshaft-900 px-4 md:flex">
<motion.div
key="menu-org-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link to="/organization/projects">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Overview
</Tab>
)}
</Link>
<Link to="/organization/app-connections">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
App Connections
</Tab>
)}
</Link>
<Link to="/organization/networking">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Networking
</Tab>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""} variant="org">
Secret Sharing
</Tab>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<Tab
variant="org"
value={
isActive ||
pathname.match(
/organization\/members|organization\/identities|organization\/groups|organization\/roles/
)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link to="/organization/audit-logs">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Audit Logs
</Tab>
)}
</Link>
<Link to="/organization/billing">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Usage & Billing
</Tab>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Settings
</Tab>
)}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
</div>
)}
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};
@@ -0,0 +1 @@
export { OrgNavBar } from "./OrgNavBar";
@@ -1,214 +0,0 @@
import {
faBook,
faCog,
faInfinity,
faMoneyBill,
faNetworkWired,
faPlug,
faShare,
faTable,
faUsers,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Menu, MenuGroup, MenuItem, Tooltip } from "@app/components/v2";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useGetOrgTrialUrl } from "@app/hooks/api";
type Props = {
isHidden?: boolean;
};
export const OrgSidebar = ({ isHidden }: Props) => {
const { subscription } = useSubscription();
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
const { currentOrg } = useOrganization();
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
return (
<>
<AnimatePresence mode="popLayout">
{!isHidden && (
<motion.aside
key="org-sidebar"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: -240 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -240 }}
layout
className="dark z-10 w-60 border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-800 to-mineshaft-900"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<Menu>
<MenuGroup title="Overview">
<Link to="/organization/projects">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faTable} />
</div>
Overview
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Organization Access
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/billing">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faMoneyBill} className="mr-4" />
</div>
Usage & Billing
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/audit-logs">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} className="mr-4" />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} className="mr-4" />
</div>
Organization Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Resources">
<Link to="/organization/app-connections">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} className="mr-4" />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/networking">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faNetworkWired} className="mr-4" />
</div>
Networking
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
<div className="grow" />
<Menu>
{subscription &&
subscription.slug === "starter" &&
!subscription.has_used_trial && (
<Tooltip content="Start Free Pro Trial">
<MenuItem
variant="org"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faInfinity}
/>
}
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
>
Pro Trial
</MenuItem>
</Tooltip>
)}
<Link to="/organization/secret-sharing">
<MenuItem
variant="org"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faShare} />
</div>
}
>
Share Secret
</MenuItem>
</Link>
{user.superAdmin && (
<Link to="/admin">
<MenuItem
variant="org"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faUserTie}
/>
</div>
}
>
Server Console
</MenuItem>
</Link>
)}
</Menu>
</nav>
</motion.aside>
)}
</AnimatePresence>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};
@@ -1 +0,0 @@
export { OrgSidebar } from "./OrgSidebar";
+72 -149
View File
@@ -1,19 +1,9 @@
import { useEffect } from "react";
import {
faBook,
faBoxOpen,
faCog,
faDisplay,
faHome,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
@@ -23,7 +13,7 @@ export const PamLayout = () => {
const { currentProject } = useProject();
const { subscription } = useSubscription();
const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
useEffect(() => {
@@ -35,152 +25,85 @@ export const PamLayout = () => {
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
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-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="groups" />
PAM
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/pam/$projectId/accounts"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUser} />
</div>
Accounts
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/resources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBoxOpen} />
</div>
Resources
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/sessions"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faDisplay} />
</div>
Sessions
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/pam/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/pam/$projectId/accounts"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Accounts</Tab>}
</Link>
</Menu>
</div>
<Link
to="/projects/pam/$projectId/resources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Resources</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/sessions"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Sessions</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/pam/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
<UpgradePlanModal
@@ -1,9 +1,7 @@
import { useTranslation } from "react-i18next";
import { faArrowLeft, faMobile } from "@fortawesome/free-solid-svg-icons";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { WishForm } from "@app/components/features/WishForm";
import { Outlet } from "@tanstack/react-router";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
@@ -12,27 +10,10 @@ export const PersonalSettingsLayout = () => {
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 md:flex">
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<aside className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:scheme-dark">
<div className="grow">
<Link to="/organization/projects">
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
Back to organization
</div>
</Link>
</div>
<div className="relative mt-10 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 />}
</div>
)
</nav>
</aside>
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 dark:scheme-dark">
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark">
<Outlet />
</main>
</div>
@@ -1,23 +1,10 @@
import { useTranslation } from "react-i18next";
import {
faBell,
faBook,
faCertificate,
faCog,
faFileLines,
faHome,
faMobile,
faPlug,
faPuzzlePiece,
faSitemap,
faStamp,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission, useSubscription } from "@app/context";
import {
useListWorkspaceCertificateTemplates,
@@ -43,131 +30,88 @@ export const PkiManagerLayout = () => {
const showLegacySection =
subscription.pkiLegacyTemplates || hasExistingSubscribers || hasExistingTemplates;
const location = useLocation();
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
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-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="note" />
PKI Manager
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/cert-management/$projectId/policies"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faFileLines} />
</div>
Certificate Policies
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificates"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCertificate} />
</div>
Certificates
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faStamp} />
</div>
Certificate Authorities
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/alerting"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBell} />
</div>
Alerting
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPuzzlePiece} />
</div>
Integrations
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/cert-management/$projectId/policies"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Policies</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/certificates"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive || location.pathname.match(/\/pki-collections\//)
? "selected"
: ""
}
>
Certificates
</Tab>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive || location.pathname.match(/\/ca\//) ? "selected" : ""}>
Certificate Authorities
</Tab>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/alerting"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Alerting</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>App Connections</Tab>
)}
</Link>
{showLegacySection && (
<MenuGroup title="Legacy">
<>
{(subscription.pkiLegacyTemplates || hasExistingSubscribers) && (
<Link
to="/projects/cert-management/$projectId/subscribers"
@@ -176,14 +120,7 @@ export const PkiManagerLayout = () => {
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} variant="project">
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faSitemap} />
</div>
Subscribers
</div>
</MenuItem>
<Tab value={isActive ? "selected" : ""}>Subscribers (Legacy)</Tab>
)}
</Link>
)}
@@ -195,96 +132,55 @@ export const PkiManagerLayout = () => {
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} variant="project">
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faFileLines} />
</div>
Certificate Templates
</div>
</MenuItem>
<Tab value={isActive ? "selected" : ""}>Certificate Templates (Legacy)</Tab>
)}
</Link>
)}
</MenuGroup>
</>
)}
<MenuGroup title="Others">
<Link
to="/projects/cert-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<Link
to="/projects/cert-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
</Menu>
</div>
<Link
to="/projects/cert-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
@@ -15,7 +15,7 @@ export const AssumePrivilegeModeBanner = () => {
if (!assumedPrivilegeDetails) return null;
return (
<div className="z-10 -mx-4 flex items-center justify-center gap-2 rounded-sm border border-mineshaft-600 bg-primary-400 p-2 text-mineshaft-800 shadow-sm">
<div className="flex w-full items-center border-b border-yellow/50 bg-yellow/30 px-4 py-2 text-sm text-yellow-200">
<div>
<FontAwesomeIcon icon={faInfoCircle} className="mr-2" />
You are currently viewing the project with privileges of{" "}
@@ -24,7 +24,7 @@ export const AssumePrivilegeModeBanner = () => {
{assumedPrivilegeDetails?.actorName}
</b>
</div>
<div>
<div className="ml-auto">
<Button
size="xs"
variant="outline_bg"
@@ -35,10 +35,19 @@ import {
import { getProjectHomePage } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { useGetUserProjects } from "@app/hooks/api";
import { Project } from "@app/hooks/api/projects/types";
import { Project, ProjectType } from "@app/hooks/api/projects/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
const PROJECT_TYPE_NAME: Record<ProjectType, string> = {
[ProjectType.SecretManager]: "Secrets Management",
[ProjectType.CertificateManager]: "PKI",
[ProjectType.SSH]: "SSH",
[ProjectType.KMS]: "KMS",
[ProjectType.PAM]: "PAM",
[ProjectType.SecretScanning]: "Secret Scanning"
};
export const ProjectSelect = () => {
const [searchProject, setSearchProject] = useState("");
const { currentProject: currentWorkspace } = useProject();
@@ -99,26 +108,24 @@ export const ProjectSelect = () => {
}, [projects, projectFavorites, currentWorkspace]);
return (
<div className="-mr-2 flex w-full items-center gap-1">
<div className="mr-2 flex items-center gap-1 overflow-hidden">
<DropdownMenu modal={false}>
<Link
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
params={{
projectId: currentWorkspace.id
}}
className="group flex cursor-pointer items-center gap-x-1.5 overflow-hidden hover:text-white"
>
<div className="relative flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary">
<Tooltip content={currentWorkspace.name} className="max-w-96 break-words">
<Badge
variant="project"
className="max-w-44 overflow-hidden text-sm text-ellipsis whitespace-nowrap"
>
<FontAwesomeIcon icon={faCube} />
{currentWorkspace?.name}
</Badge>
</Tooltip>
</div>
<p className="inline-block truncate text-mineshaft-200 group-hover:underline">
{currentWorkspace?.name}
</p>
<Badge variant="project" className="cursor-pointer">
<FontAwesomeIcon icon={faCube} />
<span>
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
</span>
</Badge>
</Link>
<DropdownMenuTrigger asChild>
<div>
@@ -1,21 +1,10 @@
import { useTranslation } from "react-i18next";
import {
faArrowsSpin,
faBook,
faCheckToSlot,
faCog,
faHome,
faMobile,
faPlug,
faPuzzlePiece,
faUsers,
faVault
} from "@fortawesome/free-solid-svg-icons";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context";
import {
useGetAccessRequestsCount,
@@ -28,10 +17,10 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
export const SecretManagerLayout = () => {
const { currentProject, projectId } = useProject();
const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
const { t } = useTranslation();
const projectSlug = currentProject?.slug || "";
const location = useLocation();
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
projectId
@@ -54,208 +43,131 @@ export const SecretManagerLayout = () => {
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
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-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="vault" />
Secrets Manager
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/secret-management/$projectId/overview"
params={{
projectId: currentProject.id,
...(currentProject.environments.length
? { envSlug: currentProject.environments[0]?.slug }
: {})
}}
>
{({ isActive }) => (
<MenuItem
variant="project"
isSelected={
isActive ||
location.pathname.startsWith(
`/projects/secret-management/${currentProject.id}/overview`
)
}
>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faVault} />
</div>
Secrets
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPuzzlePiece} />
</div>
Integrations
</div>
</MenuItem>
)}
</Link>
{Boolean(secretRotations?.length) && (
<Link
to="/projects/secret-management/$projectId/secret-rotation"
params={{
projectId: currentProject.id
}}
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/secret-management/$projectId/overview"
params={{
projectId: currentProject.id,
...(currentProject.environments.length
? { envSlug: currentProject.environments[0]?.slug }
: {})
}}
>
{({ isActive }) => (
<Tab
value={
isActive || location.pathname.match(/\/secrets\/|\/commits\//)
? "selected"
: ""
}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faArrowsSpin} />
</div>
Secret Rotations
</div>
</MenuItem>
)}
</Link>
Overview
</Tab>
)}
<Link
to="/projects/secret-management/$projectId/approval"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCheckToSlot} />
</div>
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/secret-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
variant="project"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link>
</Menu>
</div>
<Link
to="/projects/secret-management/$projectId/approval"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>
Approvals
{Boolean(
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
</Link>
{Boolean(secretRotations?.length) && (
<Link
to="/projects/secret-management/$projectId/secret-rotation"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>Secret Rotations</Tab>
)}
</Link>
)}
<Link
to="/projects/secret-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>App Connections</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/secret-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
@@ -1,17 +1,7 @@
import {
faBook,
faCog,
faDatabase,
faHome,
faMagnifyingGlass,
faPlug,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import {
ProjectPermissionSub,
useProject,
@@ -29,6 +19,7 @@ export const SecretScanningLayout = () => {
const { permission } = useProjectPermission();
const { subscription } = useSubscription();
const location = useLocation();
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(
currentProject.id,
@@ -45,158 +36,94 @@ export const SecretScanningLayout = () => {
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
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-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="secret-scan" />
Secret Scanning
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/secret-scanning/$projectId/data-sources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faDatabase} />
</div>
Data Sources
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/findings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex w-full gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faMagnifyingGlass} />
</div>
<span>Findings</span>
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="mr-2 ml-auto h-min">
{unresolvedFindings}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/secret-scanning/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
variant="project"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/secret-scanning/$projectId/data-sources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Data Sources</Tab>}
</Link>
</Menu>
</div>
<Link
to="/projects/secret-scanning/$projectId/findings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>
Findings
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="ml-2 h-min">
{unresolvedFindings}
</Badge>
)}
</Tab>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>App Connections</Tab>}
</Link>
<Link
to="/projects/secret-scanning/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/secret-scanning/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
);
+87 -141
View File
@@ -1,17 +1,8 @@
import {
faBook,
faCog,
faHome,
faServer,
faStamp,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@@ -24,149 +15,104 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
export const SshLayout = () => {
const { currentProject } = useProject();
const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
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-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="terminal" />
SSH
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/ssh/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faServer} />
</div>
Hosts
</div>
</MenuItem>
)}
</Link>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to="/projects/ssh/$projectId/cas"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faStamp} />
</div>
Certificates Authority
</div>
</MenuItem>
)}
</Link>
)
}
</ProjectPermissionCan>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/ssh/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/ssh/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/ssh/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
variant="project"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/ssh/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive || location.pathname.match(/\/ssh-host-groups\//) ? "selected" : ""
}
>
Hosts
</Tab>
)}
</Link>
</Menu>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to="/projects/ssh/$projectId/cas"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={isActive || location.pathname.match(/\/ca\//) ? "selected" : ""}
>
Certificate Authorities
</Tab>
)}
</Link>
)
}
</ProjectPermissionCan>
<Link
to="/projects/ssh/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/ssh/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/ssh/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
);
@@ -13,8 +13,8 @@ export const AccessManagementPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Access Control" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Access Control"
@@ -13,8 +13,8 @@ export const AuthenticationPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Authentication"
@@ -13,8 +13,8 @@ export const CachingPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Caching"
@@ -13,8 +13,8 @@ export const EncryptionPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Encryption"
@@ -13,8 +13,8 @@ export const EnvironmentPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Environment Variables"
@@ -15,8 +15,8 @@ export const GeneralPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="General"
@@ -13,8 +13,8 @@ export const IntegrationsPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Integrations"
@@ -13,18 +13,24 @@ export const ResourceOverviewPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Resource Overview" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Resource Overview"
description="Manage resources within your Infisical instance."
/>
<Tabs defaultValue="tab-organizations">
<Tabs orientation="vertical" defaultValue="tab-organizations">
<TabList>
<Tab value="tab-organizations">Organizations</Tab>
<Tab value="tab-users">Users</Tab>
<Tab value="tab-identities">Identities</Tab>
<Tab variant="instance" value="tab-organizations">
Organizations
</Tab>
<Tab variant="instance" value="tab-users">
Users
</Tab>
<Tab variant="instance" value="tab-identities">
Identities
</Tab>
</TabList>
<TabPanel value="tab-organizations">
<OrganizationsTable />
@@ -4,19 +4,20 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PkiAlertsSection } from "./components";
export const AlertingPage = () => {
const { t } = useTranslation();
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Alerting" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Alerting"
description="Configure alerts for expiring certificates and CAs to maintain security and compliance."
/>
@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet";
import { useNavigate, useParams } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
@@ -18,6 +20,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaType, useDeleteCa, useGetCa } from "@app/hooks/api";
import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal";
@@ -84,10 +87,24 @@ const Page = () => {
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader scope="project" title={data.name}>
<div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Certificate Authorities
</Link>
<PageHeader
scope={ProjectType.CertificateManager}
description="Manage certificate authority"
title={data.name}
>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { ExternalCaSection } from "./components/ExternalCaSection";
import { CaSection } from "./components";
@@ -11,13 +12,13 @@ import { CaSection } from "./components";
export const CertificateAuthoritiesPage = () => {
const { t } = useTranslation();
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificate Authorities" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Certificate Authorities"
description="Manage certificate authorities for issuing and signing certificates"
/>
@@ -9,6 +9,7 @@ import {
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PkiCollectionSection } from "../AlertingPage/components";
import { CertificatesSection } from "./components";
@@ -27,13 +28,13 @@ export const CertificatesPage = () => {
);
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Certificates"
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
/>
@@ -7,6 +7,7 @@ import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub, useProject } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import { PkiSyncsTab } from "./components";
@@ -42,16 +43,18 @@ export const IntegrationsListPage = () => {
<meta property="og:title" content="Manage your certificates in seconds" />
<meta name="og:description" content="Sync and manage PKI certificates across services" />
</Helmet>
<div className="relative container mx-auto max-w-7xl pb-12 text-white">
<div className="relative mx-auto max-w-8xl pb-12 text-white">
<div className="mb-8">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Integrations"
description="Manage integrations with third-party certificate services."
/>
<Tabs value={currentTab} onValueChange={updateSelectedTab}>
<Tabs orientation="vertical" value={currentTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={IntegrationsListPageTabs.PkiSyncs}>Certificate Syncs</Tab>
<Tab variant="project" value={IntegrationsListPageTabs.PkiSyncs}>
Certificate Syncs
</Tab>
</TabList>
<TabPanel value={IntegrationsListPageTabs.PkiSyncs}>
<ProjectPermissionCan
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
@@ -19,6 +21,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { useDeletePkiCollection, useGetPkiCollectionById } from "@app/hooks/api";
import { PkiItemType } from "@app/hooks/api/pkiCollections/constants";
import { ProjectType } from "@app/hooks/api/projects/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { PkiCollectionModal } from "../AlertingPage/components/PkiCollectionModal";
@@ -70,10 +73,24 @@ export const PkiCollectionPage = () => {
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader scope="project" title={data.name}>
<div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to="/projects/cert-management/$projectId/certificates"
params={{
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Certificates
</Link>
<PageHeader
scope={ProjectType.CertificateManager}
title={data.name}
description="Manage certificate collection"
>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
@@ -22,6 +24,7 @@ import {
useProject
} from "@app/context";
import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/projects/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { PkiSubscriberModal } from "../PkiSubscribersPage/components/PkiSubscriberModal";
@@ -75,10 +78,24 @@ const Page = () => {
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader scope="project" title={data.name}>
<div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to="/projects/cert-management/$projectId/subscribers"
params={{
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Subscribers
</Link>
<PageHeader
scope={ProjectType.CertificateManager}
title={data.name}
description="Manage PKI subscriber"
>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PkiSubscriberSection } from "./components";
@@ -13,10 +14,10 @@ export const PkiSubscribersPage = () => {
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Subscribers"
description="Manage subscribers that request and receive certificates, including user devices, servers, and services."
/>
@@ -68,8 +68,8 @@ const PageContent = () => {
return (
<>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<Button
variant="link"
type="submit"
@@ -48,6 +48,7 @@ import {
import { usePopUp } from "@app/hooks";
import { useDeleteCertTemplateV2 } from "@app/hooks/api";
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
import { ProjectType } from "@app/hooks/api/projects/types";
import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal";
import { PkiTemplateForm } from "./components/PkiTemplateForm";
@@ -104,15 +105,15 @@ export const PkiTemplateListPage = () => {
<title>{t("common.head-title", { title: "PKI Templates" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Certificate Templates"
description="Manage certificate template to request and issue dynamic certificates following a strict format."
/>
</div>
<div className="container mx-auto mb-6 max-w-7xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="container mx-auto mb-6 max-w-8xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
{subscription?.pkiLegacyTemplates && (
<div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
@@ -270,7 +271,7 @@ export const PkiTemplateListPage = () => {
onDeleteApproved={() => onRemovePkiSubscriberSubmit()}
/>
</div>
<div className="container mx-auto max-w-7xl" />
<div className="container mx-auto max-w-8xl" />
</div>
<Modal
isOpen={popUp?.certificateTemplate?.isOpen}
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
@@ -31,8 +32,8 @@ export const PoliciesPage = () => {
{(isAllowed) => {
if (!isAllowed) {
return (
<div className="container mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl text-center">
<div className="mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl text-center">
<p>You don&apos;t have permission to access certificate policies.</p>
</div>
</div>
@@ -40,23 +41,25 @@ export const PoliciesPage = () => {
}
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificate Policies" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Certificate Policies"
description="Manage certificate templates and profiles for unified certificate issuance"
/>
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TabSections)}>
<TabList className="w-full">
<div className="flex w-full border-b border-mineshaft-600">
<Tab value={TabSections.CertificateProfiles}>Certificate Profiles</Tab>
<Tab value={TabSections.CertificateTemplatesV2}>Certificate Templates</Tab>
</div>
<Tabs orientation="vertical" value={activeTab} onValueChange={(value) => setActiveTab(value as TabSections)}>
<TabList>
<Tab variant="project" value={TabSections.CertificateProfiles}>
Certificate Profiles
</Tab>
<Tab variant="project" value={TabSections.CertificateTemplatesV2}>
Certificate Templates
</Tab>
</TabList>
<TabPanel value={TabSections.CertificateProfiles}>
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectType } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
const tabs = [
@@ -20,12 +21,12 @@ export const SettingsPage = () => {
<Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader scope="project" title={t("settings.project.title")} />
<Tabs defaultValue={tabs[0].key}>
<div className="w-full max-w-8xl">
<PageHeader scope={ProjectType.CertificateManager} title={t("settings.project.title")} />
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
<Tab value={tab.key} variant="project" key={tab.key}>
{tab.name}
</Tab>
))}
+4 -3
View File
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { KmipClientTable } from "./components/KmipClientTable";
@@ -15,10 +16,10 @@ export const KmipPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.KMS}
title="KMIP"
description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
/>
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { CmekTable } from "./components";
@@ -15,10 +16,10 @@ export const OverviewPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title>
</Helmet>
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.KMS}
title="Overview"
description="Manage keys and perform cryptographic operations."
/>
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectType } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
const tabs = [
@@ -20,12 +21,16 @@ export const SettingsPage = () => {
<Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader scope="project" title="Settings" />
<Tabs defaultValue={tabs[0].key}>
<div className="w-full max-w-8xl">
<PageHeader
scope={ProjectType.KMS}
title="Settings"
description="Configure general project settings"
/>
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
<Tab variant="project" value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
@@ -76,11 +76,11 @@ export const AccessManagementPage = () => {
const hasNoAccess = tabSections.every((tab) => tab.isHidden);
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="org"
title="Access Control"
@@ -111,7 +111,7 @@ export const AccessManagementPage = () => {
isOpen={isUpgradePrivilegeSystemModalOpen}
onOpenChange={setIsUpgradePrivilegeSystemModalOpen}
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
{tabSections
.filter((el) => !el.isHidden)
@@ -1,17 +1,5 @@
import { motion } from "framer-motion";
import { OrgGroupsSection } from "./components";
export const OrgGroupsTab = () => {
return (
<motion.div
key="panel-org-groups"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgGroupsSection />
</motion.div>
);
return <OrgGroupsSection />;
};
@@ -1,17 +1,5 @@
import { motion } from "framer-motion";
import { IdentitySection } from "./components";
export const OrgIdentityTab = () => {
return (
<motion.div
key="panel-service-token"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<IdentitySection />
</motion.div>
);
return <IdentitySection />;
};
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faInfoCircle, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
@@ -37,9 +37,12 @@ import {
IdentityKubernetesAuthTokenReviewMode,
IdentityTrustedIp
} from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { useGetVaultExternalMigrationConfigs } from "@app/hooks/api/migration/queries";
import { VaultKubernetesAuthRole } from "@app/hooks/api/migration/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
import { VaultKubernetesAuthImportModal } from "./VaultKubernetesAuthImportModal";
const schema = z
.object({
@@ -121,6 +124,12 @@ export const IdentityKubernetesAuthForm = ({
enabled: isUpdate
});
const { popUp, handlePopUpToggle: handleImportPopUpToggle } = usePopUp([
"importFromVault"
] as const);
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
const {
control,
handleSubmit,
@@ -192,6 +201,99 @@ export const IdentityKubernetesAuthForm = ({
}
}, [data]);
const handleImportFromVault = (role: VaultKubernetesAuthRole) => {
try {
setValue("kubernetesHost", role.config.kubernetes_host, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});
if (role.bound_service_account_names?.length > 0) {
// In Vault, "*" means allow all; in Infisical, empty field means allow any
const allowedNames = role.bound_service_account_names.includes("*")
? ""
: role.bound_service_account_names.join(", ");
setValue("allowedNames", allowedNames, {
shouldDirty: true,
shouldTouch: true
});
}
if (role.bound_service_account_namespaces?.length > 0) {
// In Vault, "*" means allow all; in Infisical, empty field means allow any
const allowedNamespaces = role.bound_service_account_namespaces.includes("*")
? ""
: role.bound_service_account_namespaces.join(", ");
setValue("allowedNamespaces", allowedNamespaces, {
shouldDirty: true,
shouldTouch: true
});
}
if (role.token_ttl !== undefined) {
setValue("accessTokenTTL", String(role.token_ttl), {
shouldDirty: true,
shouldTouch: true
});
}
if (role.token_max_ttl !== undefined) {
setValue("accessTokenMaxTTL", String(role.token_max_ttl), {
shouldDirty: true,
shouldTouch: true
});
}
if (role.token_num_uses !== undefined) {
setValue("accessTokenNumUsesLimit", String(role.token_num_uses), {
shouldDirty: true,
shouldTouch: true
});
}
if (role.audience) {
setValue("allowedAudience", role.audience, {
shouldDirty: true,
shouldTouch: true
});
}
if (role.config.kubernetes_ca_cert) {
setValue("caCert", role.config.kubernetes_ca_cert, {
shouldDirty: true,
shouldTouch: true
});
}
if (
subscription?.ipAllowlisting &&
role.token_bound_cidrs &&
role.token_bound_cidrs.length > 0
) {
setValue(
"accessTokenTrustedIps",
role.token_bound_cidrs.map((cidr) => ({ ipAddress: cidr })),
{
shouldDirty: true,
shouldTouch: true
}
);
}
createNotification({
type: "info",
text: `Successfully prefilled values from Kubernetes auth role: ${role.name}`
});
} catch (err) {
console.error("Import error:", err);
createNotification({
type: "error",
text: "Failed to import Kubernetes auth configuration"
});
}
};
const onFormSubmit = async ({
kubernetesHost,
tokenReviewerJwt,
@@ -301,6 +403,28 @@ export const IdentityKubernetesAuthForm = ({
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
{hasVaultConnection && !isUpdate && (
<div className="mb-4 flex items-center justify-between rounded-md border border-primary/30 bg-primary/10 p-3">
<div className="flex items-start gap-2 text-sm">
<FontAwesomeIcon icon={faInfoCircle} className="mt-0.5 text-primary" />
<span className="text-mineshaft-200">Load values from HashiCorp Vault</span>
</div>
<Button
variant="outline_bg"
size="xs"
leftIcon={
<img
src="/images/integrations/Vault.png"
alt="HashiCorp Vault"
className="h-4 w-4"
/>
}
onClick={() => handleImportPopUpToggle("importFromVault", true)}
>
Load from Vault
</Button>
</div>
)}
<div className="flex w-full items-center gap-2">
<div className="w-full flex-1">
<OrgPermissionCan
@@ -407,6 +531,7 @@ export const IdentityKubernetesAuthForm = ({
placeholder="https://my-example-k8s-api-host.com"
type="text"
value={field.value || ""}
autoComplete="off"
/>
</FormControl>
)}
@@ -425,7 +550,7 @@ export const IdentityKubernetesAuthForm = ({
errorText={error?.message}
tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
>
<Input {...field} placeholder="" type="password" />
<Input {...field} placeholder="" type="password" autoComplete="new-password" />
</FormControl>
)}
/>
@@ -441,7 +566,12 @@ export const IdentityKubernetesAuthForm = ({
errorText={error?.message}
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
>
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
<Input
{...field}
placeholder="namespaceA, namespaceB"
type="text"
autoComplete="off"
/>
</FormControl>
)}
/>
@@ -456,7 +586,11 @@ export const IdentityKubernetesAuthForm = ({
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
errorText={error?.message}
>
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
<Input
{...field}
placeholder="service-account-1-name, service-account-1-name"
autoComplete="off"
/>
</FormControl>
)}
/>
@@ -628,6 +762,11 @@ export const IdentityKubernetesAuthForm = ({
Cancel
</Button>
</div>
<VaultKubernetesAuthImportModal
isOpen={popUp.importFromVault.isOpen}
onOpenChange={(isOpen) => handleImportPopUpToggle("importFromVault", isOpen)}
onImport={handleImportFromVault}
/>
</form>
);
};
@@ -0,0 +1,200 @@
import { useEffect, useState } from "react";
import { createNotification } from "@app/components/notifications";
import {
Button,
FilterableSelect,
FormControl,
Modal,
ModalClose,
ModalContent
} from "@app/components/v2";
import {
useGetVaultAuthMounts,
useGetVaultKubernetesAuthRoles,
useGetVaultNamespaces
} from "@app/hooks/api/migration/queries";
import { VaultKubernetesAuthRole } from "@app/hooks/api/migration/types";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onImport: (role: VaultKubernetesAuthRole) => void;
};
type ContentProps = {
onClose: () => void;
onImport: (role: VaultKubernetesAuthRole) => void;
};
const Content = ({ onClose, onImport }: ContentProps) => {
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
const [selectedMountPath, setSelectedMountPath] = useState<string | null>(null);
const [selectedRole, setSelectedRole] = useState<VaultKubernetesAuthRole | null>(null);
const [shouldFetchRoles, setShouldFetchRoles] = useState(false);
const [shouldFetchMounts, setShouldFetchMounts] = useState(false);
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
const { data: authMounts, isLoading: isLoadingMounts } = useGetVaultAuthMounts(
shouldFetchMounts,
selectedNamespace ?? undefined,
"kubernetes"
);
const { data: roles, isLoading: isLoadingRoles } = useGetVaultKubernetesAuthRoles(
shouldFetchRoles,
selectedNamespace ?? undefined,
selectedMountPath ?? undefined
);
// Enable fetching mounts when namespace is selected
useEffect(() => {
if (selectedNamespace) {
setShouldFetchMounts(true);
}
}, [selectedNamespace]);
// Enable fetching roles when both namespace and mount path are selected
useEffect(() => {
if (selectedNamespace && selectedMountPath) {
setShouldFetchRoles(true);
} else {
setShouldFetchRoles(false);
}
}, [selectedNamespace, selectedMountPath]);
const handleImportAndApply = () => {
if (!selectedRole) {
createNotification({
type: "error",
text: "Please select a Kubernetes role to load"
});
return;
}
onImport(selectedRole);
onClose();
};
return (
<>
<FormControl
label="Namespace"
className="mb-4"
tooltipText="Select the Vault namespace containing the Kubernetes auth configuration."
>
<>
<FilterableSelect
value={namespaces?.find((ns) => ns.name === selectedNamespace)}
onChange={(value) => {
if (value && !Array.isArray(value)) {
const namespace = value as { id: string; name: string };
setSelectedNamespace(namespace.name);
setSelectedMountPath(null);
setSelectedRole(null);
}
}}
options={namespaces || []}
getOptionValue={(option) => option.name}
getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)}
isDisabled={isLoadingNamespaces}
placeholder="Select namespace..."
className="w-full"
/>
<p className="mt-1 text-xs text-mineshaft-400">
Select the Vault namespace to fetch available auth mounts
</p>
</>
</FormControl>
<FormControl
label="Auth Engine"
className="mb-4"
tooltipText="Select the Kubernetes auth engine to narrow down available roles."
>
<>
<FilterableSelect
value={
selectedMountPath
? authMounts?.find((mount) => mount.path === selectedMountPath)
: null
}
onChange={(value) => {
if (value && !Array.isArray(value)) {
const mount = value as { path: string; type: string };
setSelectedMountPath(mount.path.replace(/\/$/, "")); // Remove trailing slash
setSelectedRole(null);
} else {
setSelectedMountPath(null);
}
}}
options={authMounts || []}
getOptionValue={(option) => option.path}
getOptionLabel={(option) => option.path.replace(/\/$/, "")}
isDisabled={isLoadingMounts || !authMounts?.length}
placeholder="Select auth engine..."
isClearable
className="w-full"
/>
<p className="mt-1 text-xs text-mineshaft-400">
Choose a Kubernetes auth engine to filter available roles
</p>
</>
</FormControl>
<FormControl label="Kubernetes Role" className="mb-6">
<>
<FilterableSelect
value={selectedRole}
onChange={(value) => {
if (value && !Array.isArray(value)) {
setSelectedRole(value as VaultKubernetesAuthRole);
} else {
setSelectedRole(null);
}
}}
options={roles || []}
getOptionValue={(option) => option.name}
getOptionLabel={(option) => option.name}
isDisabled={isLoadingRoles || !roles?.length || !selectedMountPath}
placeholder={
!selectedMountPath
? "Select an auth engine first..."
: "Select a Kubernetes role to load..."
}
isClearable
className="w-full"
/>
<p className="mt-1 text-xs text-mineshaft-400">
Select the Kubernetes role to load configuration from
</p>
</>
</FormControl>
<div className="mt-8 flex space-x-4">
<Button onClick={handleImportAndApply} isDisabled={!selectedRole || isLoadingRoles}>
Load
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};
export const VaultKubernetesAuthImportModal = ({ isOpen, onOpenChange, onImport }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
bodyClassName="overflow-visible"
title="Load Kubernetes Auth from HashiCorp Vault"
subTitle="Load Kubernetes authentication configuration from your Vault instance. The auth method and role settings will be automatically translated and prefilled in the form."
className="max-w-2xl"
>
<Content onClose={() => onOpenChange(false)} onImport={onImport} />
</ModalContent>
</Modal>
);
};
@@ -1,17 +1,5 @@
import { motion } from "framer-motion";
import { OrgMembersSection } from "./components";
export const OrgMembersTab = () => {
return (
<motion.div
key="panel-org-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgMembersSection />
</motion.div>
);
return <OrgMembersSection />;
};
@@ -1,17 +1,5 @@
import { motion } from "framer-motion";
import { OrgRoleTable } from "./OrgRoleTable";
export const OrgRoleTabSection = () => {
return (
<motion.div
key="role-list"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgRoleTable />
</motion.div>
);
return <OrgRoleTable />;
};
@@ -20,7 +20,7 @@ export const AppConnectionsPage = withPermission(
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
className="w-full"
@@ -12,9 +12,8 @@ export const AuditLogsPage = () => {
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<div className="flex w-full justify-center bg-bunker-800 pb-6 text-white">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
title="Audit Logs"
@@ -18,7 +18,7 @@ export const BillingPage = () => {
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
title={t("billing.title")}
@@ -17,15 +17,15 @@ const tabs = [
export const BillingTabGroup = withPermission(
() => {
const tabsFiltered = isInfisicalCloud()
? tabs
: [{ name: "Infisical Self-Hosted", key: "tab-infisical-cloud" }];
if (!isInfisicalCloud()) {
return <BillingCloudTab />;
}
return (
<Tabs defaultValue={tabs[0].key}>
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList>
{tabsFiltered.map((tab) => (
<Tab variant="org" value={tab.key}>
{tabs.map((tab) => (
<Tab variant="org" key={tab.key} value={tab.key}>
{tab.name}
</Tab>
))}
@@ -33,19 +33,15 @@ export const BillingTabGroup = withPermission(
<TabPanel value={tabs[0].key}>
<BillingCloudTab />
</TabPanel>
{isInfisicalCloud() && (
<>
<TabPanel value={tabs[1].key}>
<BillingSelfHostedTab />
</TabPanel>
<TabPanel value={tabs[2].key}>
<BillingReceiptsTab />
</TabPanel>
<TabPanel value={tabs[3].key}>
<BillingDetailsTab />
</TabPanel>
</>
)}
<TabPanel value={tabs[1].key}>
<BillingSelfHostedTab />
</TabPanel>
<TabPanel value={tabs[2].key}>
<BillingReceiptsTab />
</TabPanel>
<TabPanel value={tabs[3].key}>
<BillingDetailsTab />
</TabPanel>
</Tabs>
);
},
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -80,10 +82,20 @@ const Page = () => {
if (isPending) return <Spinner size="sm" className="mt-2 ml-2" />;
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader scope="org" title={data.group.name}>
<div className="mx-auto w-full max-w-8xl">
<Link
to="/organization/access-management"
search={{
selectedTab: TabSections.Groups
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Groups
</Link>
<PageHeader scope="org" description="Organization Group" title={data.group.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
@@ -72,10 +74,20 @@ const Page = () => {
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader scope="org" title={data.identity.name} />
<div className="mx-auto w-full max-w-8xl">
<Link
to="/organization/access-management"
search={{
selectedTab: OrgAccessControlTabSections.Identities
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Identities
</Link>
<PageHeader scope="org" description="Organization Identity" title={data.identity.name} />
<div className="flex">
<div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
@@ -12,7 +12,7 @@ export const NetworkingPage = () => {
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
title="Networking"
@@ -19,7 +19,7 @@ export const NetworkingTabGroup = () => {
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab variant="org" value={tab.key} key={tab.key}>
@@ -58,18 +58,16 @@ export const ProjectsPage = () => {
: true;
return (
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800">
<div className="mx-auto flex max-w-8xl flex-col justify-start bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="mb-4 flex flex-col items-start justify-start">
<PageHeader
scope="org"
title="Overview"
description="Your team's complete security toolkit - organized and ready when you need them."
/>
</div>
<PageHeader
scope="org"
title="Overview"
description="Your team's complete security toolkit - organized and ready when you need them."
/>
{projectListView === ProjectListView.MyProjects ? (
<MyProjectView
onAddNewProject={() => handlePopUpOpen("addNewWs")}
@@ -1,8 +1,8 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faCopy, faEllipsisV } from "@fortawesome/free-solid-svg-icons";
import { faChevronLeft, faCopy, faEllipsisV } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
@@ -77,20 +77,26 @@ export const Page = () => {
const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? "");
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto w-full max-w-8xl">
<Link
to="/organization/access-management"
search={{
selectedTab: OrgAccessControlTabSections.Roles
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Roles
</Link>
<PageHeader
scope="org"
title={
<div className="flex flex-col">
<div>
<span>{data.name}</span>
<p className="text-sm leading-3 font-normal text-mineshaft-400 normal-case">
{data.slug} {data.description && `- ${data.description}`}
</p>
</div>
</div>
title={data.name}
description={
<>
{data.slug} {data.description && `- ${data.description}`}
</>
}
>
{isCustomRole && (
@@ -20,7 +20,7 @@ export const SecretSharingPage = () => {
<meta name="og:description" content={String(t("approval.og-description"))} />
</Helmet>
<div className="h-full">
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
<div className="mx-auto h-full w-full max-w-8xl bg-bunker-800 text-white">
<PageHeader
scope="org"
title="Secret Sharing"
@@ -1,7 +1,7 @@
import { Helmet } from "react-helmet";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { Badge, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
@@ -34,14 +34,13 @@ export const ShareSecretSection = () => {
<meta property="og:image" content="/images/message.png" />
</Helmet>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={SecretSharingPageTabs.ShareSecret}>Share Secrets</Tab>
<Tab value={SecretSharingPageTabs.RequestSecret}>
<Tab variant="org" value={SecretSharingPageTabs.ShareSecret}>
Share Secrets
</Tab>
<Tab variant="org" value={SecretSharingPageTabs.RequestSecret}>
Request Secrets
<Badge variant="primary" className="ml-1 cursor-pointer text-xs">
New
</Badge>
</Tab>
</TabList>
<TabPanel value={SecretSharingPageTabs.ShareSecret}>
@@ -20,7 +20,7 @@ export const SecretSharingSettingsPage = withPermission(
<title>{t("common.head-title", { title: "Secret Share Settings" })}</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<div className="w-full max-w-8xl">
<PageHeader scope="org" title="Secret Share Settings" />
<SecretSharingSettingsTabGroup />
</div>
@@ -14,8 +14,12 @@ export const SettingsPage = () => {
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader scope="org" title={t("settings.org.title")} />
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
description="Configure organization-wide settings"
title={t("settings.org.title")}
/>
<OrgTabGroup />
</div>
</div>
@@ -7,6 +7,7 @@ import { OrgMembershipRole } from "@app/helpers/roles";
import { usePopUp } from "@app/hooks";
import { SelectImportFromPlatformModal } from "./components/SelectImportFromPlatformModal";
import { VaultConnectionSection } from "./components/VaultConnectionSection";
export const ExternalMigrationsTab = () => {
const { hasOrgRole } = useOrgPermission();
@@ -14,45 +15,70 @@ export const ExternalMigrationsTab = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const);
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<div className="flex items-center gap-2">
<p className="text-xl font-medium text-mineshaft-100">Import from external source</p>
<div className="flex flex-col gap-6">
{/* In-Platform Migration Tooling Section */}
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
<h2 className="text-xl font-medium text-mineshaft-100">In-Platform Migration Tooling</h2>
<p className="mt-1 mb-6 text-sm text-gray-400">
Configure platform connections to enable migration features throughout Infisical, such
as importing policies and resources directly within the UI.
</p>
</div>
<VaultConnectionSection />
</div>
<div>
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/external-migrations/overview"
>
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pt-[0.04rem] pb-[0.03rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="text-xxs mb-[0.07rem] ml-1.5"
/>
</div>
</a>
</div>
{/* Bulk Data Import Section */}
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
<h2 className="text-xl font-medium text-mineshaft-100">Bulk Data Import</h2>
<p className="mt-1 mb-6 text-sm text-gray-400">
Perform one-time bulk imports of data from external platforms.
</p>
</div>
<Button
onClick={() => {
handlePopUpOpen("selectImportPlatform");
}}
isDisabled={!hasOrgRole(OrgMembershipRole.Admin)}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Import
</Button>
</div>
<p className="mb-4 text-gray-400">Import data from another platform to Infisical.</p>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2">
<p className="text-base font-medium text-mineshaft-100">
Import from external source
</p>
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/external-migrations/overview"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 pt-[0.04rem] pb-[0.03rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="text-xxs mb-[0.07rem] ml-1.5"
/>
</div>
</a>
</div>
<p className="mt-1 text-sm text-gray-400">
Import data from another platform to Infisical.
</p>
</div>
<SelectImportFromPlatformModal
isOpen={popUp.selectImportPlatform.isOpen}
onToggle={(state) => handlePopUpToggle("selectImportPlatform", state)}
/>
<Button
onClick={() => {
handlePopUpOpen("selectImportPlatform");
}}
isDisabled={!hasOrgRole(OrgMembershipRole.Admin)}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Import
</Button>
</div>
<SelectImportFromPlatformModal
isOpen={popUp.selectImportPlatform.isOpen}
onToggle={(state) => handlePopUpToggle("selectImportPlatform", state)}
/>
</div>
</div>
);
};
@@ -0,0 +1,197 @@
import { useState } from "react";
import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import {
Button,
DeleteActionModal,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useListAppConnections } from "@app/hooks/api/appConnections/queries";
import {
useDeleteVaultExternalMigrationConfig,
useGetVaultExternalMigrationConfigs
} from "@app/hooks/api/migration";
import { TVaultExternalMigrationConfig } from "@app/hooks/api/migration/types";
import { VaultNamespaceConfigModal } from "./VaultNamespaceConfigModal";
export const VaultConnectionSection = () => {
const [selectedConfig, setSelectedConfig] = useState<TVaultExternalMigrationConfig | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [configToDelete, setConfigToDelete] = useState<TVaultExternalMigrationConfig | null>(null);
const { data: configs = [], isPending: isLoadingConfigs } = useGetVaultExternalMigrationConfigs();
const { data: appConnections = [] } = useListAppConnections();
const { mutateAsync: deleteConfig } = useDeleteVaultExternalMigrationConfig();
const handleEdit = (config: TVaultExternalMigrationConfig) => {
setSelectedConfig(config);
setIsModalOpen(true);
};
const handleAdd = () => {
setSelectedConfig(null);
setIsModalOpen(true);
};
const handleDeleteClick = (config: TVaultExternalMigrationConfig) => {
setConfigToDelete(config);
setIsDeleteModalOpen(true);
};
const handleDeleteConfirm = async () => {
if (!configToDelete) return;
try {
await deleteConfig({ id: configToDelete.id });
createNotification({
type: "success",
text: "Namespace configuration deleted successfully"
});
setIsDeleteModalOpen(false);
setConfigToDelete(null);
} catch (error) {
console.error("Failed to delete namespace config:", error);
createNotification({
type: "error",
text: "Failed to delete namespace configuration"
});
}
};
const getConnectionName = (connectionId: string | null) => {
if (!connectionId) return "None";
const connection = appConnections.find((conn) => conn.id === connectionId);
return connection?.name || "Unknown";
};
return (
<div>
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<img
src="/images/integrations/Vault.png"
alt="HashiCorp Vault logo"
className="h-10 w-10 rounded-md bg-bunker-500 p-2"
/>
<div>
<h3 className="text-lg font-medium text-mineshaft-100">HashiCorp Vault</h3>
<p className="text-sm text-gray-400">
Enable in-platform migration tooling for policy imports, auth methods, and secret
engine migrations
</p>
</div>
</div>
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleAdd}
>
Add Namespace
</Button>
</div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Namespace</Th>
<Th>Connection</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoadingConfigs && (
<TableSkeleton columns={3} innerKey="vault-configs-loading" rows={3} />
)}
{!isLoadingConfigs && configs.length === 0 && (
<Tr>
<Td colSpan={3}>
<EmptyState title="No namespace configurations" icon={faPlus} className="py-8">
<p className="mb-4 text-sm text-mineshaft-400">
Add a namespace configuration to enable in-platform migration features.
</p>
</EmptyState>
</Td>
</Tr>
)}
{!isLoadingConfigs &&
configs.map((config) => (
<Tr key={config.id} className="group h-10">
<Td>{config.namespace}</Td>
<Td>{getConnectionName(config.connectionId)}</Td>
<Td>
<div className="flex items-center justify-end gap-2 opacity-0 transition-opacity group-hover:opacity-100">
<Button
variant="plain"
colorSchema="secondary"
size="xs"
onClick={() => handleEdit(config)}
leftIcon={<FontAwesomeIcon icon={faEdit} />}
>
Edit
</Button>
<Button
variant="plain"
colorSchema="danger"
size="xs"
onClick={() => handleDeleteClick(config)}
leftIcon={<FontAwesomeIcon icon={faTrash} />}
>
Delete
</Button>
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
<p className="mt-4 text-xs text-mineshaft-400">
Configure namespace-specific connections to enable in-platform migration features. Manage
connections in the{" "}
<Link
to="/organization/app-connections"
className="text-primary underline hover:text-primary-300"
>
App Connections
</Link>{" "}
section.
</p>
<VaultNamespaceConfigModal
isOpen={isModalOpen}
onOpenChange={(open) => {
setIsModalOpen(open);
if (!open) setSelectedConfig(null);
}}
editConfig={selectedConfig || undefined}
/>
<DeleteActionModal
isOpen={isDeleteModalOpen}
title={`Delete namespace configuration for "${configToDelete?.namespace}"?`}
onChange={(open) => {
setIsDeleteModalOpen(open);
if (!open) setConfigToDelete(null);
}}
deleteKey="confirm"
onDeleteApproved={handleDeleteConfirm}
/>
</div>
);
};
@@ -0,0 +1,189 @@
import { useEffect, useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FilterableSelect,
FormControl,
Input,
Modal,
ModalContent
} from "@app/components/v2";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { useListAppConnections } from "@app/hooks/api/appConnections/queries";
import {
useCreateVaultExternalMigrationConfig,
useUpdateVaultExternalMigrationConfig
} from "@app/hooks/api/migration";
import { TVaultExternalMigrationConfig } from "@app/hooks/api/migration/types";
const schema = z.object({
namespace: z
.string()
.min(1, "Namespace is required. If you intend to use the root namespace, use root or /."),
connectionId: z.string().min(1, "Connection is required")
});
type FormData = z.infer<typeof schema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
editConfig?: TVaultExternalMigrationConfig;
};
export const VaultNamespaceConfigModal = ({ isOpen, onOpenChange, editConfig }: Props) => {
const isEdit = Boolean(editConfig);
const { data: appConnections = [], isPending: isLoadingConnections } = useListAppConnections();
const vaultConnections = useMemo(
() => appConnections.filter((conn) => conn.app === AppConnection.HCVault),
[appConnections]
);
const { mutateAsync: createConfig, isPending: isCreating } =
useCreateVaultExternalMigrationConfig();
const { mutateAsync: updateConfig, isPending: isUpdating } =
useUpdateVaultExternalMigrationConfig();
const {
control,
handleSubmit,
reset,
formState: { errors, isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
namespace: "",
connectionId: ""
}
});
// Reset form when editConfig changes or modal opens
useEffect(() => {
if (isOpen) {
reset({
namespace: editConfig?.namespace || "",
connectionId: editConfig?.connectionId || ""
});
}
}, [isOpen, editConfig, reset]);
const onFormSubmit = async (data: FormData) => {
try {
if (isEdit && editConfig) {
await updateConfig({
id: editConfig.id,
namespace: data.namespace,
connectionId: data.connectionId
});
createNotification({
type: "success",
text: "Namespace configuration updated successfully"
});
} else {
await createConfig({
namespace: data.namespace,
connectionId: data.connectionId
});
createNotification({
type: "success",
text: "Namespace configuration created successfully"
});
}
reset();
onOpenChange(false);
} catch (error) {
console.error("Failed to save namespace config:", error);
createNotification({
type: "error",
text: `Failed to ${isEdit ? "update" : "create"} namespace configuration`
});
}
};
const handleClose = () => {
reset();
onOpenChange(false);
};
return (
<Modal isOpen={isOpen} onOpenChange={handleClose}>
<ModalContent
title={isEdit ? "Edit Namespace Configuration" : "Add Namespace Configuration"}
subTitle={`Configure a HashiCorp Vault namespace ${isEdit ? "configuration" : "for migration tooling"}`}
bodyClassName="overflow-visible"
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="namespace"
render={({ field }) => (
<FormControl
label="Namespace"
isError={Boolean(errors.namespace)}
errorText={errors.namespace?.message}
className="mb-4"
>
<Input {...field} placeholder="e.g., admin, dev, prod" autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="connectionId"
render={({ field }) => {
const selectedConnection = vaultConnections.find((conn) => conn.id === field.value);
return (
<FormControl
label="Vault Connection"
isError={Boolean(errors.connectionId)}
errorText={errors.connectionId?.message}
tooltipText="Select a HashiCorp Vault app connection for this namespace"
>
<FilterableSelect
value={selectedConnection || null}
onChange={(newValue) => {
const singleValue = Array.isArray(newValue) ? newValue[0] : newValue;
if (singleValue && "id" in singleValue) {
field.onChange(singleValue.id);
} else {
field.onChange("");
}
}}
isLoading={isLoadingConnections}
options={vaultConnections}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
);
}}
/>
<div className="mt-8 flex items-center gap-2">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting || isCreating || isUpdating}
isDisabled={isSubmitting || isCreating || isUpdating}
>
{isEdit ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain" onClick={handleClose}>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -55,7 +55,7 @@ export const OrgTabGroup = () => {
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab variant="org" value={tab.key} key={tab.key}>
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -115,16 +117,30 @@ const Page = withPermission(
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{membership && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto w-full max-w-8xl">
<Link
to="/organization/access-management"
search={{
selectedTab: OrgAccessControlTabSections.Member
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Users
</Link>
<PageHeader
scope="org"
title={
membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
: "-"
: (membership.user.username ??
membership.user.email ??
membership.inviteEmail ??
"Unknown User")
}
description="Organization User Membership"
>
<div>
{userId !== membership.user.id && (
@@ -5,6 +5,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPamAccountActions } from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PamAccountsSection } from "./components/PamAccountsSection";
@@ -21,10 +22,10 @@ export const PamAccountsPage = () => {
a={ProjectPermissionSub.PamAccounts}
>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.PAM}
title="Accounts"
description="View, access, and manage accounts."
/>
@@ -0,0 +1,67 @@
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
type Props = {
path: string;
};
export const FolderBreadCrumbs = ({ path = "/" }: Props) => {
const navigate = useNavigate({
from: "/projects/pam/$projectId/accounts"
});
const onFolderCrumbClick = (index: number) => {
let newAccountPath = `/${path.split("/").filter(Boolean).slice(0, index).join("/")}`;
if (!newAccountPath.endsWith("/")) {
newAccountPath += "/";
}
if (path === newAccountPath) return;
navigate({
search: (prev) => ({ ...prev, accountPath: newAccountPath })
});
};
return (
<div className="flex items-center space-x-2">
<div
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 py-1 pr-2 pl-5 text-sm hover:bg-mineshaft-600"
onClick={() => onFolderCrumbClick(0)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onFolderCrumbClick(0);
}
}}
role="button"
tabIndex={0}
>
<FontAwesomeIcon icon={faFolderOpen} className="text-primary-700" />
</div>
{(path || "")
.split("/")
.filter(Boolean)
.map((pathSegment, index, arr) => (
<div
key={`path-${index + 1}`}
className={`breadcrumb relative z-20 ${
index + 1 === arr.length ? "cursor-default" : "cursor-pointer"
} border-solid border-mineshaft-600 py-1 pr-2 pl-5 text-sm text-mineshaft-200`}
onClick={() => onFolderCrumbClick(index + 1)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onFolderCrumbClick(index + 1);
}
}}
role="button"
tabIndex={0}
>
{pathSegment}
</div>
))}
</div>
);
};
@@ -45,6 +45,7 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { PAM_RESOURCE_TYPE_MAP, TPamAccount, TPamFolder } from "@app/hooks/api/pam";
import { AccountView, AccountViewToggle } from "./AccountViewToggle";
import { FolderBreadCrumbs } from "./FolderBreadCrumbs";
import { PamAccessAccountModal } from "./PamAccessAccountModal";
import { PamAccountRow } from "./PamAccountRow";
import { PamAddAccountModal } from "./PamAddAccountModal";
@@ -255,6 +256,7 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
return (
<div>
{accountView === AccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
<div className="mt-4 flex gap-2">
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
{(isAllowed) =>
@@ -5,6 +5,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPamAccountActions } from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PamResourcesSection } from "./components/PamResourcesSection";
@@ -21,10 +22,10 @@ export const PamResourcesPage = () => {
a={ProjectPermissionSub.PamResources}
>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.PAM}
title="Resources"
description="Manage resources such as servers, databases, and more."
/>
@@ -1,12 +1,15 @@
import { Helmet } from "react-helmet";
import { useParams } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useParams } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSub, useProject } from "@app/context";
import { ProjectPermissionPamSessionActions } from "@app/context/ProjectPermissionContext/types";
import { useGetPamSessionById } from "@app/hooks/api/pam";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PamSessionDetailsSection } from "./components/PamSessionDetailsSection";
import { PamSessionLogsSection } from "./components/PamSessionLogsSection";
@@ -17,13 +20,23 @@ const Page = () => {
select: (el) => el.sessionId
});
const { data: session } = useGetPamSessionById(sessionId);
const { currentProject } = useProject();
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{session && (
<div className="mx-auto mb-6 flex w-full max-w-7xl flex-col">
<div className="mx-auto mb-6 flex w-full max-w-8xl flex-col">
<Link
to="/projects/pam/$projectId/sessions"
params={{
projectId: currentProject.id
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Sessions
</Link>
<PageHeader
scope="project"
scope={ProjectType.PAM}
title={`${session.accountName} Session`}
description={`View details for this ${session.accountName} session.`}
/>
@@ -5,6 +5,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPamSessionActions } from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PamSessionSection } from "./components/PamSessionSection";
@@ -21,10 +22,10 @@ export const PamSessionPage = () => {
a={ProjectPermissionSub.PamSessions}
>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.PAM}
title="Sessions"
description="Filter and search through account sessions."
/>
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectType } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
export const SettingsPage = () => {
@@ -12,11 +13,17 @@ export const SettingsPage = () => {
<Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader scope="project" title="Settings" description="Configure your PAM project." />
<Tabs defaultValue="tab-project-general">
<div className="w-full max-w-8xl">
<PageHeader
scope={ProjectType.PAM}
title="Settings"
description="Configure your PAM project."
/>
<Tabs orientation="vertical" defaultValue="tab-project-general">
<TabList>
<Tab value="tab-project-general">General</Tab>
<Tab variant="project" value="tab-project-general">
General
</Tab>
</TabList>
<TabPanel value="tab-project-general">
<ProjectGeneralTab />
@@ -37,26 +37,32 @@ const Page = () => {
const isSecretManager = currentProject.type === ProjectType.SecretManager;
return (
<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">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={currentProject.type}
title="Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your project resources."
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={ProjectAccessControlTabs.Member}>Users</Tab>
<Tab value={ProjectAccessControlTabs.Groups}>Groups</Tab>
<Tab value={ProjectAccessControlTabs.Identities}>
<div className="flex items-center">
<p>Machine Identities</p>
</div>
<Tab variant="project" value={ProjectAccessControlTabs.Member}>
Users
</Tab>
<Tab variant="project" value={ProjectAccessControlTabs.Groups}>
Groups
</Tab>
<Tab variant="project" value={ProjectAccessControlTabs.Identities}>
Identities
</Tab>
{isSecretManager && (
<Tab value={ProjectAccessControlTabs.ServiceTokens}>Service Tokens</Tab>
<Tab variant="project" value={ProjectAccessControlTabs.ServiceTokens}>
Service Tokens
</Tab>
)}
<Tab value={ProjectAccessControlTabs.Roles}>Project Roles</Tab>
<Tab variant="project" value={ProjectAccessControlTabs.Roles}>
Roles
</Tab>
</TabList>
<TabPanel value={ProjectAccessControlTabs.Member}>
<MembersTab />
@@ -1,5 +1,3 @@
import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
@@ -7,17 +5,7 @@ import { GroupsSection } from "./components";
export const GroupsTab = withProjectPermission(
() => {
return (
<motion.div
key="panel-groups"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<GroupsSection />
</motion.div>
);
return <GroupsSection />;
},
{
action: ProjectPermissionActions.Read,
@@ -14,7 +14,6 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
@@ -158,307 +157,295 @@ export const IdentityTab = withProjectPermission(
};
return (
<motion.div
key="identity-role-panel"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-[0.16rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Identity}
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
rel="noopener noreferrer"
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("identity")}
isDisabled={!isAllowed}
>
Add Identity
</Button>
)}
</ProjectPermissionCan>
<div className="mt-[0.16rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<Input
containerClassName="mb-4"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search identities by name..."
/>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th className="w-1/3">
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={`ml-2 ${
orderBy === ProjectIdentityOrderBy.Name ? "" : "opacity-30"
}`}
ariaLabel="sort"
onClick={() => handleSort(ProjectIdentityOrderBy.Name)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC &&
orderBy === ProjectIdentityOrderBy.Name
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-1/3">Role</Th>
<Th>Added on</Th>
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="project-identities" />}
{!isPending &&
data &&
data.identityMemberships.length > 0 &&
data.identityMemberships.map((identityMember) => {
const {
identity: { id, name },
roles,
createdAt
} = identityMember;
return (
<Tr
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`st-v3-${id}`}
role="button"
tabIndex={0}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
navigate({
to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const,
params: {
projectId: currentProject.id,
identityId: id
}
});
}
}}
onClick={() =>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Identity}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("identity")}
isDisabled={!isAllowed}
>
Add Identity
</Button>
)}
</ProjectPermissionCan>
</div>
<Input
containerClassName="mb-4"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search identities by name..."
/>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th className="w-1/3">
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={`ml-2 ${
orderBy === ProjectIdentityOrderBy.Name ? "" : "opacity-30"
}`}
ariaLabel="sort"
onClick={() => handleSort(ProjectIdentityOrderBy.Name)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC &&
orderBy === ProjectIdentityOrderBy.Name
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-1/3">Role</Th>
<Th>Added on</Th>
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="project-identities" />}
{!isPending &&
data &&
data.identityMemberships.length > 0 &&
data.identityMemberships.map((identityMember) => {
const {
identity: { id, name },
roles,
createdAt
} = identityMember;
return (
<Tr
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`st-v3-${id}`}
role="button"
tabIndex={0}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
navigate({
to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const,
params: {
projectId: currentProject.id,
identityId: id
}
})
});
}
>
<Td>{name}</Td>
}}
onClick={() =>
navigate({
to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const,
params: {
projectId: currentProject.id,
identityId: id
}
})
}
>
<Td>{name}</Td>
<Td>
<div className="flex items-center space-x-2">
{roles
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(
({
role,
customRoleName,
id: roleId,
isTemporary,
temporaryAccessEndTime
}) => {
const isExpired =
new Date() > new Date(temporaryAccessEndTime || ("" as string));
return (
<Tag key={roleId}>
<div className="flex items-center space-x-2">
<div className="capitalize">
{formatProjectRoleName(role, customRoleName)}
</div>
{isTemporary && (
<div>
<Tooltip
content={
isExpired
? "Timed role expired"
: "Timed role access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(isExpired && "text-red-600")}
/>
</Tooltip>
</div>
)}
<Td>
<div className="flex items-center space-x-2">
{roles
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(
({
role,
customRoleName,
id: roleId,
isTemporary,
temporaryAccessEndTime
}) => {
const isExpired =
new Date() > new Date(temporaryAccessEndTime || ("" as string));
return (
<Tag key={roleId}>
<div className="flex items-center space-x-2">
<div className="capitalize">
{formatProjectRoleName(role, customRoleName)}
</div>
</Tag>
);
}
)}
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
<HoverCard>
<HoverCardTrigger>
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
</HoverCardTrigger>
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
{roles
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(
({
role,
customRoleName,
id: roleId,
isTemporary,
temporaryAccessEndTime
}) => {
const isExpired =
new Date() >
new Date(temporaryAccessEndTime || ("" as string));
return (
<Tag key={roleId} className="capitalize">
<div className="flex items-center space-x-2">
<div>
{formatProjectRoleName(role, customRoleName)}
</div>
{isTemporary && (
<div>
<Tooltip
content={
isExpired
? "Access expired"
: "Temporary access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(
new Date() >
new Date(
temporaryAccessEndTime as string
) && "text-red-600"
)}
/>
</Tooltip>
</div>
)}
</div>
</Tag>
);
}
)}
</HoverCardContent>
</HoverCard>
{isTemporary && (
<div>
<Tooltip
content={
isExpired ? "Timed role expired" : "Timed role access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(isExpired && "text-red-600")}
/>
</Tooltip>
</div>
)}
</div>
</Tag>
);
}
)}
</div>
</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end space-x-2">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Identity, {
identityId: id
})}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faCircleXmark} />}
isDisabled={!isAllowed}
onClick={(evt) => {
evt.stopPropagation();
evt.preventDefault();
handlePopUpOpen("deleteIdentity", {
identityId: id,
name
});
}}
>
Remove Identity From Project
</DropdownMenuItem>
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
<HoverCard>
<HoverCardTrigger>
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
</HoverCardTrigger>
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
{roles
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(
({
role,
customRoleName,
id: roleId,
isTemporary,
temporaryAccessEndTime
}) => {
const isExpired =
new Date() >
new Date(temporaryAccessEndTime || ("" as string));
return (
<Tag key={roleId} className="capitalize">
<div className="flex items-center space-x-2">
<div>{formatProjectRoleName(role, customRoleName)}</div>
{isTemporary && (
<div>
<Tooltip
content={
isExpired
? "Access expired"
: "Temporary access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(
new Date() >
new Date(
temporaryAccessEndTime as string
) && "text-red-600"
)}
/>
</Tooltip>
</div>
)}
</div>
</Tag>
);
}
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && data && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
{!isPending && data && data?.identityMemberships.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0
? "No identities match search filter"
: "No identities have been added to this project"
}
icon={faServer}
/>
)}
</TableContainer>
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure you want to remove ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveIdentitySubmit(
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
)
}
/>
</div>
</motion.div>
</HoverCardContent>
</HoverCard>
)}
</div>
</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end space-x-2">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Identity, {
identityId: id
})}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faCircleXmark} />}
isDisabled={!isAllowed}
onClick={(evt) => {
evt.stopPropagation();
evt.preventDefault();
handlePopUpOpen("deleteIdentity", {
identityId: id,
name
});
}}
>
Remove Identity From Project
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && data && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
{!isPending && data && data?.identityMemberships.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0
? "No identities match search filter"
: "No identities have been added to this project"
}
icon={faServer}
/>
)}
</TableContainer>
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure you want to remove ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveIdentitySubmit(
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
)
}
/>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity }
@@ -1,5 +1,3 @@
import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
@@ -7,17 +5,7 @@ import { MembersSection } from "./components";
export const MembersTab = withProjectPermission(
() => {
return (
<motion.div
key="panel-project-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<MembersSection />
</motion.div>
);
return <MembersSection />;
},
{
action: ProjectPermissionActions.Read,
@@ -1,5 +1,3 @@
import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
@@ -7,17 +5,7 @@ import { ProjectRoleList } from "./components/ProjectRoleList";
export const ProjectRoleListTab = withProjectPermission(
() => {
return (
<motion.div
key="role-list"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectRoleList />
</motion.div>
);
return <ProjectRoleList />;
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Role }
);
@@ -1,20 +1,12 @@
// import { faWarning } from "@fortawesome/free-solid-svg-icons";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { ServiceTokenSection } from "./components";
export const ServiceTokenTab = () => {
return (
<motion.div
key="panel-service-token"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="space-y-3">
{/* <div className="flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
<div className="space-y-3">
{/* <div className="flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
<FontAwesomeIcon icon={faWarning} className="pr-6 text-4xl text-white/80" />
<div className="flex w-full flex-col text-sm">
<span className="mb-4 text-lg font-medium">Deprecation Notice</span>
@@ -42,8 +34,7 @@ export const ServiceTokenTab = () => {
</p>
</div>
</div> */}
<ServiceTokenSection />
</div>
</motion.div>
<ServiceTokenSection />
</div>
);
};
@@ -21,9 +21,9 @@ export const AppConnectionsPage = withProjectPermission(
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<div className="w-full max-w-8xl">
<PageHeader
scope="project"
scope={currentProject.type}
className="w-full"
title="App Connections"
description="Manage project App Connections"
@@ -8,15 +8,15 @@ export const AuditLogsPage = () => {
const { currentProject } = useProject();
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>Project Audit Logs</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<div className="w-full max-w-8xl">
<PageHeader
scope="project"
scope={currentProject.type}
title="Audit logs"
description="Audit logs for security and compliance teams to monitor information access."
/>
@@ -1,11 +1,16 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useParams } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useParams } from "@tanstack/react-router";
import { formatRelative } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, PageHeader, Spinner } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import { useGetWorkspaceGroupMembershipDetails } from "@app/hooks/api/projects/queries";
import { ProjectAccessControlTabs } from "@app/types/project";
import { GroupDetailsSection } from "./components/GroupDetailsSection";
import { GroupMembersSection } from "./components/GroupMembersSection";
@@ -31,10 +36,27 @@ const Page = () => {
);
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{groupMembership ? (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader scope="project" title={groupMembership.group.name} />
<div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to={`${getProjectBaseURL(currentProject.type)}/access-management`}
params={{
projectId: currentProject.id
}}
search={{
selectedTab: ProjectAccessControlTabs.Groups
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Groups
</Link>
<PageHeader
scope={currentProject.type}
title={groupMembership.group.name}
description={`Group joined on ${formatRelative(new Date(groupMembership.createdAt || ""), new Date())}`}
/>
<div className="flex">
<div className="mr-4 w-96">
<GroupDetailsSection groupMembership={groupMembership} />

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