mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 07:26:00 +00:00
Merge branch 'main' into PAM-10
This commit is contained in:
@@ -41,7 +41,6 @@
|
||||
"common": {
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "A project with this name already exists.",
|
||||
"no-mobile": " To use Infisical, please log in through a device with larger dimensions. ",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"first-name": "First Name",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"common": {
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "Ya existe un proyecto con este nombre.",
|
||||
"no-mobile": "Para usar Infisical, inicia sesión con un dispositivo de mayores dimesiones.",
|
||||
"email": "Correo electrónico",
|
||||
"password": "Contraseña",
|
||||
"first-name": "Nombre",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"common": {
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "Un projet avec ce nom existe déjà.",
|
||||
"no-mobile": " Pour utiliser Infisical, veuillez vous connecter avec un appareil avec des dimensions plus grandes. ",
|
||||
"email": "Email",
|
||||
"password": "Mot de passe",
|
||||
"first-name": "Prénom",
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"common": {
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "동일한 이름을 가진 프로젝트가 이미 존재해요.",
|
||||
"no-mobile": " Infisical을 사용하려면, 큰 화면을 가진 디바이스로 로그인하여 주세요.",
|
||||
"email": "메일",
|
||||
"password": "비밀번호",
|
||||
"first-name": "이름",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"common": {
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "Já exite um projeto com este nome.",
|
||||
"no-mobile": "Para usar o Infisical, faça o login através de um dispositivo com dimensões maiores.",
|
||||
"email": "Email",
|
||||
"password": "Senha",
|
||||
"first-name": "Primeiro Nome",
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"common": {
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "Bu isimle bir proje zaten mevcut.",
|
||||
"no-mobile": " Infisical'ı kullanmak için, lütfen daha büyük boyutlara sahip bir cihaz üzerinden giriş yapın. ",
|
||||
"email": "Email",
|
||||
"password": "Şifre",
|
||||
"first-name": "Adınız",
|
||||
|
||||
@@ -76,8 +76,8 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="justify-betweens flex">
|
||||
<h2 className="mb-8 flex-1 text-xl font-medium text-mineshaft-100">Project Overview</h2>
|
||||
<div className="justify-betweens mb-8 flex flex-wrap gap-2">
|
||||
<h2 className="flex-1 text-xl font-medium text-mineshaft-100">Project Overview</h2>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
|
||||
@@ -8,13 +8,15 @@ import {
|
||||
} from "../enums";
|
||||
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
|
||||
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
||||
import { TSSHAccount, TSSHResource } from "./ssh-resource";
|
||||
|
||||
export * from "./mysql-resource";
|
||||
export * from "./postgres-resource";
|
||||
export * from "./ssh-resource";
|
||||
|
||||
export type TPamResource = TPostgresResource | TMySQLResource;
|
||||
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource;
|
||||
|
||||
export type TPamAccount = TPostgresAccount | TMySQLAccount;
|
||||
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount;
|
||||
|
||||
export type TPamFolder = {
|
||||
id: string;
|
||||
@@ -26,6 +28,22 @@ export type TPamFolder = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
// Session log types
|
||||
export type TPamCommandLog = {
|
||||
input: string;
|
||||
output: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
export type TTerminalEvent = {
|
||||
timestamp: string;
|
||||
eventType: "input" | "output" | "resize" | "error";
|
||||
data: string; // Base64 encoded binary data
|
||||
elapsedTime: number; // Seconds since session start (for replay)
|
||||
};
|
||||
|
||||
export type TPamSessionLog = TPamCommandLog | TTerminalEvent;
|
||||
|
||||
export type TPamSession = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
@@ -44,11 +62,7 @@ export type TPamSession = {
|
||||
endedAt?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
commandLogs: {
|
||||
input: string;
|
||||
output: string;
|
||||
timestamp: string;
|
||||
}[];
|
||||
logs: TPamSessionLog[];
|
||||
};
|
||||
|
||||
// Resource DTOs
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { PamResourceType } from "../enums";
|
||||
import { TBasePamAccount } from "./base-account";
|
||||
import { TBasePamResource } from "./base-resource";
|
||||
|
||||
export enum SSHAuthMethod {
|
||||
Password = "password",
|
||||
PublicKey = "public-key",
|
||||
Certificate = "certificate"
|
||||
}
|
||||
|
||||
export type TSSHConnectionDetails = {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
|
||||
export type TSSHPasswordCredentials = {
|
||||
authMethod: SSHAuthMethod.Password;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TSSHPublicKeyCredentials = {
|
||||
authMethod: SSHAuthMethod.PublicKey;
|
||||
username: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export type TSSHCertificateCredentials = {
|
||||
authMethod: SSHAuthMethod.Certificate;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type TSSHCredentials =
|
||||
| TSSHPasswordCredentials
|
||||
| TSSHPublicKeyCredentials
|
||||
| TSSHCertificateCredentials;
|
||||
|
||||
// Resources
|
||||
export type TSSHResource = TBasePamResource & { resourceType: PamResourceType.SSH } & {
|
||||
connectionDetails: TSSHConnectionDetails;
|
||||
rotationAccountCredentials?: TSSHCredentials | null;
|
||||
};
|
||||
|
||||
// Accounts
|
||||
export type TSSHAccount = TBasePamAccount & {
|
||||
credentials: TSSHCredentials;
|
||||
};
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { Banner } from "@app/components/page-frames/Banner";
|
||||
@@ -15,7 +12,6 @@ import { InsecureConnectionBanner } from "../OrganizationLayout/components/Insec
|
||||
import { AdminNavBar } from "./AdminNavBar";
|
||||
|
||||
export const AdminLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useServerConfig();
|
||||
const { data: serverDetails, isLoading } = useFetchServerStatus();
|
||||
const { subscription } = useSubscription();
|
||||
@@ -26,7 +22,7 @@ export const AdminLayout = () => {
|
||||
<>
|
||||
<Banner />
|
||||
<div
|
||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||
className={`dark ${containerHeight} flex w-full flex-col overflow-x-hidden bg-bunker-800 transition-all`}
|
||||
>
|
||||
<Navbar />
|
||||
{!isLoading && !serverDetails?.redisConfigured && <RedisBanner />}
|
||||
@@ -40,12 +36,6 @@ export const AdminLayout = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
<Banner />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const KmsLayout = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Outlet, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -27,8 +24,6 @@ export const OrganizationLayout = () => {
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
const { data: serverDetails, isLoading } = useFetchServerStatus();
|
||||
@@ -38,7 +33,7 @@ export const OrganizationLayout = () => {
|
||||
<>
|
||||
<Banner />
|
||||
<div
|
||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||
className={`dark ${containerHeight} flex w-full flex-col overflow-x-hidden bg-bunker-800 transition-all`}
|
||||
>
|
||||
<Navbar />
|
||||
<div className="flex grow flex-col overflow-y-hidden">
|
||||
@@ -61,12 +56,6 @@ export const OrganizationLayout = () => {
|
||||
isOpen={popUp?.createOrg?.isOpen}
|
||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||
/>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
<Banner />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,15 +16,19 @@ import {
|
||||
faSignOut,
|
||||
faToolbox,
|
||||
faUser,
|
||||
faUserCog,
|
||||
faUserPlus,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
||||
import { UserPlusIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Mfa } from "@app/components/auth/Mfa";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
BreadcrumbContainer,
|
||||
@@ -44,7 +48,13 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription,
|
||||
useUser
|
||||
} from "@app/context";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
@@ -294,7 +304,7 @@ export const Navbar = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center overflow-hidden">
|
||||
<div className="flex min-w-12 items-center overflow-hidden">
|
||||
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
|
||||
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
|
||||
<Badge
|
||||
@@ -324,7 +334,7 @@ export const Navbar = () => {
|
||||
<span>{currentOrg?.name}</span>
|
||||
</button>
|
||||
</Badge>
|
||||
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
|
||||
<div className="mr-1 hidden rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline! md:inline-block">
|
||||
{getPlan(subscription)}
|
||||
</div>
|
||||
{subscription.cardDeclined && (
|
||||
@@ -468,7 +478,7 @@ export const Navbar = () => {
|
||||
className={twMerge(
|
||||
"gap-x-1.5 text-sm",
|
||||
!isOrgScope &&
|
||||
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
|
||||
"min-w-6 bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
|
||||
)}
|
||||
>
|
||||
<Link to="/organization/projects">
|
||||
@@ -567,15 +577,36 @@ export const Navbar = () => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{user.superAdmin && !location.pathname.startsWith("/admin") && (
|
||||
<Link
|
||||
className="mr-2 flex items-center 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"
|
||||
>
|
||||
<InstanceIcon className="mr-2 inline-block size-3.5" />
|
||||
Server Console
|
||||
</Link>
|
||||
)}
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!location.pathname.startsWith("/admin") ? (
|
||||
user.superAdmin ? (
|
||||
<Link
|
||||
className="mr-2 flex h-[34px] items-center 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"
|
||||
>
|
||||
<InstanceIcon className="inline-block size-3.5" />
|
||||
<span className="ml-2 hidden md:inline-block">Server Console</span>
|
||||
</Link>
|
||||
) : (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) =>
|
||||
isAllowed ? (
|
||||
<Link
|
||||
className="mr-2 flex h-[34px] items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||
to="/organization/access-management"
|
||||
search={{
|
||||
selectedTab: "members",
|
||||
action: "invite-members"
|
||||
}}
|
||||
>
|
||||
<UserPlusIcon className="inline-block size-3.5" />
|
||||
<span className="ml-2 hidden md:inline-block">Invite Members</span>
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
</OrgPermissionCan>
|
||||
)
|
||||
) : null}
|
||||
<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">
|
||||
@@ -657,8 +688,27 @@ export const Navbar = () => {
|
||||
</div>
|
||||
</div>
|
||||
<Link to="/personal-settings">
|
||||
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserCog} />}>
|
||||
Personal Settings
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) =>
|
||||
isAllowed ? (
|
||||
<Link
|
||||
to="/organization/access-management"
|
||||
search={{
|
||||
selectedTab: "members",
|
||||
action: "invite-members"
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserPlus} />}>
|
||||
Invite Members
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
</OrgPermissionCan>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
|
||||
@@ -21,7 +21,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
|
||||
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">
|
||||
<div className="dark flex w-full flex-col overflow-x-hidden border-b border-mineshaft-600 bg-mineshaft-900 px-4">
|
||||
<motion.div
|
||||
key="menu-org-items"
|
||||
initial={{ x: -150 }}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const PamLayout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
|
||||
export const PersonalSettingsLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<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>
|
||||
<div className="dark flex h-screen w-full flex-col overflow-x-hidden bg-bunker-800">
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<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>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
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";
|
||||
|
||||
@@ -17,7 +14,6 @@ export const PkiManagerLayout = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
const { subscription } = useSubscription();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: subscribers = [] } = useListWorkspacePkiSubscribers(currentProject?.id || "");
|
||||
const { data: templatesData } = useListWorkspaceCertificateTemplates({
|
||||
@@ -32,147 +28,137 @@ export const PkiManagerLayout = () => {
|
||||
|
||||
const location = useLocation();
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<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="px-4"
|
||||
>
|
||||
<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" : ""}>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 && (
|
||||
<>
|
||||
{(subscription.pkiLegacyTemplates || hasExistingSubscribers) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/subscribers"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>Subscribers (Legacy)</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{(subscription.pkiLegacyTemplates || hasExistingTemplates) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-templates"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Certificate Templates (Legacy)
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<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="px-4"
|
||||
>
|
||||
<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" : ""}>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
|
||||
to="/projects/cert-management/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive ||
|
||||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
</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 && (
|
||||
<>
|
||||
{(subscription.pkiLegacyTemplates || hasExistingSubscribers) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/subscribers"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
Access Control
|
||||
</Tab>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>Subscribers (Legacy)</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.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>
|
||||
{(subscription.pkiLegacyTemplates || hasExistingTemplates) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-templates"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Certificate Templates (Legacy)
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive ||
|
||||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Access Control
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/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>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -106,7 +106,9 @@ export const ProjectSelect = () => {
|
||||
</p>
|
||||
<Badge variant="project">
|
||||
<ProjectIcon />
|
||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||
<span className="hidden sm:inline-block">
|
||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||
</span>
|
||||
</Badge>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
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";
|
||||
|
||||
@@ -20,7 +17,6 @@ export const SecretManagerLayout = () => {
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
const location = useLocation();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const projectSlug = currentProject?.slug || "";
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
|
||||
@@ -42,141 +38,131 @@ export const SecretManagerLayout = () => {
|
||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<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="px-4"
|
||||
>
|
||||
<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"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Overview
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/approval"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<Badge variant="warning" isSquare 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
|
||||
}}
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<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="px-4"
|
||||
>
|
||||
<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 }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>Secret Rotations</Tab>
|
||||
)}
|
||||
</Link>
|
||||
Overview
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/approval"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<Badge variant="warning" isSquare 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/app-connections"
|
||||
to="/projects/secret-management/$projectId/secret-rotation"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>App Connections</Tab>
|
||||
<Tab value={isActive ? "selected" : ""}>Secret Rotations</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>
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ export const SecretScanningLayout = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SshLayout = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
|
||||
+1
-1
@@ -368,7 +368,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
name="type"
|
||||
defaultValue={CaType.ACME}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export const OrgGroupsSection = () => {
|
||||
|
||||
return (
|
||||
<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="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Groups</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
|
||||
|
||||
+2
-2
@@ -97,7 +97,7 @@ export const IdentitySection = withPermission(
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex w-full items-center gap-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-1 items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
||||
@@ -139,7 +139,7 @@ export const IdentitySection = withPermission(
|
||||
</div>
|
||||
{/* Identity Auth Templates Section */}
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identity Auth Templates</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/auth-templates" />
|
||||
|
||||
+25
-6
@@ -1,7 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { BanIcon } from "lucide-react";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { BanIcon, UserPlusIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge, DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -35,6 +37,7 @@ import { OrgMembersTable } from "./OrgMembersTable";
|
||||
export const OrgMembersSection = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const navigate = useNavigate();
|
||||
const orgId = currentOrg?.id ?? "";
|
||||
const { user } = useUser();
|
||||
const userId = user?.id || "";
|
||||
@@ -55,6 +58,22 @@ export const OrgMembersSection = () => {
|
||||
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
||||
|
||||
const urlAction = useSearch({
|
||||
from: ROUTE_PATHS.Organization.AccessControlPage.id,
|
||||
select: (el) => el.action,
|
||||
structuralSharing: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (urlAction === "invite-members") {
|
||||
handlePopUpOpen("addMember");
|
||||
navigate({
|
||||
to: ".",
|
||||
search: ({ action, ...search }) => search
|
||||
});
|
||||
}
|
||||
}, [urlAction]);
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
||||
const { mutateAsync: deleteBatchMutateAsync } = useDeleteOrgMembershipBatch();
|
||||
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
|
||||
@@ -184,7 +203,7 @@ export const OrgMembersSection = () => {
|
||||
</div>
|
||||
</div>
|
||||
<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="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Users</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
|
||||
@@ -194,13 +213,13 @@ export const OrgMembersSection = () => {
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
leftIcon={<UserPlusIcon size={16} />}
|
||||
onClick={() =>
|
||||
isSubOrganization ? handlePopUpOpen("addMemberToSubOrg") : handleAddMemberModal()
|
||||
}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Member
|
||||
{isSubOrganization ? "Add Members" : "Invite Members"}
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
|
||||
+2
-2
@@ -392,7 +392,7 @@ export const OrgMembersTable = ({
|
||||
}}
|
||||
/>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<Th className="min-w-40 md:w-1/3 md:min-w-0">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
@@ -505,7 +505,7 @@ export const OrgMembersTable = ({
|
||||
<Td
|
||||
className={twMerge("group max-w-0", isActive ? "" : "text-mineshaft-400")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex w-full items-center gap-x-2">
|
||||
<p className="truncate">
|
||||
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ export const OrgRoleTable = () => {
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">
|
||||
{isSubOrganization ? "Sub-" : ""}Organization Roles
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ export const AppConnectionsTable = ({ projectId, projectType }: Props) => {
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">App Connections</p>
|
||||
|
||||
@@ -150,8 +150,8 @@ const Page = () => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<div className="w-full md:w-96">
|
||||
<GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<GroupMembersSection groupId={groupId} groupSlug={data.group.slug} />
|
||||
|
||||
@@ -116,8 +116,8 @@ const Page = () => {
|
||||
)}
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<div className="w-full md:w-96">
|
||||
<IdentityDetailsSection
|
||||
isOrgIdentity={data.identity.orgId === currentOrg.id}
|
||||
identityId={identityId}
|
||||
|
||||
@@ -125,7 +125,7 @@ export const AllProjectView = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex w-full flex-row flex-wrap gap-2 md:flex-nowrap md:gap-0">
|
||||
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
|
||||
@@ -360,7 +360,7 @@ export const MyProjectView = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex w-full flex-row flex-wrap gap-2 md:flex-nowrap md:gap-0">
|
||||
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50/60 duration-200 focus:bg-mineshaft-700/80"
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ export const ExternalMigrationsTab = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-base font-medium text-mineshaft-100">
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ export const VaultConnectionSection = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
|
||||
+6
-4
@@ -34,9 +34,11 @@ export const ProjectTemplatesSection = () => {
|
||||
project setup
|
||||
</p>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Templates</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/project-templates" />
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Templates</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/project-templates" />
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
@@ -57,7 +59,7 @@ export const ProjectTemplatesSection = () => {
|
||||
handlePopUpOpen("addTemplate");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-auto"
|
||||
// className="ml-auto"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
|
||||
@@ -242,8 +242,8 @@ const Page = withPermission(
|
||||
)}
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<div className="w-full md:w-96">
|
||||
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<div className="w-full space-y-2">
|
||||
|
||||
@@ -58,15 +58,19 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props)
|
||||
return duration;
|
||||
}, [duration]);
|
||||
|
||||
const command = useMemo(
|
||||
() =>
|
||||
account &&
|
||||
(account.resource.resourceType === PamResourceType.Postgres ||
|
||||
account.resource.resourceType === PamResourceType.MySQL)
|
||||
? `infisical pam db access-account ${account.id} --duration ${cliDuration}`
|
||||
: "",
|
||||
[account, cliDuration]
|
||||
);
|
||||
const command = useMemo(() => {
|
||||
if (!account) return "";
|
||||
|
||||
switch (account.resource.resourceType) {
|
||||
case PamResourceType.Postgres:
|
||||
case PamResourceType.MySQL:
|
||||
return `infisical pam db access-account ${account.id} --duration ${cliDuration}`;
|
||||
case PamResourceType.SSH:
|
||||
return `infisical pam ssh access-account ${account.id} --duration ${cliDuration}`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}, [account, cliDuration]);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DiscriminativePick } from "@app/types";
|
||||
import { PamAccountHeader } from "../PamAccountHeader";
|
||||
import { MySQLAccountForm } from "./MySQLAccountForm";
|
||||
import { PostgresAccountForm } from "./PostgresAccountForm";
|
||||
import { SshAccountForm } from "./SshAccountForm";
|
||||
|
||||
type FormProps = {
|
||||
onComplete: (account: TPamAccount) => void;
|
||||
@@ -65,6 +66,10 @@ const CreateForm = ({
|
||||
return (
|
||||
<MySQLAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
|
||||
);
|
||||
case PamResourceType.SSH:
|
||||
return (
|
||||
<SshAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||
}
|
||||
@@ -90,9 +95,11 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
|
||||
|
||||
switch (account.resource.resourceType) {
|
||||
case PamResourceType.Postgres:
|
||||
return <PostgresAccountForm account={account} onSubmit={onSubmit} />;
|
||||
return <PostgresAccountForm account={account as any} onSubmit={onSubmit} />;
|
||||
case PamResourceType.MySQL:
|
||||
return <MySQLAccountForm account={account} onSubmit={onSubmit} />;
|
||||
return <MySQLAccountForm account={account as any} onSubmit={onSubmit} />;
|
||||
case PamResourceType.SSH:
|
||||
return <SshAccountForm account={account as any} onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, FormProvider, useForm, useFormContext, useWatch } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { PamResourceType, TSSHAccount } from "@app/hooks/api/pam";
|
||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||
import { SSHAuthMethod } from "@app/hooks/api/pam/types/ssh-resource";
|
||||
|
||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||
|
||||
type Props = {
|
||||
account?: TSSHAccount;
|
||||
resourceId?: string;
|
||||
resourceType?: PamResourceType;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const SSHPasswordCredentialsSchema = z.object({
|
||||
authMethod: z.literal(SSHAuthMethod.Password),
|
||||
username: z.string().trim().min(1, "Username is required"),
|
||||
password: z.string().trim().min(1, "Password is required")
|
||||
});
|
||||
|
||||
const SSHPublicKeyCredentialsSchema = z.object({
|
||||
authMethod: z.literal(SSHAuthMethod.PublicKey),
|
||||
username: z.string().trim().min(1, "Username is required"),
|
||||
privateKey: z.string().trim().min(1, "Private key is required")
|
||||
});
|
||||
|
||||
const SSHCertificateCredentialsSchema = z.object({
|
||||
authMethod: z.literal(SSHAuthMethod.Certificate),
|
||||
username: z.string().trim().min(1, "Username is required")
|
||||
});
|
||||
|
||||
const BaseSshAccountSchema = z.discriminatedUnion("authMethod", [
|
||||
SSHPasswordCredentialsSchema,
|
||||
SSHPublicKeyCredentialsSchema,
|
||||
SSHCertificateCredentialsSchema
|
||||
]);
|
||||
|
||||
const formSchema = genericAccountFieldsSchema.extend({
|
||||
credentials: BaseSshAccountSchema,
|
||||
// We don't support rotation for now, just feed a false value to
|
||||
// make the schema happy
|
||||
rotationEnabled: z.boolean().default(false)
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
const SshAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
const { control, setValue } = useFormContext();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const authMethod =
|
||||
useWatch({ control, name: "credentials.authMethod" }) || SSHAuthMethod.Password;
|
||||
const password = useWatch({ control, name: "credentials.password" });
|
||||
|
||||
useEffect(() => {
|
||||
if (password === UNCHANGED_PASSWORD_SENTINEL) {
|
||||
setShowPassword(false);
|
||||
}
|
||||
}, [password]);
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-sm border border-mineshaft-600 bg-mineshaft-700/70 p-3">
|
||||
<Controller
|
||||
name="credentials.authMethod"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-3"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
label="Authentication Method"
|
||||
>
|
||||
<Select
|
||||
value={value || SSHAuthMethod.Password}
|
||||
onValueChange={(newAuthMethod) => {
|
||||
onChange(newAuthMethod);
|
||||
// Clear out credentials from other auth methods
|
||||
setValue("credentials.password", undefined, { shouldDirty: true });
|
||||
setValue("credentials.privateKey", undefined, { shouldDirty: true });
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
<SelectItem value={SSHAuthMethod.Password}>Password</SelectItem>
|
||||
<SelectItem value={SSHAuthMethod.PublicKey}>SSH Key</SelectItem>
|
||||
<SelectItem value={SSHAuthMethod.Certificate}>Certificate</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="credentials.username"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-3"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Username"
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{authMethod === SSHAuthMethod.Password && (
|
||||
<Controller
|
||||
name="credentials.password"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Password"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
onFocus={() => {
|
||||
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
|
||||
field.onChange("");
|
||||
}
|
||||
setShowPassword(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (isUpdate && field.value === "") {
|
||||
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
|
||||
}
|
||||
setShowPassword(false);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{authMethod === SSHAuthMethod.PublicKey && (
|
||||
<Controller
|
||||
name="credentials.privateKey"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Private Key"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
value={field.value === UNCHANGED_PASSWORD_SENTINEL ? "" : field.value}
|
||||
className="min-h-32 resize-y font-mono text-xs"
|
||||
placeholder={
|
||||
isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL
|
||||
? "Private key unchanged - click to update"
|
||||
: "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
}
|
||||
onFocus={() => {
|
||||
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
|
||||
field.onChange("");
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (isUpdate && field.value === "") {
|
||||
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{authMethod === SSHAuthMethod.Certificate && (
|
||||
<p className="mb-0 text-xs text-mineshaft-400">
|
||||
Certificate-based authentication will use the certificate configured on the SSH resource.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SshAccountForm = ({ account, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(account);
|
||||
|
||||
const getDefaultCredentials = () => {
|
||||
if (!account) return undefined;
|
||||
|
||||
if (account.credentials.authMethod === SSHAuthMethod.Password) {
|
||||
return {
|
||||
...account.credentials,
|
||||
password: UNCHANGED_PASSWORD_SENTINEL
|
||||
};
|
||||
}
|
||||
|
||||
if (account.credentials.authMethod === SSHAuthMethod.PublicKey) {
|
||||
return {
|
||||
...account.credentials,
|
||||
privateKey: UNCHANGED_PASSWORD_SENTINEL
|
||||
};
|
||||
}
|
||||
|
||||
return account.credentials;
|
||||
};
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: account
|
||||
? {
|
||||
...account,
|
||||
credentials: getDefaultCredentials()
|
||||
}
|
||||
: {
|
||||
name: "",
|
||||
description: "",
|
||||
credentials: {
|
||||
authMethod: SSHAuthMethod.Password,
|
||||
username: "",
|
||||
password: ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<GenericAccountFields />
|
||||
<SshAccountFields isUpdate={isUpdate} />
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Account" : "Create Account"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { DiscriminativePick } from "@app/types";
|
||||
import { PamResourceHeader } from "../PamResourceHeader";
|
||||
import { MySQLResourceForm } from "./MySQLResourceForm";
|
||||
import { PostgresResourceForm } from "./PostgresResourceForm";
|
||||
import { SSHResourceForm } from "./SSHResourceForm";
|
||||
|
||||
type FormProps = {
|
||||
onComplete: (resource: TPamResource) => void;
|
||||
@@ -51,6 +52,8 @@ const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) =>
|
||||
return <PostgresResourceForm onSubmit={onSubmit} />;
|
||||
case PamResourceType.MySQL:
|
||||
return <MySQLResourceForm onSubmit={onSubmit} />;
|
||||
case PamResourceType.SSH:
|
||||
return <SSHResourceForm onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||
}
|
||||
@@ -79,6 +82,8 @@ const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => {
|
||||
return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />;
|
||||
case PamResourceType.MySQL:
|
||||
return <MySQLResourceForm resource={resource} onSubmit={onSubmit} />;
|
||||
case PamResourceType.SSH:
|
||||
return <SSHResourceForm resource={resource} onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${(resource as any).resourceType}`);
|
||||
}
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { PamResourceType, TSSHResource } from "@app/hooks/api/pam";
|
||||
|
||||
import { SshResourceFields } from "./shared/SshResourceFields";
|
||||
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
|
||||
|
||||
type Props = {
|
||||
resource?: TSSHResource;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const BaseSshConnectionDetailsSchema = z.object({
|
||||
host: z.string().trim().min(1, "Host is required"),
|
||||
port: z.number().int().min(1).max(65535)
|
||||
});
|
||||
|
||||
const formSchema = genericResourceFieldsSchema.extend({
|
||||
resourceType: z.literal(PamResourceType.SSH),
|
||||
connectionDetails: BaseSshConnectionDetailsSchema
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const SSHResourceForm = ({ resource, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(resource);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: resource ?? {
|
||||
resourceType: PamResourceType.SSH,
|
||||
connectionDetails: {
|
||||
host: "",
|
||||
port: 22
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<GenericResourceFields />
|
||||
<SshResourceFields />
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Details" : "Create Resource"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
|
||||
export const SshResourceFields = () => {
|
||||
const { control } = useFormContext();
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-sm border border-mineshaft-600 bg-mineshaft-700/70 p-3">
|
||||
<div className="mt-[0.675rem] flex items-start gap-2">
|
||||
<Controller
|
||||
name="connectionDetails.host"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="flex-1"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Host"
|
||||
>
|
||||
<Input placeholder="example.com or 192.168.1.1" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="connectionDetails.port"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-28"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Port"
|
||||
>
|
||||
<Input type="number" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -78,7 +78,6 @@ export const ResourceTypeSelect = ({ onSelect }: Props) => {
|
||||
// We temporarily show a special license modal for these because we will have to write some code to complete the integration
|
||||
if (
|
||||
resource === PamResourceType.RDP ||
|
||||
resource === PamResourceType.SSH ||
|
||||
resource === PamResourceType.Kubernetes ||
|
||||
resource === PamResourceType.MCP ||
|
||||
resource === PamResourceType.Redis ||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { faChevronRight, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Input } from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { TPamCommandLog } from "@app/hooks/api/pam";
|
||||
|
||||
import { formatLogContent } from "./PamSessionLogsSection.utils";
|
||||
|
||||
type Props = {
|
||||
logs: TPamCommandLog[];
|
||||
};
|
||||
|
||||
export const CommandLogView = ({ logs }: Props) => {
|
||||
const [expandedLogTimestamps, setExpandedLogTimestamps] = useState<Set<string>>(new Set());
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const toggleExpand = (timestamp: string) => {
|
||||
setExpandedLogTimestamps((prev) => {
|
||||
if (prev.has(timestamp)) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set([timestamp]);
|
||||
});
|
||||
};
|
||||
|
||||
const filteredLogs = useMemo(
|
||||
() =>
|
||||
logs.filter((log) => {
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
return (
|
||||
log.input.toLowerCase().includes(searchValue) ||
|
||||
log.output.toLowerCase().includes(searchValue)
|
||||
);
|
||||
}),
|
||||
[logs, search]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search logs..."
|
||||
className="flex-1 bg-mineshaft-800"
|
||||
containerClassName="bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex grow flex-col gap-2 overflow-y-auto text-xs">
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((log, index) => {
|
||||
const isExpanded = search.length || expandedLogTimestamps.has(log.timestamp);
|
||||
const formattedInput = formatLogContent(log.input);
|
||||
const logKey = `${log.timestamp}-${index}`;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={logKey}
|
||||
className={`flex w-full flex-col rounded-md border border-mineshaft-700 p-3 text-left focus:inset-ring-2 focus:inset-ring-mineshaft-400 focus:outline-hidden ${
|
||||
isExpanded ? "bg-mineshaft-700" : "bg-mineshaft-800 hover:bg-mineshaft-700"
|
||||
}`}
|
||||
onClick={() => toggleExpand(log.timestamp)}
|
||||
>
|
||||
<div className="flex items-center justify-between text-bunker-400">
|
||||
<div className="flex items-center gap-2 select-none">
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className={twMerge(
|
||||
"size-3 transition-transform duration-100 ease-in-out",
|
||||
isExpanded && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
<span>{new Date(log.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`mt-2 font-mono ${
|
||||
isExpanded ? "break-all whitespace-pre-wrap" : "truncate"
|
||||
}`}
|
||||
>
|
||||
<HighlightText text={formattedInput} highlight={search} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"grid transition-all duration-100 ease-in-out",
|
||||
isExpanded && log.output ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
{log.output && (
|
||||
<div className="pt-2 text-bunker-300">
|
||||
<HighlightText text={log.output} highlight={search} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex grow items-center justify-center text-bunker-300">
|
||||
{search.length ? (
|
||||
<div className="text-center">
|
||||
<div className="mb-2">No logs match search criteria</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<div className="mb-2">Session logs are not yet available</div>
|
||||
<div className="text-xs text-bunker-400">
|
||||
Logs will be uploaded after the session duration has elapsed.
|
||||
<br />
|
||||
If logs do not appear after some time, please contact your Gateway administrators.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +1,19 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { faChevronRight, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { PamResourceType, TPamCommandLog, TPamSession, TTerminalEvent } from "@app/hooks/api/pam";
|
||||
|
||||
import { Input } from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { TPamSession } from "@app/hooks/api/pam";
|
||||
|
||||
import { formatLogContent } from "./PamSessionLogsSection.utils";
|
||||
import { CommandLogView } from "./CommandLogView";
|
||||
import { TerminalEventView } from "./TerminalEventView";
|
||||
|
||||
type Props = {
|
||||
session: TPamSession;
|
||||
};
|
||||
|
||||
export const PamSessionLogsSection = ({ session }: Props) => {
|
||||
const [expandedLogTimestamps, setExpandedLogTimestamps] = useState<Set<string>>(new Set());
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const toggleExpand = (timestamp: string) => {
|
||||
setExpandedLogTimestamps((prev) => {
|
||||
if (prev.has(timestamp)) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set([timestamp]);
|
||||
});
|
||||
};
|
||||
|
||||
const filteredLogs = useMemo(
|
||||
() =>
|
||||
session.commandLogs.filter((log) => {
|
||||
const { input, output } = log;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
input.toLowerCase().includes(searchValue) || output.toLowerCase().includes(searchValue)
|
||||
);
|
||||
}),
|
||||
[session.commandLogs, search]
|
||||
);
|
||||
// Determine log type based on resource type
|
||||
const isSSHSession = session.resourceType === PamResourceType.SSH;
|
||||
const isDatabaseSession =
|
||||
session.resourceType === PamResourceType.Postgres ||
|
||||
session.resourceType === PamResourceType.MySQL;
|
||||
const hasLogs = session.logs.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
@@ -46,91 +21,20 @@ export const PamSessionLogsSection = ({ session }: Props) => {
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Session Logs</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
const newSearch = e.target.value;
|
||||
setSearch(newSearch);
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search logs..."
|
||||
className="flex-1 bg-mineshaft-800"
|
||||
containerClassName="bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex grow flex-col gap-2 overflow-y-auto text-xs">
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((log) => {
|
||||
const isExpanded = search.length || expandedLogTimestamps.has(log.timestamp);
|
||||
const formattedInput = formatLogContent(log.input);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={log.timestamp}
|
||||
className={`flex w-full flex-col rounded-md border border-mineshaft-700 p-3 text-left focus:inset-ring-2 focus:inset-ring-mineshaft-400 focus:outline-hidden ${
|
||||
isExpanded ? "bg-mineshaft-700" : "bg-mineshaft-800 hover:bg-mineshaft-700"
|
||||
}`}
|
||||
onClick={() => toggleExpand(log.timestamp)}
|
||||
>
|
||||
<div className="flex items-center justify-between text-bunker-400">
|
||||
<div className="flex items-center gap-2 select-none">
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className={twMerge(
|
||||
"size-3 transition-transform duration-100 ease-in-out",
|
||||
isExpanded && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
<span>{new Date(log.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`mt-2 font-mono ${
|
||||
isExpanded ? "break-all whitespace-pre-wrap" : "truncate"
|
||||
}`}
|
||||
>
|
||||
<HighlightText text={formattedInput} highlight={search} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"grid transition-all duration-100 ease-in-out",
|
||||
isExpanded && log.output ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
{log.output && (
|
||||
<div className="pt-2 text-bunker-300">
|
||||
<HighlightText text={log.output} highlight={search} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex grow items-center justify-center text-bunker-300">
|
||||
{search.length ? (
|
||||
<div className="text-center">
|
||||
<div className="mb-2">No logs match search criteria</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<div className="mb-2">Session logs are not yet available</div>
|
||||
<div className="text-xs text-bunker-400">
|
||||
Logs will be uploaded after the session duration has elapsed.
|
||||
<br />
|
||||
If logs do not appear after some time, please contact your Gateway administrators.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isDatabaseSession && hasLogs && <CommandLogView logs={session.logs as TPamCommandLog[]} />}
|
||||
{isSSHSession && hasLogs && <TerminalEventView events={session.logs as TTerminalEvent[]} />}
|
||||
{!hasLogs && (
|
||||
<div className="flex grow items-center justify-center text-bunker-300">
|
||||
<div className="text-center">
|
||||
<div className="mb-2">Session logs are not yet available</div>
|
||||
<div className="text-xs text-bunker-400">
|
||||
Logs will be uploaded after the session duration has elapsed.
|
||||
<br />
|
||||
If logs do not appear after some time, please contact your Gateway administrators.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Input } from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { TTerminalEvent } from "@app/hooks/api/pam";
|
||||
|
||||
import { aggregateTerminalEvents } from "./terminal-utils";
|
||||
|
||||
type Props = {
|
||||
events: TTerminalEvent[];
|
||||
};
|
||||
|
||||
export const TerminalEventView = ({ events }: Props) => {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const aggregatedEvents = useMemo(() => aggregateTerminalEvents(events), [events]);
|
||||
|
||||
const filteredEvents = useMemo(
|
||||
() =>
|
||||
aggregatedEvents.filter((event) => {
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
if (!searchValue) return true;
|
||||
return event.data.toLowerCase().includes(searchValue);
|
||||
}),
|
||||
[aggregatedEvents, search]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search terminal output..."
|
||||
className="flex-1 bg-mineshaft-800"
|
||||
containerClassName="bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex grow flex-col gap-2 overflow-y-auto text-xs">
|
||||
{filteredEvents.length > 0 ? (
|
||||
filteredEvents.map((event, index) => {
|
||||
const eventKey = `${event.timestamp}-${index}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={eventKey}
|
||||
className="flex w-full flex-col rounded-md border border-mineshaft-700 bg-mineshaft-800 p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between text-bunker-400">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span>{new Date(event.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 font-mono whitespace-pre-wrap text-bunker-100">
|
||||
<HighlightText text={event.data} highlight={search} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex grow items-center justify-center text-bunker-300">
|
||||
{search.length ? (
|
||||
<div className="text-center">
|
||||
<div className="mb-2">No terminal output matches search criteria</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<div className="mb-2">Terminal session logs are not yet available</div>
|
||||
<div className="text-xs text-bunker-400">
|
||||
Logs will be uploaded after the session duration has elapsed.
|
||||
<br />
|
||||
If logs do not appear after some time, please contact your Gateway administrators.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { TTerminalEvent } from "@app/hooks/api/pam";
|
||||
|
||||
// Strip ANSI escape codes from terminal output
|
||||
export const stripAnsiCodes = (text: string): string => {
|
||||
// Remove ANSI escape sequences
|
||||
// eslint-disable-next-line no-control-regex
|
||||
return text.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "").replace(/\x1b\][0-9];[^\x07]*\x07/g, "");
|
||||
};
|
||||
|
||||
export type AggregatedTerminalEvent = {
|
||||
timestamp: string;
|
||||
eventType: string;
|
||||
data: string;
|
||||
elapsedTime: number;
|
||||
eventCount: number;
|
||||
};
|
||||
|
||||
// Aggregate consecutive output events to avoid character-by-character display
|
||||
export const aggregateTerminalEvents = (events: TTerminalEvent[]): AggregatedTerminalEvent[] => {
|
||||
// Filter to only show output events (input is echoed, so redundant)
|
||||
const outputEvents = events.filter((e) => e.eventType === "output");
|
||||
|
||||
if (outputEvents.length === 0) return [];
|
||||
|
||||
// First, combine all events into one string to process
|
||||
const allText = outputEvents
|
||||
.map((e) => {
|
||||
try {
|
||||
return stripAnsiCodes(atob(e.data));
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
|
||||
// Split on lines that contain shell prompts
|
||||
// Pattern matches: user@hostname:path# or user@hostname:path$
|
||||
const promptPattern = /^[\w-]+@[\w-]+[^\s]*[:#$]\s+/;
|
||||
|
||||
const lines = allText.split("\n");
|
||||
const segments: string[] = [];
|
||||
let currentSegment: string[] = [];
|
||||
|
||||
lines.forEach((line) => {
|
||||
const hasPrompt = promptPattern.test(line);
|
||||
|
||||
if (hasPrompt && currentSegment.length > 0) {
|
||||
// Found a new prompt, save current segment and start new one
|
||||
segments.push(currentSegment.join("\n"));
|
||||
currentSegment = [line];
|
||||
} else {
|
||||
// Add line to current segment
|
||||
currentSegment.push(line);
|
||||
}
|
||||
});
|
||||
|
||||
// Add the last segment
|
||||
if (currentSegment.length > 0) {
|
||||
segments.push(currentSegment.join("\n"));
|
||||
}
|
||||
|
||||
// Filter out empty segments and convert to aggregated events
|
||||
const validSegments = segments.filter((seg) => seg.trim().length > 0);
|
||||
|
||||
return validSegments.map((segment) => ({
|
||||
timestamp: outputEvents[0].timestamp,
|
||||
eventType: "output",
|
||||
data: segment,
|
||||
elapsedTime: outputEvents[0].elapsedTime,
|
||||
eventCount: Math.ceil(outputEvents.length / validSegments.length)
|
||||
}));
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
faBoxOpen,
|
||||
faChevronDown,
|
||||
@@ -25,18 +25,19 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamSession } from "@app/hooks/api/pam";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TTerminalEvent, TPamSession } from "@app/hooks/api/pam";
|
||||
|
||||
import { formatLogContent } from "../../PamSessionsByIDPage/components/PamSessionLogsSection.utils";
|
||||
import { aggregateTerminalEvents } from "../../PamSessionsByIDPage/components/terminal-utils";
|
||||
import { PamSessionStatusBadge } from "./PamSessionStatusBadge";
|
||||
|
||||
type Props = {
|
||||
session: TPamSession;
|
||||
search: string;
|
||||
filteredCommandLogs: TPamSession["commandLogs"];
|
||||
filteredLogs: TPamSession["logs"];
|
||||
};
|
||||
|
||||
export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) => {
|
||||
export const PamSessionRow = ({ session, search, filteredLogs }: Props) => {
|
||||
const router = useRouter();
|
||||
const [showAllLogs, setShowAllLogs] = useState(false);
|
||||
|
||||
@@ -55,8 +56,24 @@ export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) =
|
||||
|
||||
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
|
||||
// Check if logs are terminal events and aggregate them
|
||||
const processedLogs = useMemo(() => {
|
||||
if (filteredLogs.length === 0) return [];
|
||||
|
||||
// Check if first log is a terminal event
|
||||
const isTerminalEvents = "data" in filteredLogs[0];
|
||||
|
||||
if (isTerminalEvents) {
|
||||
// Aggregate terminal events for better display
|
||||
return aggregateTerminalEvents(filteredLogs as TTerminalEvent[]);
|
||||
}
|
||||
|
||||
// Return command logs as-is
|
||||
return filteredLogs;
|
||||
}, [filteredLogs]);
|
||||
|
||||
const LOGS_TO_SHOW = 5;
|
||||
const logsToShow = showAllLogs ? filteredCommandLogs : filteredCommandLogs.slice(0, LOGS_TO_SHOW);
|
||||
const logsToShow = showAllLogs ? processedLogs : processedLogs.slice(0, LOGS_TO_SHOW);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -135,8 +152,8 @@ export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) =
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.PamResources}
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.PamSessions}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
@@ -157,32 +174,56 @@ export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) =
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
{filteredCommandLogs.length > 0 && (
|
||||
{filteredLogs.length > 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5} className="py-3 text-xs">
|
||||
{logsToShow.map((log) => {
|
||||
const formattedInput = formatLogContent(log.input);
|
||||
// Handle command logs (database sessions)
|
||||
if ("input" in log && "output" in log) {
|
||||
const formattedInput = formatLogContent(log.input);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${id}-log-${log.timestamp}`}
|
||||
className="mb-4 flex flex-col gap-1 last:mb-0"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-bunker-400">
|
||||
<FontAwesomeIcon icon={faTerminal} className="size-3" />
|
||||
<span>{new Date(log.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key={`${id}-log-${log.timestamp}`}
|
||||
className="mb-4 flex flex-col gap-1 last:mb-0"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-bunker-400">
|
||||
<FontAwesomeIcon icon={faTerminal} className="size-3" />
|
||||
<span>{new Date(log.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="font-mono break-all whitespace-pre-wrap">
|
||||
<HighlightText text={formattedInput} highlight={search} />
|
||||
<div className="font-mono break-all whitespace-pre-wrap">
|
||||
<HighlightText text={formattedInput} highlight={search} />
|
||||
</div>
|
||||
<div className="font-mono text-bunker-300">
|
||||
<HighlightText text={log.output.trim()} highlight={search} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-mono text-bunker-300">
|
||||
<HighlightText text={log.output.trim()} highlight={search} />
|
||||
);
|
||||
}
|
||||
|
||||
// Handle aggregated terminal events (SSH sessions)
|
||||
if ("data" in log && typeof log.data === "string") {
|
||||
return (
|
||||
<div
|
||||
key={`${id}-log-${log.timestamp}`}
|
||||
className="mb-4 flex flex-col gap-1 last:mb-0"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-bunker-400">
|
||||
<FontAwesomeIcon icon={faTerminal} className="size-3" />
|
||||
<span>{new Date(log.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="font-mono break-all whitespace-pre-wrap text-bunker-300">
|
||||
<HighlightText text={log.data.trim()} highlight={search} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
{filteredCommandLogs.length > LOGS_TO_SHOW && (
|
||||
{filteredLogs.length > LOGS_TO_SHOW && (
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
variant="link"
|
||||
@@ -193,7 +234,7 @@ export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) =
|
||||
>
|
||||
{showAllLogs
|
||||
? "Show less"
|
||||
: `Show ${filteredCommandLogs.length - LOGS_TO_SHOW} more log${filteredCommandLogs.length - LOGS_TO_SHOW === 1 ? "" : "s"}`}
|
||||
: `Show ${filteredLogs.length - LOGS_TO_SHOW} more log${filteredLogs.length - LOGS_TO_SHOW === 1 ? "" : "s"}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -113,7 +113,7 @@ export const PamSessionsTable = ({ sessions }: Props) => {
|
||||
id,
|
||||
resourceName,
|
||||
userId,
|
||||
commandLogs
|
||||
logs
|
||||
} = session;
|
||||
|
||||
const { name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
@@ -131,11 +131,25 @@ export const PamSessionsTable = ({ sessions }: Props) => {
|
||||
|
||||
const filteredLogs =
|
||||
searchValue.length >= 2
|
||||
? commandLogs.filter(
|
||||
(log) =>
|
||||
log.input.toLowerCase().includes(searchValue) ||
|
||||
log.output.toLowerCase().includes(searchValue)
|
||||
)
|
||||
? logs.filter((log) => {
|
||||
// Handle command logs (database sessions)
|
||||
if ("input" in log && "output" in log) {
|
||||
return (
|
||||
log.input.toLowerCase().includes(searchValue) ||
|
||||
log.output.toLowerCase().includes(searchValue)
|
||||
);
|
||||
}
|
||||
// Handle terminal events (SSH sessions)
|
||||
if ("data" in log) {
|
||||
try {
|
||||
const decodedData = atob(log.data);
|
||||
return decodedData.toLowerCase().includes(searchValue);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
@@ -385,7 +399,7 @@ export const PamSessionsTable = ({ sessions }: Props) => {
|
||||
key={session.id}
|
||||
session={session}
|
||||
search={search.trim().toLowerCase()}
|
||||
filteredCommandLogs={filteredLogs}
|
||||
filteredLogs={filteredLogs}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ export const SecretSyncsTab = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="text-lg font-medium text-mineshaft-100">Secret Syncs</p>
|
||||
|
||||
@@ -959,9 +959,9 @@ export const OverviewPage = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||
<div className="flex flex-row items-center justify-center space-x-2">
|
||||
<div className="flex flex-row flex-wrap items-center gap-2">
|
||||
{isTableFiltered && (
|
||||
<Button
|
||||
variant="plain"
|
||||
|
||||
+1
-1
@@ -294,7 +294,7 @@ export const AccessApprovalRequest = ({
|
||||
return (
|
||||
<>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Access Requests</p>
|
||||
|
||||
+1
-1
@@ -240,7 +240,7 @@ export const ApprovalPolicyList = ({ projectId }: IProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Policies</p>
|
||||
|
||||
+1
-1
@@ -690,7 +690,7 @@ export const ActionBar = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-4 flex items-center space-x-2">
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
<SecretSearchInput
|
||||
isSingleEnv
|
||||
className="w-2/5"
|
||||
|
||||
+2
-1
@@ -40,12 +40,13 @@ export const SecretSharingSection = () => {
|
||||
<p className="mb-3 text-xl font-medium">Allow Secret Sharing</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<div>
|
||||
<Checkbox
|
||||
id="secretSharing"
|
||||
isDisabled={!isAllowed || isLoading}
|
||||
isChecked={currentProject?.secretSharing ?? true}
|
||||
onCheckedChange={(state) => handleToggle(state as boolean)}
|
||||
allowMultilineLabel
|
||||
>
|
||||
This feature enables your project members to securely share secrets.
|
||||
</Checkbox>
|
||||
|
||||
+2
-1
@@ -40,12 +40,13 @@ export const SecretSnapshotsLegacySection = () => {
|
||||
<p className="mb-3 text-xl font-medium">Show Secret Snapshots ( legacy )</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<div>
|
||||
<Checkbox
|
||||
id="showSnapshotsLegacy"
|
||||
isDisabled={!isAllowed || isLoading}
|
||||
isChecked={currentProject?.showSnapshotsLegacy ?? false}
|
||||
onCheckedChange={(state) => handleToggle(state as boolean)}
|
||||
allowMultilineLabel
|
||||
>
|
||||
This feature enables your project members to view secret snapshots in the legacy
|
||||
format.
|
||||
|
||||
Reference in New Issue
Block a user