mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 23:16:55 +00:00
Merge remote-tracking branch 'origin/main' into pki-revamp-v3
This commit is contained in:
@@ -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."
|
||||
/>
|
||||
|
||||
+21
-4
@@ -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
-3
@@ -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
|
||||
|
||||
+21
-4
@@ -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">
|
||||
|
||||
+21
-4
@@ -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'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,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
-13
@@ -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
-13
@@ -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 />;
|
||||
};
|
||||
|
||||
+144
-5
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+200
@@ -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
-13
@@ -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
-13
@@ -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 />;
|
||||
};
|
||||
|
||||
+1
-1
@@ -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")}
|
||||
|
||||
+15
-19
@@ -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">
|
||||
|
||||
+16
-4
@@ -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"
|
||||
|
||||
+1
-1
@@ -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}>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+61
-35
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+197
@@ -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>
|
||||
);
|
||||
};
|
||||
+189
@@ -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,
|
||||
|
||||
+276
-289
@@ -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
-13
@@ -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 }
|
||||
);
|
||||
|
||||
+4
-13
@@ -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} />
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { subject } from "@casl/ability";
|
||||
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 { formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
useGetWorkspaceIdentityMembershipDetails
|
||||
} from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection";
|
||||
import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSection";
|
||||
@@ -113,11 +116,24 @@ const Page = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex max-w-8xl flex-col justify-between bg-bunker-800 text-white">
|
||||
{identityMembershipDetails ? (
|
||||
<>
|
||||
<Link
|
||||
to={`${getProjectBaseURL(currentProject.type)}/access-management`}
|
||||
params={{
|
||||
projectId
|
||||
}}
|
||||
search={{
|
||||
selectedTab: ProjectAccessControlTabs.Identities
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Identities
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={currentProject.type}
|
||||
title={identityMembershipDetails?.identity?.name}
|
||||
description={`Identity joined on ${identityMembershipDetails?.createdAt && formatRelative(new Date(identityMembershipDetails?.createdAt || ""), new Date())}`}
|
||||
>
|
||||
|
||||
@@ -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 { formatRelative } from "date-fns";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
useGetWorkspaceUserDetails
|
||||
} from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
||||
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
||||
@@ -115,11 +118,24 @@ export const Page = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex max-w-8xl flex-col justify-between bg-bunker-800 text-white">
|
||||
{membershipDetails ? (
|
||||
<>
|
||||
<Link
|
||||
to={`${getProjectBaseURL(currentProject.type)}/access-management`}
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
search={{
|
||||
selectedTab: ProjectAccessControlTabs.Member
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Users
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={currentProject.type}
|
||||
title={
|
||||
membershipDetails.user.firstName || membershipDetails.user.lastName
|
||||
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faCopy, faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faChevronLeft,
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faTrash
|
||||
} 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 { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -86,20 +92,29 @@ 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">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to={`${getProjectBaseURL(currentProject.type)}/access-management`}
|
||||
params={{
|
||||
projectId
|
||||
}}
|
||||
search={{
|
||||
selectedTab: ProjectAccessControlTabs.Roles
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Roles
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
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>
|
||||
scope={currentProject.type}
|
||||
title={data.name}
|
||||
description={
|
||||
<>
|
||||
{data.slug} {data.description && `- ${data.description}`}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{isCustomRole && (
|
||||
|
||||
@@ -6,12 +6,17 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
IconButton
|
||||
IconButton,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useOrgPermission } from "@app/context";
|
||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetVaultExternalMigrationConfigs } from "@app/hooks/api/migration";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import { PolicyTemplateModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal";
|
||||
import { VaultPolicyImportModal } from "@app/pages/project/RoleDetailsBySlugPage/components/VaultPolicyImportModal";
|
||||
|
||||
type Props = {
|
||||
isDisabled?: boolean;
|
||||
@@ -22,9 +27,16 @@ export const AddPoliciesButton = ({ isDisabled, projectType }: Props) => {
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addPolicy",
|
||||
"addPolicyOptions",
|
||||
"applyTemplate"
|
||||
"applyTemplate",
|
||||
"importFromVault"
|
||||
] as const);
|
||||
|
||||
const { hasOrgRole } = useOrgPermission();
|
||||
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
||||
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
||||
const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin);
|
||||
const isVaultImportDisabled = isDisabled || !isOrgAdmin;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
@@ -64,6 +76,35 @@ export const AddPoliciesButton = ({ isDisabled, projectType }: Props) => {
|
||||
>
|
||||
Add From Template
|
||||
</Button>
|
||||
{hasVaultConnection && (
|
||||
<Tooltip
|
||||
content={
|
||||
!isOrgAdmin
|
||||
? "Only organization admins can import policies from HashiCorp Vault"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Button
|
||||
leftIcon={
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("importFromVault");
|
||||
handlePopUpClose("addPolicyOptions");
|
||||
}}
|
||||
isDisabled={isVaultImportDisabled}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add from HashiCorp Vault
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -77,6 +118,10 @@ export const AddPoliciesButton = ({ isDisabled, projectType }: Props) => {
|
||||
isOpen={popUp.applyTemplate.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("applyTemplate", isOpen)}
|
||||
/>
|
||||
<VaultPolicyImportModal
|
||||
isOpen={popUp.importFromVault.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("importFromVault", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import {
|
||||
useGetVaultMounts,
|
||||
useGetVaultNamespaces,
|
||||
useGetVaultPolicies
|
||||
} from "@app/hooks/api/migration/queries";
|
||||
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
import { parseVaultPolicyToInfisical } from "./VaultPolicyImportModal.utils";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const Content = ({ onClose }: ContentProps) => {
|
||||
const rootForm = useFormContext<TFormSchema>();
|
||||
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
|
||||
const [selectedPolicy, setSelectedPolicy] = useState<string | null>(null);
|
||||
const [hclPolicy, setHclPolicy] = useState<string>("");
|
||||
const [shouldFetchPolicies, setShouldFetchPolicies] = useState(false);
|
||||
const [shouldFetchMounts, setShouldFetchMounts] = useState(false);
|
||||
|
||||
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
|
||||
const { data: policies, isLoading: isLoadingPolicies } = useGetVaultPolicies(
|
||||
shouldFetchPolicies,
|
||||
selectedNamespace ?? undefined
|
||||
);
|
||||
const { data: mounts, isLoading: isLoadingMounts } = useGetVaultMounts(
|
||||
shouldFetchMounts,
|
||||
selectedNamespace ?? undefined
|
||||
);
|
||||
|
||||
// Enable fetching policies and mounts when namespace is selected
|
||||
useEffect(() => {
|
||||
if (selectedNamespace) {
|
||||
setShouldFetchPolicies(true);
|
||||
setShouldFetchMounts(true);
|
||||
}
|
||||
}, [selectedNamespace]);
|
||||
|
||||
// Auto-populate HCL when a policy is selected
|
||||
useEffect(() => {
|
||||
if (selectedPolicy && policies) {
|
||||
const policy = policies.find((p) => p.name === selectedPolicy);
|
||||
if (policy) {
|
||||
setHclPolicy(policy.rules);
|
||||
}
|
||||
}
|
||||
}, [selectedPolicy, policies]);
|
||||
|
||||
const handleTranslateAndApply = () => {
|
||||
if (!hclPolicy.trim()) {
|
||||
createNotification({ type: "error", text: "Please provide a Vault HCL policy" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounts || mounts.length === 0) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "No Vault mounts found. Please ensure you have KV secret engines configured."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedPermissions = parseVaultPolicyToInfisical(hclPolicy, mounts);
|
||||
|
||||
if (!parsedPermissions || Object.keys(parsedPermissions).length === 0) {
|
||||
createNotification({
|
||||
type: "warning",
|
||||
text: "No translatable permissions found in the policy. Ensure the policy contains KV secret paths (e.g., secret/data/*, secret/metadata/*)."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the parsed permissions to the form
|
||||
(Object.keys(parsedPermissions) as ProjectPermissionSub[]).forEach((subjectKey) => {
|
||||
const value = parsedPermissions[subjectKey];
|
||||
if (!value) return;
|
||||
|
||||
const existingValue = rootForm.getValues(`permissions.${subjectKey}`) as unknown[];
|
||||
|
||||
if (Array.isArray(existingValue) && existingValue.length > 0) {
|
||||
// Merge with existing permissions
|
||||
rootForm.setValue(`permissions.${subjectKey}`, [...existingValue, ...value] as never, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true
|
||||
});
|
||||
} else {
|
||||
rootForm.setValue(`permissions.${subjectKey}`, value as never, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Vault policy translated and prefilled"
|
||||
});
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error("Translation error:", err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to translate policy. Please check the HCL format."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 rounded-md bg-primary/10 p-3 text-sm text-mineshaft-200">
|
||||
<div className="flex items-start gap-2">
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mt-0.5 text-primary" />
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<strong>How Policy Translation Works</strong>
|
||||
</div>
|
||||
<div className="space-y-1.5 text-xs leading-relaxed">
|
||||
<p>
|
||||
Policies are translated by identifying KV secret engine mounts and parsing path
|
||||
structures to extract environments and secret paths.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Key assumptions:</strong> The first path segment after the mount is treated
|
||||
as the environment (e.g., <code className="text-xs">secret/data/prod/app</code> →
|
||||
env: <code className="text-xs">prod</code>, path:{" "}
|
||||
<code className="text-xs">/app</code>). Vault capabilities and wildcards are
|
||||
automatically mapped to equivalent Infisical permissions and glob patterns.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormControl
|
||||
label="Namespace"
|
||||
className="mb-4"
|
||||
tooltipText="Required to fetch mount information. Policies will be translated using your Vault's KV secret engine mounts to extract environments and secret paths."
|
||||
>
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={namespaces?.find((ns) => ns.id === selectedNamespace)}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
const namespace = value as { id: string; name: string };
|
||||
setSelectedNamespace(namespace.name);
|
||||
setSelectedPolicy(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 policies and mount information
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="Select Vault Policy (Optional)" className="mb-4">
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={selectedPolicy ? policies?.find((p) => p.name === selectedPolicy) : null}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
const policy = value as { name: string; rules: string };
|
||||
setSelectedPolicy(policy.name);
|
||||
} else {
|
||||
setSelectedPolicy(null);
|
||||
}
|
||||
}}
|
||||
options={policies || []}
|
||||
getOptionValue={(option) => option.name}
|
||||
getOptionLabel={(option) => option.name}
|
||||
isDisabled={isLoadingPolicies}
|
||||
placeholder="Choose a policy to import..."
|
||||
isClearable
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Select a policy to auto-populate the HCL editor below, or skip to paste your own
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="Vault HCL Policy" className="mb-6">
|
||||
<>
|
||||
<TextArea
|
||||
value={hclPolicy}
|
||||
onChange={(e) => setHclPolicy(e.target.value)}
|
||||
placeholder={`path "secret/data/prod/app/*" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "secret/metadata/prod/*" {
|
||||
capabilities = ["list"]
|
||||
}`}
|
||||
rows={12}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Paste your HCL policy here or select one from the dropdown above. The translator will
|
||||
extract environments and paths automatically.
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button
|
||||
onClick={handleTranslateAndApply}
|
||||
isDisabled={!hclPolicy.trim() || isLoadingMounts || !mounts}
|
||||
>
|
||||
Translate & Apply
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const VaultPolicyImportModal = ({ isOpen, onOpenChange }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Import from HashiCorp Vault"
|
||||
subTitle="Select a policy from your Vault namespace or paste your own HCL policy to translate it into Infisical permissions."
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<Content onClose={() => onOpenChange(false)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+449
@@ -0,0 +1,449 @@
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type VaultMount = {
|
||||
path: string;
|
||||
type: string;
|
||||
version: string | null;
|
||||
};
|
||||
|
||||
type ArrayElement<T> = T extends (infer U)[] ? U : never;
|
||||
|
||||
export type SecretPermissionRule = ArrayElement<
|
||||
NonNullable<TFormSchema["permissions"]>[ProjectPermissionSub.Secrets]
|
||||
>;
|
||||
|
||||
export type FolderPermissionRule = ArrayElement<
|
||||
NonNullable<TFormSchema["permissions"]>[ProjectPermissionSub.SecretFolders]
|
||||
>;
|
||||
|
||||
type ParsedVaultPath = {
|
||||
environment: string | null;
|
||||
secretPath: string | null;
|
||||
mount: VaultMount | null;
|
||||
isWildcardMount: boolean;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Path Parsing
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parses a Vault policy path to extract mount, environment, and secret path.
|
||||
*
|
||||
* Handles three types of path patterns:
|
||||
* 1. Global wildcards: "*" or "+" → matches all mounts, environments, paths
|
||||
* 2. Wildcard mounts: "* /data/prod/*" → matches all mounts with specific path
|
||||
* 3. Regular paths: "secret/data/prod/api-keys" → specific mount and path
|
||||
*
|
||||
* For KV v2 mounts:
|
||||
* - data/ paths → secret operations (read, write values)
|
||||
* - metadata/ paths → folder operations (create, delete folders)
|
||||
*
|
||||
* Path structure after mount:
|
||||
* - KV v2: [data|metadata]/{environment}/{secretPath}
|
||||
* - KV v1: {environment}/{secretPath}
|
||||
*/
|
||||
export const parseVaultPath = (vaultPath: string, mounts: VaultMount[]): ParsedVaultPath => {
|
||||
// Case 1: Global wildcard (e.g., "*" or "+") - matches everything
|
||||
if (vaultPath === "*" || vaultPath === "+") {
|
||||
const syntheticMount: VaultMount = {
|
||||
path: "*",
|
||||
type: "kv",
|
||||
version: "1" // Default to v1 for global wildcards
|
||||
};
|
||||
return {
|
||||
environment: "*",
|
||||
secretPath: "/*",
|
||||
mount: syntheticMount,
|
||||
isWildcardMount: true
|
||||
};
|
||||
}
|
||||
|
||||
// Case 2: Wildcard mount (e.g., "*/data/*") - matches any mount with pattern
|
||||
const isWildcardMount = vaultPath.startsWith("*/") || vaultPath.startsWith("+/");
|
||||
|
||||
if (isWildcardMount) {
|
||||
let remainingPath = vaultPath.slice(2); // Remove "*/" or "+/"
|
||||
if (remainingPath.startsWith("/")) remainingPath = remainingPath.slice(1);
|
||||
|
||||
let environment: string | null = null;
|
||||
let secretPath: string | null = null;
|
||||
let isDataPath = false;
|
||||
let isMetadataPath = false;
|
||||
|
||||
// Check for KV v2 data/ or metadata/ prefix
|
||||
if (remainingPath.startsWith("data/")) {
|
||||
isDataPath = true;
|
||||
remainingPath = remainingPath.slice(5);
|
||||
} else if (remainingPath.startsWith("metadata/")) {
|
||||
isMetadataPath = true;
|
||||
remainingPath = remainingPath.slice(9);
|
||||
}
|
||||
|
||||
// Parse remaining segments
|
||||
const segments = remainingPath.split("/").filter(Boolean);
|
||||
|
||||
if (segments.length > 0) {
|
||||
if (segments.length === 1 && (segments[0] === "*" || segments[0] === "+")) {
|
||||
environment = "*";
|
||||
secretPath = "/*";
|
||||
} else {
|
||||
[environment] = segments;
|
||||
secretPath = segments.length > 1 ? `/${segments.slice(1).join("/")}` : "/";
|
||||
}
|
||||
}
|
||||
|
||||
// Create synthetic mount based on detected version
|
||||
const syntheticMount: VaultMount = {
|
||||
path: "*",
|
||||
type: "kv",
|
||||
version: isDataPath || isMetadataPath ? "2" : "1"
|
||||
};
|
||||
|
||||
return { environment, secretPath, mount: syntheticMount, isWildcardMount: true };
|
||||
}
|
||||
|
||||
// Case 3: Regular path (e.g., "secret/data/prod/api-keys")
|
||||
// Find matching mount (longest path first for most specific match)
|
||||
const sortedMounts = [...mounts].sort((a, b) => b.path.length - a.path.length);
|
||||
const mount = sortedMounts.find((m) => vaultPath.startsWith(m.path));
|
||||
|
||||
if (!mount) {
|
||||
return { environment: null, secretPath: null, mount: null, isWildcardMount: false };
|
||||
}
|
||||
|
||||
// Remove mount prefix
|
||||
let remainingPath = vaultPath.slice(mount.path.length);
|
||||
if (remainingPath.startsWith("/")) remainingPath = remainingPath.slice(1);
|
||||
|
||||
const isKvV2 = mount.version === "2" || mount.type === "kv";
|
||||
|
||||
// For KV v2, remove data/ or metadata/ prefix
|
||||
if (isKvV2) {
|
||||
if (remainingPath.startsWith("data/")) {
|
||||
remainingPath = remainingPath.slice(5);
|
||||
} else if (remainingPath.startsWith("metadata/")) {
|
||||
remainingPath = remainingPath.slice(9);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse environment and secret path
|
||||
const segments = remainingPath.split("/").filter(Boolean);
|
||||
let environment: string | null = null;
|
||||
let secretPath: string | null = null;
|
||||
|
||||
if (segments.length > 0) {
|
||||
if (segments.length === 1 && (segments[0] === "*" || segments[0] === "+")) {
|
||||
// Single wildcard segment
|
||||
environment = null;
|
||||
secretPath = "/*";
|
||||
} else {
|
||||
// First segment is the environment
|
||||
[environment] = segments;
|
||||
// Remaining segments form the secret path
|
||||
secretPath = segments.length > 1 ? `/${segments.slice(1).join("/")}` : "/";
|
||||
}
|
||||
}
|
||||
|
||||
return { environment, secretPath, mount, isWildcardMount: false };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Capability Mapping
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Maps Vault capabilities to Infisical secret actions.
|
||||
*
|
||||
* Mapping:
|
||||
* - create → Create
|
||||
* - list → DescribeSecret (view metadata without values)
|
||||
* - read → DescribeSecret + ReadValue (full access)
|
||||
* - update/patch → Edit
|
||||
* - delete → Delete
|
||||
*/
|
||||
const mapVaultCapabilitiesToSecretActions = (capabilities: string[]): Record<string, boolean> => {
|
||||
const actions: Record<string, boolean> = {};
|
||||
|
||||
if (capabilities.includes("create")) {
|
||||
actions[ProjectPermissionSecretActions.Create] = true;
|
||||
}
|
||||
if (capabilities.includes("list")) {
|
||||
actions[ProjectPermissionSecretActions.DescribeSecret] = true;
|
||||
}
|
||||
if (capabilities.includes("read")) {
|
||||
actions[ProjectPermissionSecretActions.DescribeSecret] = true;
|
||||
actions[ProjectPermissionSecretActions.ReadValue] = true;
|
||||
}
|
||||
if (capabilities.includes("update") || capabilities.includes("patch")) {
|
||||
actions[ProjectPermissionSecretActions.Edit] = true;
|
||||
}
|
||||
if (capabilities.includes("delete")) {
|
||||
actions[ProjectPermissionSecretActions.Delete] = true;
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps Vault capabilities to Infisical folder actions.
|
||||
*
|
||||
* Mapping:
|
||||
* - create → Create
|
||||
* - update/patch → Edit
|
||||
* - delete → Delete
|
||||
*
|
||||
* Note: 'list' is not mapped for folders as it's handled at the secret level
|
||||
*/
|
||||
const mapVaultCapabilitiesToFolderActions = (capabilities: string[]): Record<string, boolean> => {
|
||||
const actions: Record<string, boolean> = {};
|
||||
|
||||
if (capabilities.includes("create")) {
|
||||
actions[ProjectPermissionActions.Create] = true;
|
||||
}
|
||||
if (capabilities.includes("update") || capabilities.includes("patch")) {
|
||||
actions[ProjectPermissionActions.Edit] = true;
|
||||
}
|
||||
if (capabilities.includes("delete")) {
|
||||
actions[ProjectPermissionActions.Delete] = true;
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Condition Building
|
||||
// ============================================================================
|
||||
|
||||
type PermissionCondition = {
|
||||
lhs: string;
|
||||
operator: string;
|
||||
rhs: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts Vault wildcard patterns to Infisical glob patterns.
|
||||
* - Vault '+' → picomatch '*' (matches single segment)
|
||||
* - Vault '*' → picomatch '**' (matches any depth)
|
||||
*/
|
||||
const convertVaultWildcardToGlob = (vaultPattern: string): string => {
|
||||
// Use a placeholder to avoid replacing + twice
|
||||
// Step 1: Replace + with a placeholder
|
||||
let result = vaultPattern.replace(/\+/g, "__PLUS__");
|
||||
// Step 2: Replace * with **
|
||||
result = result.replace(/\*/g, "**");
|
||||
// Step 3: Replace placeholder with *
|
||||
result = result.replace(/__PLUS__/g, "*");
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds permission conditions for environment and secret path filtering.
|
||||
* Returns empty array if no restrictions are needed (matches everything).
|
||||
*/
|
||||
const buildConditions = (
|
||||
environment: string | null,
|
||||
secretPath: string | null
|
||||
): PermissionCondition[] => {
|
||||
const conditions: PermissionCondition[] = [];
|
||||
|
||||
// Add environment condition if present and not matching everything
|
||||
if (environment) {
|
||||
const globEnv = convertVaultWildcardToGlob(environment);
|
||||
// Skip if matches everything (Vault * becomes **)
|
||||
if (globEnv !== "**") {
|
||||
const hasWildcard = globEnv.includes("*");
|
||||
conditions.push({
|
||||
lhs: "environment",
|
||||
operator: hasWildcard
|
||||
? PermissionConditionOperators.$GLOB
|
||||
: PermissionConditionOperators.$EQ,
|
||||
rhs: globEnv
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add secret path condition if present and not matching everything
|
||||
if (secretPath && secretPath !== "/*") {
|
||||
const globPath = convertVaultWildcardToGlob(secretPath);
|
||||
// After conversion, /* becomes /** which matches everything
|
||||
if (globPath !== "/**") {
|
||||
const hasWildcard = globPath.includes("*");
|
||||
conditions.push({
|
||||
lhs: "secretPath",
|
||||
operator: hasWildcard
|
||||
? PermissionConditionOperators.$GLOB
|
||||
: PermissionConditionOperators.$EQ,
|
||||
rhs: globPath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return conditions;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Rule Deduplication
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Creates a unique key for deduplication of permission rules.
|
||||
* Combines all actions and conditions into a single string identifier.
|
||||
*/
|
||||
const createPermissionRuleKey = (rule: SecretPermissionRule | FolderPermissionRule): string => {
|
||||
const actions = Object.entries(rule)
|
||||
.filter(([key]) => key !== "conditions")
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join("|");
|
||||
|
||||
const conditions = (rule.conditions || [])
|
||||
.map((c) => `${c.lhs}${c.operator}${c.rhs}`)
|
||||
.sort()
|
||||
.join("|");
|
||||
|
||||
return `${actions}::${conditions}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a permission rule to the list if it's not a duplicate.
|
||||
*/
|
||||
const addPermissionRuleIfUnique = <T extends SecretPermissionRule | FolderPermissionRule>(
|
||||
rule: T,
|
||||
rulesList: T[],
|
||||
seenRules: Set<string>
|
||||
): void => {
|
||||
const ruleKey = createPermissionRuleKey(rule);
|
||||
if (!seenRules.has(ruleKey)) {
|
||||
seenRules.add(ruleKey);
|
||||
rulesList.push(rule);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Main Parser
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parses Vault HCL policy and converts it to Infisical permissions.
|
||||
*
|
||||
* Process:
|
||||
* 1. Clean HCL (remove comments, whitespace)
|
||||
* 2. Extract path blocks with regex
|
||||
* 3. For each path:
|
||||
* - Parse to extract mount, environment, and secret path
|
||||
* - Determine if it's a data path (secrets) or metadata path (folders)
|
||||
* - Map Vault capabilities to Infisical actions
|
||||
* - Build conditions for environment and path filtering
|
||||
* - Create permission rule and add if unique
|
||||
*
|
||||
* @param hclPolicy - Raw Vault HCL policy string
|
||||
* @param mounts - List of Vault mounts to match paths against
|
||||
* @returns Parsed permissions object ready for Infisical role creation
|
||||
*/
|
||||
export const parseVaultPolicyToInfisical = (
|
||||
hclPolicy: string,
|
||||
mounts: VaultMount[]
|
||||
): Partial<TFormSchema["permissions"]> => {
|
||||
const secretsPermissions: SecretPermissionRule[] = [];
|
||||
const foldersPermissions: FolderPermissionRule[] = [];
|
||||
|
||||
const seenSecretRules = new Set<string>();
|
||||
const seenFolderRules = new Set<string>();
|
||||
|
||||
try {
|
||||
// Step 1: Clean HCL policy - remove comments and extra whitespace
|
||||
const cleanedPolicy = hclPolicy
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/#.*$/, "").trim())
|
||||
.filter((line) => line.length > 0)
|
||||
.join(" ");
|
||||
|
||||
// Step 2: Extract path blocks using regex
|
||||
const pathRegex = /path\s+"([^"]+)"\s*\{[^}]*capabilities\s*=\s*\[([^\]]+)\][^}]*\}/gi;
|
||||
let match = pathRegex.exec(cleanedPolicy);
|
||||
|
||||
// Step 3: Process each path block
|
||||
while (match !== null) {
|
||||
const [, path, capabilitiesStr] = match;
|
||||
|
||||
// Parse capabilities list
|
||||
const capabilities = capabilitiesStr
|
||||
.split(",")
|
||||
.map((c) => c.trim().replace(/["'\s]/g, ""))
|
||||
.filter((c) => c.length > 0);
|
||||
|
||||
// Parse the Vault path
|
||||
const { environment, secretPath, mount } = parseVaultPath(path, mounts);
|
||||
|
||||
// Only process KV (Key-Value) secret engines
|
||||
if (mount && (mount.type === "kv" || mount.type === "generic")) {
|
||||
const isKvV2 = mount.version === "2";
|
||||
const isMetadata = isKvV2 && path.includes("/metadata/");
|
||||
|
||||
if (isMetadata) {
|
||||
// Metadata paths → Folder permissions only (KV v2 metadata endpoint)
|
||||
const actions = mapVaultCapabilitiesToFolderActions(capabilities);
|
||||
if (Object.keys(actions).length > 0) {
|
||||
const conditions = buildConditions(environment, secretPath);
|
||||
addPermissionRuleIfUnique(
|
||||
{ ...actions, conditions },
|
||||
foldersPermissions,
|
||||
seenFolderRules
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Data paths → Both secret AND folder permissions (KV v1 and v2 data paths)
|
||||
// Users need both to fully manage secrets and their containing folders
|
||||
const conditions = buildConditions(environment, secretPath);
|
||||
|
||||
// Create secret permissions
|
||||
const secretActions = mapVaultCapabilitiesToSecretActions(capabilities);
|
||||
if (Object.keys(secretActions).length > 0) {
|
||||
addPermissionRuleIfUnique(
|
||||
{ ...secretActions, conditions },
|
||||
secretsPermissions,
|
||||
seenSecretRules
|
||||
);
|
||||
}
|
||||
|
||||
// Create folder permissions for create/update/delete capabilities
|
||||
const folderActions = mapVaultCapabilitiesToFolderActions(capabilities);
|
||||
if (Object.keys(folderActions).length > 0) {
|
||||
addPermissionRuleIfUnique(
|
||||
{ ...folderActions, conditions },
|
||||
foldersPermissions,
|
||||
seenFolderRules
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match = pathRegex.exec(cleanedPolicy);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error parsing HCL policy:", err);
|
||||
}
|
||||
|
||||
// Build final permissions object
|
||||
const permissions: Partial<TFormSchema["permissions"]> = {};
|
||||
if (secretsPermissions.length > 0) {
|
||||
permissions[ProjectPermissionSub.Secrets] = secretsPermissions;
|
||||
}
|
||||
if (foldersPermissions.length > 0) {
|
||||
permissions[ProjectPermissionSub.SecretFolders] = foldersPermissions;
|
||||
}
|
||||
|
||||
return permissions;
|
||||
};
|
||||
+2
-1
@@ -35,7 +35,7 @@ export const DeleteProjectProtection = () => {
|
||||
<p className="mb-3 text-xl font-medium">Delete Protection</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<div>
|
||||
<Checkbox
|
||||
id="hasDeleteProtection"
|
||||
isDisabled={!isAllowed}
|
||||
@@ -43,6 +43,7 @@ export const DeleteProjectProtection = () => {
|
||||
onCheckedChange={(state) => {
|
||||
handleToggleDeleteProjectProtection(state as boolean);
|
||||
}}
|
||||
allowMultilineLabel
|
||||
>
|
||||
Protects the project from being deleted accidentally. While this option is enabled,
|
||||
you can't delete the project.
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ErrorPage = ({ error }: ErrorComponentProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-mineshaft-900">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="flex max-w-3xl flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-8 text-center text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faBugs} className="my-2 inline text-6xl" />
|
||||
<p>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const CommitDetailsPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pt-2 pb-4 text-white">
|
||||
<div className="mx-auto flex w-full max-w-8xl justify-center bg-bunker-800 pt-2 pb-4 text-white">
|
||||
<div className="w-full max-w-[75vw]">
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import {
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { CommitWithChanges } from "@app/hooks/api/folderCommits";
|
||||
import { useCommitRevert, useGetCommitDetails } from "@app/hooks/api/folderCommits/queries";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { CommitType } from "@app/hooks/api/types";
|
||||
|
||||
import { SecretVersionDiffView } from "../SecretVersionDiffView";
|
||||
@@ -260,7 +261,7 @@ export const CommitDetailsTab = ({
|
||||
Commit History
|
||||
</Button>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.SecretManager}
|
||||
title={`${parsedCommitDetails.changes?.message}` || "No message"}
|
||||
description={
|
||||
<>
|
||||
|
||||
+3
-2
@@ -24,6 +24,7 @@ import {
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCommitRollback, useGetRollbackPreview } from "@app/hooks/api/folderCommits/queries";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { SecretVersionDiffView } from "../SecretVersionDiffView";
|
||||
|
||||
@@ -297,7 +298,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pt-2 pb-4 text-white">
|
||||
<div className="mx-auto flex w-full max-w-8xl justify-center bg-bunker-800 pt-2 pb-4 text-white">
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionCommitsActions.PerformRollback}
|
||||
@@ -307,7 +308,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
|
||||
<div className="h-full w-full">
|
||||
<div>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.SecretManager}
|
||||
title={`Restore folder at commit ${selectedCommitId.substring(0, 8)}`}
|
||||
description={`Will return all changes in this folder to how they appeared at the point of commit ${selectedCommitId.substring(0, 8)}. Any modifications made after this commit will be undone.`}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
@@ -9,6 +11,7 @@ import {
|
||||
ProjectPermissionCommitsActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { CommitHistoryTab } from "./components/CommitHistoryTab";
|
||||
|
||||
@@ -46,10 +49,21 @@ export const CommitsPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white">
|
||||
<div className="w-full max-w-[75vw]">
|
||||
<div className="mx-auto mb-4 flex h-full w-full max-w-8xl justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full">
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/secrets/$envSlug"
|
||||
params={{
|
||||
projectId: currentProject.id,
|
||||
envSlug
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Secrets
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.SecretManager}
|
||||
title="Commits"
|
||||
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
|
||||
/>
|
||||
|
||||
@@ -15,7 +15,7 @@ export const IPAllowListPage = () => {
|
||||
<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 px-6">
|
||||
<div className="w-full max-w-8xl px-6">
|
||||
<div className="my-6">
|
||||
<p className="text-3xl font-medium text-gray-200">IP Allowlist</p>
|
||||
<div />
|
||||
|
||||
+3
-3
@@ -90,11 +90,11 @@ export const IntegrationDetailsByIDPage = () => {
|
||||
<meta property="og:title" content="Manage your .env files in seconds" />
|
||||
<meta name="og:description" content={t("integrations.description") as string} />
|
||||
</Helmet>
|
||||
<div className="mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex max-w-8xl flex-col justify-between bg-bunker-800 text-white">
|
||||
{integration ? (
|
||||
<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.SecretManager}
|
||||
title={`${integrationSlugNameMapping[integration.integration]} Integration`}
|
||||
>
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
|
||||
import {
|
||||
@@ -45,21 +46,25 @@ export const IntegrationsListPage = () => {
|
||||
<meta property="og:title" content="Manage your .env files in seconds" />
|
||||
<meta name="og:description" content={t("integrations.description") as string} />
|
||||
</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.SecretManager}
|
||||
title="Integrations"
|
||||
description="Manage integrations with third-party services."
|
||||
/>
|
||||
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
|
||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
||||
<TabList>
|
||||
<Tab value={IntegrationsListPageTabs.SecretSyncs}>Secret Syncs</Tab>
|
||||
<Tab value={IntegrationsListPageTabs.NativeIntegrations}>Native Integrations</Tab>
|
||||
<Tab value={IntegrationsListPageTabs.FrameworkIntegrations}>
|
||||
<Tab variant="project" value={IntegrationsListPageTabs.SecretSyncs}>
|
||||
Secret Syncs
|
||||
</Tab>
|
||||
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
|
||||
Native Integrations
|
||||
</Tab>
|
||||
<Tab variant="project" value={IntegrationsListPageTabs.FrameworkIntegrations}>
|
||||
Framework Integrations
|
||||
</Tab>
|
||||
<Tab value={IntegrationsListPageTabs.InfrastructureIntegrations}>
|
||||
<Tab variant="project" value={IntegrationsListPageTabs.InfrastructureIntegrations}>
|
||||
Infrastructure Integrations
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
@@ -86,13 +86,14 @@ import {
|
||||
useCreateSecretV3,
|
||||
useDeleteSecretV3,
|
||||
useGetImportedSecretsAllEnvs,
|
||||
useGetOrCreateFolder,
|
||||
useGetWsTags,
|
||||
useUpdateSecretV3
|
||||
} from "@app/hooks/api";
|
||||
import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard/queries";
|
||||
import { DashboardSecretsOrderBy, ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/projects/types";
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
|
||||
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
@@ -367,6 +368,7 @@ export const OverviewPage = () => {
|
||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
const { mutateAsync: getOrCreateFolder } = useGetOrCreateFolder();
|
||||
const { mutateAsync: updateFolderBatch } = useUpdateFolderBatch();
|
||||
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
@@ -384,16 +386,32 @@ export const OverviewPage = () => {
|
||||
] as const);
|
||||
|
||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||
const promises = userAvailableEnvs.map((env) => {
|
||||
const environment = env.slug;
|
||||
return createFolder({
|
||||
name: folderName,
|
||||
path: secretPath,
|
||||
environment,
|
||||
projectId,
|
||||
description
|
||||
const promises = userAvailableEnvs
|
||||
.map((env) => {
|
||||
const environment = env.slug;
|
||||
const isFolderPresent = isFolderPresentInEnv(folderName, environment);
|
||||
if (isFolderPresent) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return createFolder({
|
||||
name: folderName,
|
||||
path: secretPath,
|
||||
environment,
|
||||
projectId,
|
||||
description
|
||||
});
|
||||
})
|
||||
.filter((promise) => promise !== undefined);
|
||||
|
||||
if (promises.length === 0) {
|
||||
handlePopUpClose("addFolder");
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Folder already exists in all environments"
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const isFoldersAdded = results.some((result) => result.status === "fulfilled");
|
||||
@@ -481,7 +499,7 @@ export const OverviewPage = () => {
|
||||
})
|
||||
);
|
||||
if (folderName && parentPath && canCreateFolder) {
|
||||
await createFolder({
|
||||
await getOrCreateFolder({
|
||||
projectId,
|
||||
path: parentPath,
|
||||
environment: env,
|
||||
@@ -645,7 +663,7 @@ export const OverviewPage = () => {
|
||||
})
|
||||
);
|
||||
if (folderName && parentPath && canCreateFolder) {
|
||||
await createFolder({
|
||||
await getOrCreateFolder({
|
||||
projectId,
|
||||
environment: slug,
|
||||
path: parentPath,
|
||||
@@ -911,11 +929,11 @@ export const OverviewPage = () => {
|
||||
<meta property="og:title" content={String(t("dashboard.og-title"))} />
|
||||
<meta name="og:description" content={String(t("dashboard.og-description"))} />
|
||||
</Helmet>
|
||||
<div className="relative mx-auto max-w-7xl text-mineshaft-50 dark:scheme-dark">
|
||||
<div className="relative mx-auto max-w-8xl text-mineshaft-50 dark:scheme-dark">
|
||||
<div className="flex w-full items-baseline justify-between">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Secrets Overview"
|
||||
scope={ProjectType.SecretManager}
|
||||
title="Overview"
|
||||
description={
|
||||
<p className="text-md text-bunker-300">
|
||||
Inject your secrets using
|
||||
@@ -959,7 +977,7 @@ export const OverviewPage = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="flex items-center justify-between">
|
||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||
<div className="flex flex-row items-center justify-center space-x-2">
|
||||
{isTableFiltered && (
|
||||
|
||||
+8
-3
@@ -25,7 +25,12 @@ import {
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
|
||||
import {
|
||||
useCreateSecretV3,
|
||||
useCreateWsTag,
|
||||
useGetOrCreateFolder,
|
||||
useGetWsTags
|
||||
} from "@app/hooks/api";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
|
||||
const typeSchema = z
|
||||
@@ -64,7 +69,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
|
||||
const environments = currentProject?.environments || [];
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
const { mutateAsync: getOrCreateFolder } = useGetOrCreateFolder();
|
||||
const { data: projectTags, isPending: isTagsLoading } = useGetWsTags(
|
||||
canReadTags ? projectId : ""
|
||||
);
|
||||
@@ -92,7 +97,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
|
||||
);
|
||||
|
||||
if (folderName && parentPath && canCreateFolder) {
|
||||
await createFolder({
|
||||
await getOrCreateFolder({
|
||||
projectId,
|
||||
path: parentPath,
|
||||
environment,
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export const QuickSearchDynamicSecretItem = ({
|
||||
</span>
|
||||
<span className="text-xs text-mineshaft-400">
|
||||
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "}
|
||||
<Tooltip className="max-w-7xl" content={groupDynamicSecret.path}>
|
||||
<Tooltip className="max-w-8xl" content={groupDynamicSecret.path}>
|
||||
<span>{reverseTruncate(groupDynamicSecret.path)}</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export const QuickSearchFolderItem = ({ folderGroup, onClose }: Props) => {
|
||||
>
|
||||
<Td className="w-full whitespace-nowrap">
|
||||
<FontAwesomeIcon className="text-yellow-700" icon={faFolder} />
|
||||
<Tooltip content={groupFolder.path} className="max-w-7xl">
|
||||
<Tooltip content={groupFolder.path} className="max-w-8xl">
|
||||
<div className="ml-2 inline-block">{reverseTruncate(groupFolder.path)}</div>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ export const QuickSearchSecretItem = ({
|
||||
</span>
|
||||
<span className="text-xs text-mineshaft-400">
|
||||
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "}
|
||||
<Tooltip className="max-w-7xl" content={groupSecret.path}>
|
||||
<Tooltip className="max-w-8xl" content={groupSecret.path}>
|
||||
<span>{reverseTruncate(groupSecret.path ?? "")}</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export const QuickSearchSecretRotationItem = ({ secretRotationGroup, onClose }:
|
||||
</span>
|
||||
<span className="text-xs text-mineshaft-400">
|
||||
<FontAwesomeIcon size="xs" className="mr-0.5 text-yellow-700" icon={faFolder} />{" "}
|
||||
<Tooltip className="max-w-7xl" content={groupSecretRotation.folder.path}>
|
||||
<Tooltip className="max-w-8xl" content={groupSecretRotation.folder.path}>
|
||||
<span>{reverseTruncate(groupSecretRotation.folder.path)}</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export const SecretTableResourceCount = ({
|
||||
secretRotationCount = 0
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2 divide-x divide-mineshaft-500 text-sm text-mineshaft-400">
|
||||
<div className="flex items-center divide-x divide-mineshaft-500 text-sm text-mineshaft-400 [&>*]:pr-2">
|
||||
{importCount > 0 && (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const getExpandedRowStyle = (scrollOffset: number) => ({
|
||||
marginLeft: scrollOffset,
|
||||
width: "calc(100vw - 275px)", // accounts for sidebar and margin
|
||||
maxWidth: "1270px" // largest width of table on ultra-wide
|
||||
width: "calc(100vw - 110px)", // accounts for sidebar and margin
|
||||
maxWidth: "1404px" // largest width of table on ultra-wide
|
||||
});
|
||||
|
||||
type GetHeaderStyleParams = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
import { useProject } from "@app/context";
|
||||
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { AccessApprovalRequest } from "./components/AccessApprovalRequest";
|
||||
import { ApprovalPolicyList } from "./components/ApprovalPolicyList";
|
||||
@@ -38,27 +39,29 @@ export const SecretApprovalsPage = () => {
|
||||
<meta property="og:title" content={String(t("approval.og-title"))} />
|
||||
<meta name="og:description" content={String(t("approval.og-description"))} />
|
||||
</Helmet>
|
||||
<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="project"
|
||||
scope={ProjectType.SecretManager}
|
||||
title="Approval Workflows"
|
||||
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
|
||||
/>
|
||||
<Tabs defaultValue={defaultTab}>
|
||||
<Tabs orientation="vertical" defaultValue={defaultTab}>
|
||||
<TabList>
|
||||
<Tab value={TabSection.SecretApprovalRequests}>
|
||||
<Tab variant="project" value={TabSection.SecretApprovalRequests}>
|
||||
Change Requests
|
||||
{Boolean(secretApprovalReqCount?.open) && (
|
||||
<Badge className="ml-2">{secretApprovalReqCount?.open}</Badge>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab value={TabSection.ResourceApprovalRequests}>
|
||||
<Tab variant="project" value={TabSection.ResourceApprovalRequests}>
|
||||
Access Requests
|
||||
{Boolean(accessApprovalRequestCount?.pendingCount) && (
|
||||
<Badge className="ml-2">{accessApprovalRequestCount?.pendingCount}</Badge>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab value={TabSection.Policies}>Policies</Tab>
|
||||
<Tab variant="project" value={TabSection.Policies}>
|
||||
Policies
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSection.SecretApprovalRequests}>
|
||||
<SecretApprovalRequest />
|
||||
|
||||
+269
-284
@@ -19,7 +19,6 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
@@ -298,108 +297,132 @@ export const AccessApprovalRequest = ({
|
||||
const isFiltered = Boolean(search || envFilter || requestedByFilter);
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="approval-changes-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="rounded-md text-gray-300"
|
||||
>
|
||||
<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>
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Access Requests</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/access-controls/access-requests"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.32rem] 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>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Request and review access to secrets in sensitive environments and folders
|
||||
</p>
|
||||
</div>
|
||||
<Tooltip
|
||||
content="To submit Access Requests, your project needs to create Access Request policies first."
|
||||
isDisabled={policiesLoading || !!policies?.length}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (subscription && !subscription?.secretApproval) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("requestAccess");
|
||||
}}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={policiesLoading || !policies?.length}
|
||||
<>
|
||||
<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>
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Access Requests</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/access-controls/access-requests"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Request Access
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<div className="mt-[0.32rem] 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>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Request and review access to secrets in sensitive environments and folders
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search approval requests by requesting user or environment..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("open")}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("open");
|
||||
<Tooltip
|
||||
content="To submit Access Requests, your project needs to create Access Request policies first."
|
||||
isDisabled={policiesLoading || !!policies?.length}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (subscription && !subscription?.secretApproval) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("requestAccess");
|
||||
}}
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={policiesLoading || !policies?.length}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
||||
{!!requestCount && requestCount?.pendingCount} Pending
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("close")}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("close");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||
{!!requestCount && requestCount.finalizedCount} Closed
|
||||
</div>
|
||||
<div className="flex grow justify-end space-x-8">
|
||||
Request Access
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search approval requests by requesting user or environment..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("open")}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("open");
|
||||
}}
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
||||
{!!requestCount && requestCount?.pendingCount} Pending
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("close")}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("close");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||
{!!requestCount && requestCount.finalizedCount} Closed
|
||||
</div>
|
||||
<div className="flex grow justify-end space-x-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
||||
>
|
||||
Environments
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="max-h-80 thin-scrollbar overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select an Environment
|
||||
</DropdownMenuLabel>
|
||||
{currentProject?.environments.map(({ slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||
key={`request-filter-${slug}`}
|
||||
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
{name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{!!permission.can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||
className={requestedByFilter ? "text-white" : "text-bunker-300"}
|
||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
||||
>
|
||||
Environments
|
||||
Requested By
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -408,210 +431,172 @@ export const AccessApprovalRequest = ({
|
||||
className="max-h-80 thin-scrollbar overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select an Environment
|
||||
Select Requesting User
|
||||
</DropdownMenuLabel>
|
||||
{currentProject?.environments.map(({ slug, name }) => (
|
||||
{members?.map(({ user: membershipUser, id }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||
key={`request-filter-${slug}`}
|
||||
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
onClick={() =>
|
||||
setRequestedByFilter((state) =>
|
||||
state === membershipUser.id ? undefined : membershipUser.id
|
||||
)
|
||||
}
|
||||
key={`request-filter-member-${id}`}
|
||||
icon={
|
||||
requestedByFilter === membershipUser.id && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
{name}
|
||||
{membershipUser.username}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{!!permission.can(
|
||||
ProjectPermissionMemberActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className={requestedByFilter ? "text-white" : "text-bunker-300"}
|
||||
rightIcon={
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||
}
|
||||
>
|
||||
Requested By
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="max-h-80 thin-scrollbar overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select Requesting User
|
||||
</DropdownMenuLabel>
|
||||
{members?.map(({ user: membershipUser, id }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setRequestedByFilter((state) =>
|
||||
state === membershipUser.id ? undefined : membershipUser.id
|
||||
)
|
||||
}
|
||||
key={`request-filter-member-${id}`}
|
||||
icon={
|
||||
requestedByFilter === membershipUser.id && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
{membershipUser.username}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
|
||||
{filteredRequests?.length === 0 && !isFiltered && (
|
||||
<div className="py-12">
|
||||
<EmptyState
|
||||
title={`No ${statusFilter === "open" ? "Pending" : "Closed"} Access Requests`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(!filteredRequests?.length && isFiltered && !areRequestsPending) && (
|
||||
<div className="py-12">
|
||||
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||
</div>
|
||||
)}
|
||||
{!!filteredRequests?.length &&
|
||||
filteredRequests?.slice(offset, perPage * page).map((request) => {
|
||||
const details = generateRequestDetails(request);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={request.id}
|
||||
className="flex w-full cursor-pointer border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleSelectRequest(request)}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
handleSelectRequest(request);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full flex-col justify-between">
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faLock}
|
||||
size="xs"
|
||||
className="mr-1.5 text-mineshaft-300"
|
||||
/>
|
||||
{generateRequestText(request)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs leading-3 text-gray-500">
|
||||
{membersGroupById?.[request.requestedByUserId]?.user && (
|
||||
<>
|
||||
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
|
||||
ago by{" "}
|
||||
{membersGroupById?.[request.requestedByUserId]?.user?.firstName}{" "}
|
||||
{membersGroupById?.[request.requestedByUserId]?.user?.lastName} (
|
||||
{membersGroupById?.[request.requestedByUserId]?.user?.email}){" "}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{request.requestedByUserId === user.id && (
|
||||
<div className="flex items-center gap-1.5 text-xs whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faUser} size="sm" />
|
||||
<span>Requested By You</span>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip content={details.displayData.tooltipContent}>
|
||||
<div>
|
||||
<Badge
|
||||
className="flex items-center gap-1.5 whitespace-nowrap"
|
||||
variant={details.displayData.type}
|
||||
>
|
||||
{details.displayData.icon && (
|
||||
<FontAwesomeIcon icon={details.displayData.icon} />
|
||||
)}
|
||||
<span>{details.displayData.label}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Boolean(filteredRequests.length) && (
|
||||
<Pagination
|
||||
className="border-none"
|
||||
count={filteredRequests.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!!policies && (
|
||||
<RequestAccessModal
|
||||
policies={policies}
|
||||
isOpen={popUp.requestAccess.isOpen}
|
||||
onOpenChange={() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(
|
||||
projectSlug,
|
||||
envFilter,
|
||||
requestedByFilter
|
||||
)
|
||||
});
|
||||
handlePopUpClose("requestAccess");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
|
||||
{filteredRequests?.length === 0 && !isFiltered && (
|
||||
<div className="py-12">
|
||||
<EmptyState
|
||||
title={`No ${statusFilter === "open" ? "Pending" : "Closed"} Access Requests`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(!filteredRequests?.length && isFiltered && !areRequestsPending) && (
|
||||
<div className="py-12">
|
||||
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||
</div>
|
||||
)}
|
||||
{!!filteredRequests?.length &&
|
||||
filteredRequests?.slice(offset, perPage * page).map((request) => {
|
||||
const details = generateRequestDetails(request);
|
||||
|
||||
{!!selectedRequest && (
|
||||
<ReviewAccessRequestModal
|
||||
selectedEnvSlug={envFilter}
|
||||
policies={policies || []}
|
||||
selectedRequester={requestedByFilter}
|
||||
projectSlug={projectSlug}
|
||||
request={selectedRequest}
|
||||
members={members || []}
|
||||
isOpen={popUp.reviewRequest.isOpen}
|
||||
onOpenChange={() => {
|
||||
handlePopUpClose("reviewRequest");
|
||||
setSelectedRequest(null);
|
||||
refetchRequests();
|
||||
}}
|
||||
onUpdate={(request) => {
|
||||
// scott: this isn't ideal but our current use of state makes this complicated...
|
||||
// we shouldn't be using state like this...
|
||||
handleSelectRequest({
|
||||
...selectedRequest,
|
||||
isTemporary: request.isTemporary,
|
||||
temporaryRange: request.temporaryRange,
|
||||
reviewers: []
|
||||
});
|
||||
}}
|
||||
canBypass={generateRequestDetails(selectedRequest).canBypass}
|
||||
/>
|
||||
)}
|
||||
|
||||
<UpgradePlanModal
|
||||
text="You need to upgrade your plan to access this feature"
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
||||
return (
|
||||
<div
|
||||
key={request.id}
|
||||
className="flex w-full cursor-pointer border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleSelectRequest(request)}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
handleSelectRequest(request);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full flex-col justify-between">
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faLock}
|
||||
size="xs"
|
||||
className="mr-1.5 text-mineshaft-300"
|
||||
/>
|
||||
{generateRequestText(request)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs leading-3 text-gray-500">
|
||||
{membersGroupById?.[request.requestedByUserId]?.user && (
|
||||
<>
|
||||
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
|
||||
ago by{" "}
|
||||
{membersGroupById?.[request.requestedByUserId]?.user?.firstName}{" "}
|
||||
{membersGroupById?.[request.requestedByUserId]?.user?.lastName} (
|
||||
{membersGroupById?.[request.requestedByUserId]?.user?.email}){" "}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{request.requestedByUserId === user.id && (
|
||||
<div className="flex items-center gap-1.5 text-xs whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faUser} size="sm" />
|
||||
<span>Requested By You</span>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip content={details.displayData.tooltipContent}>
|
||||
<div>
|
||||
<Badge
|
||||
className="flex items-center gap-1.5 whitespace-nowrap"
|
||||
variant={details.displayData.type}
|
||||
>
|
||||
{details.displayData.icon && (
|
||||
<FontAwesomeIcon icon={details.displayData.icon} />
|
||||
)}
|
||||
<span>{details.displayData.label}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Boolean(filteredRequests.length) && (
|
||||
<Pagination
|
||||
className="border-none"
|
||||
count={filteredRequests.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!!policies && (
|
||||
<RequestAccessModal
|
||||
policies={policies}
|
||||
isOpen={popUp.requestAccess.isOpen}
|
||||
onOpenChange={() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(
|
||||
projectSlug,
|
||||
envFilter,
|
||||
requestedByFilter
|
||||
)
|
||||
});
|
||||
handlePopUpClose("requestAccess");
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
|
||||
{!!selectedRequest && (
|
||||
<ReviewAccessRequestModal
|
||||
selectedEnvSlug={envFilter}
|
||||
policies={policies || []}
|
||||
selectedRequester={requestedByFilter}
|
||||
projectSlug={projectSlug}
|
||||
request={selectedRequest}
|
||||
members={members || []}
|
||||
isOpen={popUp.reviewRequest.isOpen}
|
||||
onOpenChange={() => {
|
||||
handlePopUpClose("reviewRequest");
|
||||
setSelectedRequest(null);
|
||||
refetchRequests();
|
||||
}}
|
||||
onUpdate={(request) => {
|
||||
// scott: this isn't ideal but our current use of state makes this complicated...
|
||||
// we shouldn't be using state like this...
|
||||
handleSelectRequest({
|
||||
...selectedRequest,
|
||||
isTemporary: request.isTemporary,
|
||||
temporaryRange: request.temporaryRange,
|
||||
reviewers: []
|
||||
});
|
||||
}}
|
||||
canBypass={generateRequestDetails(selectedRequest).canBypass}
|
||||
/>
|
||||
)}
|
||||
|
||||
<UpgradePlanModal
|
||||
text="You need to upgrade your plan to access this feature"
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+244
-254
@@ -12,7 +12,6 @@ import {
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
@@ -240,262 +239,253 @@ export const ApprovalPolicyList = ({ projectId }: IProps) => {
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="approval-changes-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="rounded-md text-gray-300"
|
||||
>
|
||||
<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>
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Policies</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.32rem] 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>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Implement granular policies for access requests and secrets management
|
||||
</p>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (subscription && !subscription?.secretApproval) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("policyForm");
|
||||
}}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create Policy
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search policies by name, type, environment or secret path..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter findings"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="max-h-[70vh] thin-scrollbar overflow-y-auto"
|
||||
align="end"
|
||||
<>
|
||||
<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>
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Policies</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<DropdownMenuLabel>Policy Type</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
type: null
|
||||
}))
|
||||
}
|
||||
icon={!filters && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
All
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
type: PolicyType.AccessPolicy
|
||||
}))
|
||||
}
|
||||
icon={
|
||||
filters.type === PolicyType.AccessPolicy && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Access Policy
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
type: PolicyType.ChangePolicy
|
||||
}))
|
||||
}
|
||||
icon={
|
||||
filters.type === PolicyType.ChangePolicy && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Change Policy
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel>Environment</DropdownMenuLabel>
|
||||
{currentProject.environments.map((env) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
environmentIds: prev.environmentIds.includes(env.id)
|
||||
? prev.environmentIds.filter((i) => i !== env.id)
|
||||
: [...prev.environmentIds, env.id]
|
||||
}));
|
||||
}}
|
||||
key={env.id}
|
||||
icon={
|
||||
filters.environmentIds.includes(env.id) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<span className="capitalize">{env.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Environment
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.Environment)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.Environment)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Environment)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Secret Path
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.SecretPath)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.SecretPath)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.SecretPath)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Type
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.Type)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.Type)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Type)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPoliciesLoading && (
|
||||
<TableSkeleton
|
||||
columns={5}
|
||||
innerKey="secret-policies"
|
||||
className="bg-mineshaft-700"
|
||||
<div className="mt-[0.32rem] 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]"
|
||||
/>
|
||||
)}
|
||||
{!isPoliciesLoading && !policies?.length && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No Policies Found" icon={faFileShield} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!!currentProject &&
|
||||
filteredPolicies
|
||||
?.slice(offset, perPage * page)
|
||||
.map((policy) => (
|
||||
<ApprovalPolicyRow
|
||||
policy={policy}
|
||||
key={policy.id}
|
||||
members={members}
|
||||
groups={groups}
|
||||
onEdit={() => handlePopUpOpen("policyForm", policy)}
|
||||
onDelete={() => handlePopUpOpen("deletePolicy", policy)}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(!filteredPolicies.length && policies.length && !isPoliciesLoading) && (
|
||||
<EmptyState title="No Policies Match Search" icon={faSearch} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Implement granular policies for access requests and secrets management
|
||||
</p>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (subscription && !subscription?.secretApproval) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("policyForm");
|
||||
}}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create Policy
|
||||
</Button>
|
||||
)}
|
||||
{Boolean(filteredPolicies.length) && (
|
||||
<Pagination
|
||||
count={filteredPolicies.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</motion.div>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search policies by name, type, environment or secret path..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter findings"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="max-h-[70vh] thin-scrollbar overflow-y-auto"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuLabel>Policy Type</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
type: null
|
||||
}))
|
||||
}
|
||||
icon={!filters && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
All
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
type: PolicyType.AccessPolicy
|
||||
}))
|
||||
}
|
||||
icon={
|
||||
filters.type === PolicyType.AccessPolicy && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Access Policy
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
type: PolicyType.ChangePolicy
|
||||
}))
|
||||
}
|
||||
icon={
|
||||
filters.type === PolicyType.ChangePolicy && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Change Policy
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel>Environment</DropdownMenuLabel>
|
||||
{currentProject.environments.map((env) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
environmentIds: prev.environmentIds.includes(env.id)
|
||||
? prev.environmentIds.filter((i) => i !== env.id)
|
||||
: [...prev.environmentIds, env.id]
|
||||
}));
|
||||
}}
|
||||
key={env.id}
|
||||
icon={
|
||||
filters.environmentIds.includes(env.id) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<span className="capitalize">{env.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Environment
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.Environment)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.Environment)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Environment)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Secret Path
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.SecretPath)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.SecretPath)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.SecretPath)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Type
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PolicyOrderBy.Type)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PolicyOrderBy.Type)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Type)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPoliciesLoading && (
|
||||
<TableSkeleton
|
||||
columns={5}
|
||||
innerKey="secret-policies"
|
||||
className="bg-mineshaft-700"
|
||||
/>
|
||||
)}
|
||||
{!isPoliciesLoading && !policies?.length && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No Policies Found" icon={faFileShield} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!!currentProject &&
|
||||
filteredPolicies
|
||||
?.slice(offset, perPage * page)
|
||||
.map((policy) => (
|
||||
<ApprovalPolicyRow
|
||||
policy={policy}
|
||||
key={policy.id}
|
||||
members={members}
|
||||
groups={groups}
|
||||
onEdit={() => handlePopUpOpen("policyForm", policy)}
|
||||
onDelete={() => handlePopUpOpen("deletePolicy", policy)}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(!filteredPolicies.length && policies.length && !isPoliciesLoading) && (
|
||||
<EmptyState title="No Policies Match Search" icon={faSearch} />
|
||||
)}
|
||||
{Boolean(filteredPolicies.length) && (
|
||||
<Pagination
|
||||
count={filteredPolicies.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
<AccessPolicyForm
|
||||
projectId={currentProject.id}
|
||||
projectSlug={currentProject.slug}
|
||||
@@ -517,6 +507,6 @@ export const ApprovalPolicyList = ({ projectId }: IProps) => {
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+240
-275
@@ -14,7 +14,6 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
@@ -131,284 +130,250 @@ export const SecretApprovalRequest = () => {
|
||||
|
||||
const isFiltered = Boolean(searchFilter || envFilter || committerFilter);
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{isSecretApprovalScreen ? (
|
||||
<motion.div
|
||||
key="approval-changes-details"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<SecretApprovalRequestChanges
|
||||
approvalRequestId={selectedApprovalId || ""}
|
||||
onGoBack={handleGoBackSecretRequestDetail}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="approval-changes-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="rounded-md text-gray-300"
|
||||
>
|
||||
<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>
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Change Requests</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.32rem] 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>
|
||||
<p className="text-sm text-bunker-300">Review pending and closed change requests</p>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search change requests by author, environment or policy path..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("open")}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("open");
|
||||
}}
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("close")}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("close");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount.closed} Closed
|
||||
</div>
|
||||
<div className="flex grow justify-end space-x-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||
rightIcon={
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||
}
|
||||
>
|
||||
Environments
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="max-h-80 thin-scrollbar overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select an Environment
|
||||
</DropdownMenuLabel>
|
||||
{currentProject?.environments.map(({ slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||
key={`request-filter-${slug}`}
|
||||
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
{name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{!!permission.can(
|
||||
ProjectPermissionMemberActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className={committerFilter ? "text-white" : "text-bunker-300"}
|
||||
rightIcon={
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||
}
|
||||
>
|
||||
Author
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="max-h-80 thin-scrollbar overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select an Author
|
||||
</DropdownMenuLabel>
|
||||
{members?.map(({ user, id }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setCommitterFilter((state) => (state === user.id ? undefined : user.id))
|
||||
}
|
||||
key={`request-filter-member-${id}`}
|
||||
icon={
|
||||
committerFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
{user.username}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
|
||||
{isRequestListEmpty && !isFiltered && (
|
||||
<div className="py-12">
|
||||
<EmptyState
|
||||
title={`No ${statusFilter === "open" ? "Open" : "Closed"} Change Requests`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{secretApprovalRequests.map((secretApproval) => {
|
||||
const {
|
||||
id: reqId,
|
||||
commits,
|
||||
createdAt,
|
||||
reviewers,
|
||||
status,
|
||||
committerUser,
|
||||
hasMerged,
|
||||
updatedAt
|
||||
} = secretApproval;
|
||||
const isReviewed = reviewers.some(
|
||||
({ status: reviewStatus, userId }) =>
|
||||
userId === userSession.id && reviewStatus === ApprovalStatus.APPROVED
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={reqId}
|
||||
className="flex border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedApprovalId(secretApproval.id)}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-1 text-sm">
|
||||
<FontAwesomeIcon
|
||||
icon={faCodeBranch}
|
||||
size="sm"
|
||||
className="mr-1.5 text-mineshaft-300"
|
||||
/>
|
||||
{secretApproval.isReplicated
|
||||
? `${commits.length} secret pending import`
|
||||
: generateCommitText(commits)}
|
||||
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
|
||||
</div>
|
||||
<span className="text-xs leading-3 text-gray-500">
|
||||
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
||||
{committerUser ? (
|
||||
<>
|
||||
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
||||
{committerUser?.email})
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-600">Deleted User</span>
|
||||
)}
|
||||
{!isReviewed && status === "open" && " - Review required"}
|
||||
</span>
|
||||
</div>
|
||||
{status === "close" && (
|
||||
<Tooltip
|
||||
content={updatedAt ? format(new Date(updatedAt), "M/dd/yyyy h:mm a") : ""}
|
||||
>
|
||||
<div className="my-auto ml-auto">
|
||||
<Badge
|
||||
variant={hasMerged ? "success" : "danger"}
|
||||
className="flex h-min items-center gap-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={hasMerged ? faCodeMerge : faXmark} />
|
||||
{hasMerged ? "Merged" : "Rejected"}
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Boolean(
|
||||
!secretApprovalRequests.length && isFiltered && !isApprovalRequestLoading
|
||||
) && (
|
||||
<div className="py-12">
|
||||
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||
</div>
|
||||
)}
|
||||
{Boolean(totalApprovalCount) && (
|
||||
<Pagination
|
||||
className="border-none"
|
||||
count={totalApprovalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
return isSecretApprovalScreen ? (
|
||||
<SecretApprovalRequestChanges
|
||||
approvalRequestId={selectedApprovalId || ""}
|
||||
onGoBack={handleGoBackSecretRequestDetail}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Change Requests</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.32rem] 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]"
|
||||
/>
|
||||
)}
|
||||
{isApprovalRequestLoading && (
|
||||
<div>
|
||||
{Array.apply(0, Array(3)).map((_x, index) => (
|
||||
<div
|
||||
key={`approval-request-loading-${index + 1}`}
|
||||
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="mb-2 flex items-center">
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||
<Skeleton className="w-1/4 bg-mineshaft-600" />
|
||||
</div>
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">Review pending and closed change requests</p>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search change requests by author, environment or policy path..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("open")}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("open");
|
||||
}}
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"font-medium",
|
||||
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setStatusFilter("close")}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setStatusFilter("close");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount.closed} Closed
|
||||
</div>
|
||||
<div className="flex grow justify-end space-x-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
||||
>
|
||||
Environments
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="max-h-80 thin-scrollbar overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select an Environment
|
||||
</DropdownMenuLabel>
|
||||
{currentProject?.environments.map(({ slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||
key={`request-filter-${slug}`}
|
||||
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
{name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{!!permission.can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className={committerFilter ? "text-white" : "text-bunker-300"}
|
||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
||||
>
|
||||
Author
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={1}
|
||||
className="max-h-80 thin-scrollbar overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Select an Author
|
||||
</DropdownMenuLabel>
|
||||
{members?.map(({ user, id }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setCommitterFilter((state) => (state === user.id ? undefined : user.id))
|
||||
}
|
||||
key={`request-filter-member-${id}`}
|
||||
icon={committerFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
{user.username}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
|
||||
{isRequestListEmpty && !isFiltered && (
|
||||
<div className="py-12">
|
||||
<EmptyState
|
||||
title={`No ${statusFilter === "open" ? "Open" : "Closed"} Change Requests`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{secretApprovalRequests.map((secretApproval) => {
|
||||
const {
|
||||
id: reqId,
|
||||
commits,
|
||||
createdAt,
|
||||
reviewers,
|
||||
status,
|
||||
committerUser,
|
||||
hasMerged,
|
||||
updatedAt
|
||||
} = secretApproval;
|
||||
const isReviewed = reviewers.some(
|
||||
({ status: reviewStatus, userId }) =>
|
||||
userId === userSession.id && reviewStatus === ApprovalStatus.APPROVED
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={reqId}
|
||||
className="flex border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedApprovalId(secretApproval.id)}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-1 text-sm">
|
||||
<FontAwesomeIcon
|
||||
icon={faCodeBranch}
|
||||
size="sm"
|
||||
className="mr-1.5 text-mineshaft-300"
|
||||
/>
|
||||
{secretApproval.isReplicated
|
||||
? `${commits.length} secret pending import`
|
||||
: generateCommitText(commits)}
|
||||
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
|
||||
</div>
|
||||
<span className="text-xs leading-3 text-gray-500">
|
||||
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
||||
{committerUser ? (
|
||||
<>
|
||||
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
||||
{committerUser?.email})
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-600">Deleted User</span>
|
||||
)}
|
||||
{!isReviewed && status === "open" && " - Review required"}
|
||||
</span>
|
||||
</div>
|
||||
{status === "close" && (
|
||||
<Tooltip content={updatedAt ? format(new Date(updatedAt), "M/dd/yyyy h:mm a") : ""}>
|
||||
<div className="my-auto ml-auto">
|
||||
<Badge
|
||||
variant={hasMerged ? "success" : "danger"}
|
||||
className="flex h-min items-center gap-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={hasMerged ? faCodeMerge : faXmark} />
|
||||
{hasMerged ? "Merged" : "Rejected"}
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Boolean(!secretApprovalRequests.length && isFiltered && !isApprovalRequestLoading) && (
|
||||
<div className="py-12">
|
||||
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
{Boolean(totalApprovalCount) && (
|
||||
<Pagination
|
||||
className="border-none"
|
||||
count={totalApprovalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{isApprovalRequestLoading && (
|
||||
<div>
|
||||
{Array.apply(0, Array(3)).map((_x, index) => (
|
||||
<div
|
||||
key={`approval-request-loading-${index + 1}`}
|
||||
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="mb-2 flex items-center">
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||
<Skeleton className="w-1/4 bg-mineshaft-600" />
|
||||
</div>
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -228,7 +228,7 @@ export const SecretApprovalRequestChanges = ({ approvalRequestId, onGoBack }: Pr
|
||||
return (
|
||||
<div className="flex flex-col space-x-6 lg:flex-row">
|
||||
<div className="flex-1 lg:max-w-[calc(100%-17rem)]">
|
||||
<div className="sticky -top-8 z-20 flex items-center space-x-4 bg-bunker-800 pt-2 pb-6">
|
||||
<div className="sticky -top-10 z-20 flex items-center space-x-4 bg-bunker-800 pt-2 pb-6">
|
||||
<IconButton variant="outline_bg" ariaLabel="go-back" onClick={onGoBack}>
|
||||
<FontAwesomeIcon icon={faArrowLeft} />
|
||||
</IconButton>
|
||||
|
||||
@@ -3,9 +3,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faArrowDown, faArrowUp, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faChevronLeft,
|
||||
faInfoCircle
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||
import { Link, useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -51,7 +56,7 @@ import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/projects/types";
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
|
||||
import { queryClient } from "@app/hooks/api/reactQuery";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
||||
@@ -760,9 +765,19 @@ const Page = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:scheme-dark">
|
||||
<div className="mx-auto flex max-w-8xl flex-col text-mineshaft-50 dark:scheme-dark">
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/overview"
|
||||
params={{
|
||||
projectId
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Secrets Overview
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.SecretManager}
|
||||
title="Secrets Management"
|
||||
description={
|
||||
<p className="text-md text-bunker-300">
|
||||
|
||||
+85
-1
@@ -77,6 +77,11 @@ import {
|
||||
fetchDashboardProjectSecretsByKeys
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||
import {
|
||||
useGetVaultExternalMigrationConfigs,
|
||||
useImportVaultSecrets
|
||||
} from "@app/hooks/api/migration";
|
||||
import { VaultImportStatus } from "@app/hooks/api/migration/types";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
@@ -98,6 +103,7 @@ import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
||||
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
||||
import { FolderForm } from "./FolderForm";
|
||||
import { MoveSecretsModal } from "./MoveSecretsModal";
|
||||
import { VaultSecretImportModal } from "./VaultSecretImportModal";
|
||||
|
||||
type TParsedEnv = { value: string; comments: string[]; secretPath?: string; secretKey: string }[];
|
||||
type TParsedFolderEnv = Record<
|
||||
@@ -171,7 +177,8 @@ export const ActionBar = ({
|
||||
"upgradePlan",
|
||||
"replicateFolder",
|
||||
"confirmUpload",
|
||||
"requestAccess"
|
||||
"requestAccess",
|
||||
"importFromVault"
|
||||
] as const);
|
||||
const isProtectedBranch = Boolean(protectedBranchPolicyName);
|
||||
const { subscription } = useSubscription();
|
||||
@@ -185,6 +192,7 @@ export const ActionBar = ({
|
||||
const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({
|
||||
options: { onSuccess: undefined }
|
||||
});
|
||||
const { mutateAsync: importVaultSecrets } = useImportVaultSecrets();
|
||||
const queryClient = useQueryClient();
|
||||
const { addPendingChange } = useBatchModeActions();
|
||||
|
||||
@@ -193,6 +201,8 @@ export const ActionBar = ({
|
||||
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
||||
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
||||
|
||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||
try {
|
||||
@@ -663,6 +673,40 @@ export const ActionBar = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleVaultImport = async (vaultPath: string, namespace: string) => {
|
||||
try {
|
||||
const result = await importVaultSecrets({
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
vaultNamespace: namespace,
|
||||
vaultSecretPath: vaultPath
|
||||
});
|
||||
|
||||
if (result.status === VaultImportStatus.ApprovalRequired) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Secret change request created successfully. Awaiting approval."
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully imported secrets from HashiCorp Vault"
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Vault import error:", err);
|
||||
const error = err as AxiosError<{ message?: string }>;
|
||||
const errorMessage =
|
||||
error.response?.data?.message || "Failed to import secrets from Vault. Please try again.";
|
||||
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: errorMessage
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isTableFiltered =
|
||||
Object.values(filter.tags).some(Boolean) || Object.values(filter.include).some(Boolean);
|
||||
|
||||
@@ -1059,6 +1103,39 @@ export const ActionBar = ({
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{hasVaultConnection && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("importFromVault");
|
||||
handlePopUpClose("misc");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add from HashiCorp Vault
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -1277,6 +1354,13 @@ export const ActionBar = ({
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<VaultSecretImportModal
|
||||
isOpen={popUp.importFromVault.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("importFromVault", isOpen)}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
onImport={handleVaultImport}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
useGetVaultMounts,
|
||||
useGetVaultNamespaces,
|
||||
useGetVaultSecretPaths
|
||||
} from "@app/hooks/api/migration/queries";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
onImport: (vaultPath: string, namespace: string) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
onImport: (vaultPath: string, namespace: string) => void;
|
||||
};
|
||||
|
||||
const Content = ({ onClose, environment, secretPath, onImport }: ContentProps) => {
|
||||
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
|
||||
const [selectedMountPath, setSelectedMountPath] = useState<string | null>(null);
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||
const [shouldFetchPaths, setShouldFetchPaths] = useState(false);
|
||||
const [shouldFetchMounts, setShouldFetchMounts] = useState(false);
|
||||
|
||||
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
|
||||
const { data: secretPaths, isLoading: isLoadingPaths } = useGetVaultSecretPaths(
|
||||
shouldFetchPaths,
|
||||
selectedNamespace ?? undefined,
|
||||
selectedMountPath ?? undefined
|
||||
);
|
||||
const { data: mounts, isLoading: isLoadingMounts } = useGetVaultMounts(
|
||||
shouldFetchMounts,
|
||||
selectedNamespace ?? undefined
|
||||
);
|
||||
|
||||
// Filter to only show KV mounts
|
||||
const kvMounts = mounts?.filter((mount) => mount.type === "kv" || mount.type.startsWith("kv"));
|
||||
|
||||
// Enable fetching mounts when namespace is selected
|
||||
useEffect(() => {
|
||||
if (selectedNamespace) {
|
||||
setShouldFetchMounts(true);
|
||||
}
|
||||
}, [selectedNamespace]);
|
||||
|
||||
// Enable fetching paths when both namespace and mount path are selected
|
||||
useEffect(() => {
|
||||
if (selectedNamespace && selectedMountPath) {
|
||||
setShouldFetchPaths(true);
|
||||
} else {
|
||||
setShouldFetchPaths(false);
|
||||
}
|
||||
}, [selectedNamespace, selectedMountPath]);
|
||||
|
||||
const handleImport = () => {
|
||||
if (!selectedPath) {
|
||||
createNotification({ type: "error", text: "Please select a Vault secret path to import" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedNamespace) {
|
||||
createNotification({ type: "error", text: "Please select a namespace" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounts || mounts.length === 0) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "No Vault mounts found. Please ensure you have KV secret engines configured."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onImport(selectedPath, selectedNamespace);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 rounded-md bg-primary/10 p-3 text-sm text-mineshaft-200">
|
||||
<div className="flex items-start gap-2">
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mt-0.5 text-primary" />
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<strong>Import Secrets from HashiCorp Vault</strong>
|
||||
</div>
|
||||
<div className="space-y-1.5 text-xs leading-relaxed">
|
||||
<p>
|
||||
Select a Vault namespace and secret path to import secrets into the current
|
||||
Infisical environment (<code className="text-xs">{environment}</code>) at path{" "}
|
||||
<code className="text-xs">{secretPath}</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormControl
|
||||
label="Namespace"
|
||||
className="mb-4"
|
||||
tooltipText="Select the Vault namespace containing the secrets you want to import."
|
||||
>
|
||||
<>
|
||||
<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);
|
||||
setSelectedPath(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 mounts
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Secrets Engine"
|
||||
className="mb-4"
|
||||
tooltipText="Select the KV secrets engine to narrow down secret paths."
|
||||
>
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={kvMounts?.find((mount) => mount.path === selectedMountPath)}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
const mount = value as { path: string; type: string; version: string | null };
|
||||
setSelectedMountPath(mount.path.replace(/\/$/, "")); // Remove trailing slash
|
||||
setSelectedPath(null);
|
||||
}
|
||||
}}
|
||||
options={kvMounts || []}
|
||||
getOptionValue={(option) => option.path}
|
||||
getOptionLabel={(option) => option.path.replace(/\/$/, "")}
|
||||
isDisabled={isLoadingMounts || !kvMounts?.length}
|
||||
placeholder="Select secrets engine..."
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Choose a KV secrets engine to filter available secret paths
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="Vault Secret Path" className="mb-6">
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={selectedPath ? { path: selectedPath } : null}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
setSelectedPath((value as { path: string }).path);
|
||||
} else {
|
||||
setSelectedPath(null);
|
||||
}
|
||||
}}
|
||||
options={(secretPaths || []).map((path) => ({ path }))}
|
||||
getOptionValue={(option) => option.path}
|
||||
getOptionLabel={(option) => option.path}
|
||||
isDisabled={isLoadingPaths || !secretPaths?.length || !selectedMountPath}
|
||||
placeholder={
|
||||
!selectedMountPath
|
||||
? "Select a mount path first..."
|
||||
: "Select a Vault path to import..."
|
||||
}
|
||||
isClearable
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Choose a secret path from the selected mount to import into Infisical
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
isDisabled={!selectedPath || isLoadingMounts || isLoadingPaths}
|
||||
>
|
||||
Import Secrets
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const VaultSecretImportModal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
environment,
|
||||
secretPath,
|
||||
onImport
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
title="Import from HashiCorp Vault"
|
||||
subTitle="Select a Vault namespace and secret path to import secrets into the current environment and folder."
|
||||
className="max-w-2xl"
|
||||
>
|
||||
<Content
|
||||
onClose={() => onOpenChange(false)}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
onImport={onImport}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -383,7 +383,7 @@ export const CommitForm: React.FC<CommitFormProps> = ({
|
||||
</div>
|
||||
}
|
||||
subTitle="Write a commit message and review the changes you're about to save."
|
||||
className="max-h-[90vh] max-w-[95%] md:max-w-7xl"
|
||||
className="max-h-[90vh] max-w-[95%] md:max-w-8xl"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Changes List */}
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
useGetSecretRotations,
|
||||
useRestartSecretRotation
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { TSecretRotationProviderTemplate } from "@app/hooks/api/secretRotation/types";
|
||||
import { CreateRotationForm } from "@app/pages/secret-manager/SecretRotationPage/components/CreateRotationForm";
|
||||
|
||||
@@ -145,9 +146,9 @@ const Page = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto w-full max-w-7xl bg-bunker-800 text-white">
|
||||
<div className="container mx-auto w-full max-w-8xl bg-bunker-800 text-white">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.SecretManager}
|
||||
title="Secret Rotation"
|
||||
description="Stop manually rotating secrets and automate credential rotation."
|
||||
>
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ 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 mb-6 w-full max-w-8xl">
|
||||
<Button
|
||||
variant="link"
|
||||
type="submit"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { ProjectVersion } from "@app/hooks/api/projects/types";
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
|
||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||
|
||||
import { EncryptionTab } from "./components/EncryptionTab";
|
||||
@@ -40,18 +40,18 @@ export const SettingsPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
</Helmet>
|
||||
<div className="w-full max-w-7xl">
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.SecretManager}
|
||||
title="Settings"
|
||||
description="Configure your secret manager's encryption, environments, webhooks and other configurations."
|
||||
/>
|
||||
<Tabs defaultValue={tabs[0].key}>
|
||||
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
||||
<TabList>
|
||||
{tabs
|
||||
.filter((el) => !el.isHidden)
|
||||
.map((tab) => (
|
||||
<Tab value={tab.key} key={tab.key}>
|
||||
<Tab variant="project" value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user