Merge branch 'main' into feature/slack-secret-sync-error-notification

This commit is contained in:
Victor Santos
2025-11-03 15:55:10 -03:00
679 changed files with 19636 additions and 8864 deletions
@@ -7,16 +7,15 @@ import {
faTrash,
faUsers,
faUserXmark,
faWarning,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AlertTriangleIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
Checkbox,
DeleteActionModal,
@@ -38,6 +37,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useSubscription, useUser } from "@app/context";
import {
getUserTablePreference,
@@ -432,7 +432,7 @@ export const ServerAdminsTable = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`}
text="Your current plan does not allow removing server admins. To unlock this feature, please upgrade to Infisical Pro plan."
/>
<DeleteActionModal
isOpen={popUp.removeUsers.isOpen}
@@ -451,7 +451,7 @@ export const ServerAdminsTable = () => {
const email = user.email ?? user.username;
return (
<li key={user.id}>
<div className="flex items-center">
<div className="flex items-center gap-x-1">
<p>
{user.firstName || user.lastName ? (
<>
@@ -464,15 +464,10 @@ export const ServerAdminsTable = () => {
</p>
{userId === user.id && (
<Tooltip content="Are you sure you want to remove yourself from this instance?">
<div className="inline-block">
<Badge
variant="primary"
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
</Badge>
</div>
<Badge variant="danger">
<AlertTriangleIcon />
Deleting Yourself
</Badge>
</Tooltip>
)}
</div>
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { faRotate } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { RefreshCwIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
import { Button, DeleteActionModal } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useUser } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useInvalidateCache } from "@app/hooks/api";
@@ -62,11 +62,8 @@ export const CachingPageForm = () => {
<div className="mb-2 flex items-center gap-3">
<span className="text-xl font-medium text-mineshaft-100">Secrets Cache</span>
{isInvalidating && (
<Badge
variant="danger"
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
<Badge variant="danger">
<RefreshCwIcon className="animate-spin" />
Invalidating Cache
</Badge>
)}
@@ -1,13 +1,13 @@
import { useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { InfoIcon } from "lucide-react";
import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { Badge, Button, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { Button, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useServerConfig, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
@@ -54,7 +54,8 @@ export const EncryptionPageForm = () => {
if (!subscription.hsm) {
handlePopUpOpen("upgradePlan", {
description: "Hardware Security Module's (HSM's), are only available on Enterprise plans."
isEnterpriseFeature: true,
text: "Your current plan does not include access to Hardware Security Module (HSM). To unlock this feature, please upgrade to Infisical Enterprise plan."
});
return;
}
@@ -132,12 +133,10 @@ export const EncryptionPageForm = () => {
{config.fipsEnabled && (
<Tooltip content="FIPS mode of operation is enabled for your instance. All cryptographic operations within the FIPS boundaries are validated to be FIPS compliant.">
<div>
<Badge className="flex items-center gap-2" variant="primary">
FIPS Mode: Enabled
<FontAwesomeIcon icon={faInfoCircle} />
</Badge>
</div>
<Badge variant="info">
FIPS Mode: Enabled
<InfoIcon />
</Badge>
</Tooltip>
)}
</div>
@@ -145,7 +144,8 @@ export const EncryptionPageForm = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
text={popUp.upgradePlan?.data?.text}
isEnterpriseFeature={popUp.upgradePlan?.data?.isEnterpriseFeature}
/>
</>
);
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Control, Controller, useForm, useWatch } from "react-hook-form";
import {
faArrowUpRightFromSquare,
faBookOpen,
faChevronRight,
faExclamationTriangle,
faMagnifyingGlass
@@ -14,6 +12,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { DocumentationLinkBadge } from "@app/components/v3";
import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api";
type TForm = Record<string, string>;
@@ -213,22 +212,9 @@ export const EnvironmentPageForm = () => {
>
<div className="flex w-full flex-row items-center justify-between">
<div>
<div className="flex items-start gap-1">
<div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Overrides</p>
<a
href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides"
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>
<DocumentationLinkBadge href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides" />
</div>
<p className="text-sm text-bunker-300">
Override specific environment variables. After saving, it may take up to 5 minutes for
@@ -7,10 +7,10 @@ import {
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ServerCogIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import {
Badge,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
@@ -29,6 +29,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
getUserTablePreference,
PreferenceKey,
@@ -110,7 +111,8 @@ const IdentityPanelTable = ({
<Td>
{name}
{isInstanceAdmin && (
<Badge variant="primary" className="ml-2">
<Badge variant="info" className="ml-2">
<ServerCogIcon />
Server Admin
</Badge>
)}
@@ -3,7 +3,6 @@ import {
faArrowDown,
faArrowUp,
faBuilding,
faCircleQuestion,
faEllipsisV,
faEnvelope,
faEye,
@@ -19,11 +18,11 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { CircleQuestionMarkIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
@@ -46,6 +45,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useUser } from "@app/context";
import { OrgMembershipRole } from "@app/helpers/roles";
import {
@@ -295,15 +295,17 @@ const ViewMembersModalContent = ({
)}
</div>
</Td>
<Td className="max-w-0">
<Badge className="flex w-fit max-w-full items-center gap-x-1 bg-mineshaft-400/50 whitespace-nowrap text-bunker-200">
<p className="truncate capitalize">{member.role.replace("-", " ")}</p>
{Boolean(member.roleId) && (
<Tooltip content="This member has a custom role assigned.">
<FontAwesomeIcon icon={faCircleQuestion} className="w-3" />
</Tooltip>
)}
</Badge>
<Td>
<div className="flex max-w-32">
<Tooltip
content={member.roleId ? "This member has a custom role assigned." : ""}
>
<Badge isTruncatable variant="neutral">
<span className="capitalize">{member.role.replace("-", " ")}</span>
{Boolean(member.roleId) && <CircleQuestionMarkIcon />}
</Badge>
</Tooltip>
</div>
</Td>
<Td>
<div className="flex justify-end">
@@ -9,16 +9,15 @@ import {
faUsers,
faUserShield,
faUserXmark,
faWarning,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AlertTriangleIcon, UserCogIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
Checkbox,
DeleteActionModal,
@@ -41,6 +40,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useSubscription, useUser } from "@app/context";
import {
getUserTablePreference,
@@ -58,9 +58,6 @@ import {
import { User } from "@app/hooks/api/users/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions";
const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user";
const UserPanelTable = ({
handlePopUpOpen,
users,
@@ -84,7 +81,7 @@ const UserPanelTable = ({
data?: {
username: string;
id: string;
message?: string;
text?: string;
}
) => void;
isPending: boolean;
@@ -218,7 +215,8 @@ const UserPanelTable = ({
{name ?? <span className="text-mineshaft-400">Not Set</span>}
</p>
{superAdmin && (
<Badge variant="primary" className="ml-2 whitespace-nowrap">
<Badge variant="info" className="ml-2">
<UserCogIcon />
Server Admin
</Badge>
)}
@@ -259,7 +257,7 @@ const UserPanelTable = ({
handlePopUpOpen("upgradePlan", {
username,
id,
message: addServerAdminUpgradePlanMessage
text: "Your current plan does not allow setting additional server admins. To unlock this feature, please upgrade to Infisical Pro plan."
});
return;
}
@@ -287,7 +285,7 @@ const UserPanelTable = ({
handlePopUpOpen("upgradePlan", {
username,
id,
message: removeServerAdminUpgradePlanMessage
text: "Your current plan does not allow removing server admins. To unlock this feature, please upgrade to Infisical Pro plan."
});
return;
}
@@ -529,7 +527,7 @@ export const UserIdentitiesTable = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`}
text={popUp.upgradePlan.data?.text}
/>
<DeleteActionModal
isOpen={popUp.removeUsers.isOpen}
@@ -548,7 +546,7 @@ export const UserIdentitiesTable = () => {
const email = user.email ?? user.username;
return (
<li key={user.id}>
<div className="flex items-center">
<div className="flex items-center gap-x-1">
<p>
{user.firstName || user.lastName ? (
<>
@@ -561,15 +559,10 @@ export const UserIdentitiesTable = () => {
</p>
{userId === user.id && (
<Tooltip content="Are you sure you want to remove yourself from this instance?">
<div className="inline-block">
<Badge
variant="primary"
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
</Badge>
</div>
<Badge variant="danger">
<AlertTriangleIcon />
Deleting Yourself
</Badge>
</Tooltip>
)}
</div>
@@ -7,7 +7,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -22,6 +21,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useGetCaCerts } from "@app/hooks/api";
@@ -60,7 +60,7 @@ export const CaCertificatesTable = ({ caId }: Props) => {
<div className="flex items-center">
CA Certificate {caCert.version}
{isLastItem && (
<Badge variant="success" className="ml-4">
<Badge variant="info" className="ml-4">
Current
</Badge>
)}
@@ -1,7 +1,6 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
@@ -24,8 +23,7 @@ export const CaSection = () => {
"caCert",
"installCaCert",
"deleteCa",
"caStatus", // enable / disable
"upgradePlan"
"caStatus" // enable / disable
] as const);
const onRemoveCaSubmit = async (caName: string) => {
@@ -124,11 +122,6 @@ export const CaSection = () => {
onUpdateCaStatus(popUp?.caStatus?.data as { caName: string; status: CaStatus })
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};
@@ -6,7 +6,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -22,6 +21,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
import {
@@ -34,15 +34,12 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["installCaCert", "caCert", "ca", "deleteCa", "caStatus", "upgradePlan"]
>,
popUpName: keyof UsePopUpState<["installCaCert", "caCert", "ca", "deleteCa", "caStatus"]>,
data?: {
caId?: string;
caName?: string;
dn?: string;
status?: CaStatus;
description?: string;
}
) => void;
};
@@ -124,7 +124,7 @@ type Props = {
const caTypes = [
{ label: "ACME", value: CaType.ACME },
{ label: "Azure AD Certificate Service", value: CaType.AZURE_AD_CS }
{ label: "Active Directory Certificate Services (AD CS)", value: CaType.AZURE_AD_CS }
];
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
@@ -1,7 +1,6 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
@@ -20,8 +19,7 @@ export const ExternalCaSection = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"ca",
"deleteCa",
"caStatus", // enable / disable
"upgradePlan"
"caStatus" // enable / disable
] as const);
const onRemoveCaSubmit = async (caName: string, type: CaType) => {
@@ -131,11 +129,6 @@ export const ExternalCaSection = () => {
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};
@@ -10,7 +10,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -26,6 +25,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
@@ -33,12 +33,11 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus", "upgradePlan"]>,
popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus"]>,
data?: {
name?: string;
type?: CaType;
status?: CaStatus;
description?: string;
}
) => void;
};
@@ -1,61 +0,0 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PkiCollectionSection } from "../AlertingPage/components";
import { CertificatesSection } from "./components";
export const CertificatesPage = () => {
const { t } = useTranslation();
const { permission } = useProjectPermission();
const canAccessPkiColl = permission.can(
ProjectPermissionActions.Read,
ProjectPermissionSub.PkiCollections
);
const canAccessCerts = permission.can(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
return (
<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-8xl">
<PageHeader
scope={ProjectType.CertificateManager}
title="Certificates"
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
/>
{/* If both are false, the section does not render. This is to prevent duplicate banners. */}
{(canAccessCerts || canAccessPkiColl) && (
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.PkiCollections}
>
<PkiCollectionSection />
</ProjectPermissionCan>
)}
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
>
<CertificatesSection />
</ProjectPermissionCan>
</div>
</div>
);
};
@@ -122,11 +122,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const { data: profilesData } = useListCertificateProfiles({
projectId: currentProject?.id || "",
includeMetrics: false,
enrollmentType: "api"
});
const { mutateAsync: createCertificate } = useCreateCertificateV3();
const { mutateAsync: createCertificate } = useCreateCertificateV3({
projectId: currentProject?.id
});
const formResolver = useMemo(() => {
return zodResolver(createSchema(shouldShowSubjectSection));
@@ -0,0 +1,296 @@
import { useEffect, useMemo, useState } from "react";
import { faPlus, faSearch } from "@fortawesome/free-solid-svg-icons";
import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
EmptyState,
Input,
Modal,
ModalContent,
Pagination,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useProject } from "@app/context";
import {
PkiSync,
useAddCertificatesToPkiSync,
useListPkiSyncsWithCertificate,
useRemoveCertificatesFromPkiSync
} from "@app/hooks/api/pkiSyncs";
import { IntegrationsListPageTabs } from "@app/types/integrations";
type Props = {
popUp: {
isOpen: boolean;
data?: {
certificateId?: string;
commonName?: string;
};
};
handlePopUpToggle: (popUpName: "managePkiSyncs", state?: boolean) => void;
};
const PER_PAGE = 10;
export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Props) => {
const [selectedSyncIds, setSelectedSyncIds] = useState<Set<string>>(new Set());
const [initialSyncIds, setInitialSyncIds] = useState<Set<string>>(new Set());
const [isSubmitting, setIsSubmitting] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const { currentProject } = useProject();
const navigate = useNavigate();
const { certificateId, commonName } = popUp.data || {};
const { data: pkiSyncs = [], isPending } = useListPkiSyncsWithCertificate(
currentProject?.id || "",
certificateId || "",
{
enabled: !!currentProject?.id && !!certificateId
}
);
const addCertificatesToSync = useAddCertificatesToPkiSync();
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
const filteredSyncs = useMemo(() => {
if (!searchTerm.trim()) return pkiSyncs;
const searchLower = searchTerm.toLowerCase();
return pkiSyncs.filter((sync) => sync.name.toLowerCase().includes(searchLower));
}, [pkiSyncs, searchTerm]);
const startIndex = (currentPage - 1) * PER_PAGE;
const endIndex = startIndex + PER_PAGE;
const paginatedSyncs = filteredSyncs.slice(startIndex, endIndex);
useEffect(() => {
setCurrentPage(1);
}, [searchTerm]);
const handleClose = () => {
handlePopUpToggle("managePkiSyncs", false);
setSelectedSyncIds(new Set());
setInitialSyncIds(new Set());
setSearchTerm("");
setCurrentPage(1);
};
const handleNavigateToPkiSyncs = () => {
if (!currentProject?.id) return;
navigate({
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
params: {
projectId: currentProject.id
},
search: {
selectedTab: IntegrationsListPageTabs.PkiSyncs
}
});
handleClose();
};
const getDestinationDisplayName = (destination: string) => {
switch (destination) {
case PkiSync.AzureKeyVault:
return "Azure Key Vault";
case PkiSync.AwsCertificateManager:
return "AWS Certificate Manager";
default:
return destination;
}
};
useEffect(() => {
if (!certificateId || !pkiSyncs || pkiSyncs.length === 0) return;
const currentSyncIds = new Set(
pkiSyncs.filter((sync) => sync.hasCertificate).map((sync) => sync.id)
);
setSelectedSyncIds(currentSyncIds);
setInitialSyncIds(new Set(currentSyncIds));
}, [certificateId, pkiSyncs]);
const handleSyncToggle = (syncId: string) => {
setSelectedSyncIds((prev) => {
const newSet = new Set(prev);
if (newSet.has(syncId)) {
newSet.delete(syncId);
} else {
newSet.add(syncId);
}
return newSet;
});
};
const handleSaveChanges = async () => {
if (!certificateId) return;
try {
setIsSubmitting(true);
const syncsToAdd = Array.from(selectedSyncIds).filter((id) => !initialSyncIds.has(id));
const syncsToRemove = Array.from(initialSyncIds).filter((id) => !selectedSyncIds.has(id));
await Promise.all(
syncsToAdd.map((syncId) =>
addCertificatesToSync.mutateAsync({
pkiSyncId: syncId,
certificateIds: [certificateId]
})
)
);
await Promise.all(
syncsToRemove.map((syncId) =>
removeCertificatesFromSync.mutateAsync({
pkiSyncId: syncId,
certificateIds: [certificateId]
})
)
);
createNotification({
text: `PKI sync settings updated for certificate "${commonName}"`,
type: "success"
});
handleClose();
} catch (error) {
console.error(error);
createNotification({
text: "Failed to update PKI sync settings",
type: "error"
});
} finally {
setIsSubmitting(false);
}
};
return (
<Modal isOpen={popUp.isOpen} onOpenChange={handleClose}>
<ModalContent
title="Manage PKI Syncs"
subTitle={`Select which PKI syncs "${commonName}" should be part of`}
className="max-w-3xl"
>
<div className="mb-4">
<Input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search PKI syncs by name..."
/>
</div>
<div className="mt-4 max-h-96 overflow-y-auto">
{isPending && (
<div className="flex h-32 items-center justify-center">
<div className="text-bunker-300">Loading PKI syncs...</div>
</div>
)}
{!isPending && pkiSyncs.length === 0 && (
<EmptyState title="No PKI syncs available" icon={faPlus}>
<div className="mt-1">
Create a{" "}
<button
type="button"
onClick={handleNavigateToPkiSyncs}
className="cursor-pointer underline hover:text-mineshaft-300"
>
PKI sync
</button>{" "}
first to manage certificate syncing.
</div>
</EmptyState>
)}
{!isPending && pkiSyncs.length > 0 && filteredSyncs.length === 0 && searchTerm && (
<EmptyState title="No PKI syncs found" icon={faSearch}>
<div className="mt-1">
No PKI syncs match your search criteria. Try a different search term.
</div>
</EmptyState>
)}
{!isPending && filteredSyncs.length > 0 && (
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-12" />
<Th className="w-1/2">Name</Th>
<Th className="w-1/2">Destination</Th>
</Tr>
</THead>
<TBody>
{paginatedSyncs.map((sync) => (
<Tr
key={sync.id}
className="cursor-pointer hover:bg-mineshaft-700"
onClick={() => handleSyncToggle(sync.id)}
>
<Td>
<Checkbox
isChecked={selectedSyncIds.has(sync.id)}
onCheckedChange={() => handleSyncToggle(sync.id)}
id={`sync-${sync.id}`}
/>
</Td>
<Td className="w-1/2 max-w-0">
<div className="truncate" title={sync.name}>
{sync.name}
</div>
</Td>
<Td className="w-1/2 max-w-0">
<div
className="truncate capitalize"
title={getDestinationDisplayName(sync.destination)}
>
{getDestinationDisplayName(sync.destination)}
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
)}
{!isPending && filteredSyncs.length > PER_PAGE && (
<div className="mt-4">
<Pagination
count={filteredSyncs.length}
page={currentPage}
perPage={PER_PAGE}
onChangePage={setCurrentPage}
onChangePerPage={() => {}}
/>
</div>
)}
</div>
<div className="mt-6 flex justify-end gap-3">
<Button variant="outline_bg" onClick={handleClose} isDisabled={isSubmitting}>
Cancel
</Button>
<Button
variant="solid"
colorSchema="primary"
onClick={handleSaveChanges}
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Save Changes
</Button>
</div>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,265 @@
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, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useProject } from "@app/context";
import { useUpdateRenewalConfig } from "@app/hooks/api";
import { useGetCertificateProfileById } from "@app/hooks/api/certificateProfiles";
import { UsePopUpState } from "@app/hooks/usePopUp";
const DEFAULT_RENEWAL_BEFORE_DAYS = 20;
const MIN_RENEWAL_BEFORE_DAYS = 1;
const MAX_RENEWAL_BEFORE_DAYS = 30;
const createFormSchema = (ttlDays: number, notAfter: string) =>
z.object({
renewBeforeDays: z
.number()
.min(MIN_RENEWAL_BEFORE_DAYS, `Renewal days must be at least ${MIN_RENEWAL_BEFORE_DAYS}`)
.max(MAX_RENEWAL_BEFORE_DAYS, `Renewal days cannot exceed ${MAX_RENEWAL_BEFORE_DAYS}`)
.refine(
(value) => value < ttlDays,
(value) => ({
message: `Renewal days (${value}) must be less than certificate TTL (${ttlDays} days)`
})
)
.refine(
(value) => {
const expiryDate = new Date(notAfter);
const renewalDate = new Date(expiryDate.getTime() - value * 24 * 60 * 60 * 1000);
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
return renewalDate >= tomorrow;
},
() => ({
message: "Renewals can only be scheduled from tomorrow onwards."
})
)
});
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
type Props = {
popUp: UsePopUpState<["manageRenewal"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["manageRenewal"]>, state?: boolean) => void;
};
const RenewalConfigForm = ({
control,
errors,
onSubmit,
isLoading,
buttonText,
onCancel
}: {
control: any;
errors: { renewBeforeDays?: { message?: string } };
onSubmit: (e?: React.BaseSyntheticEvent) => Promise<void>;
isLoading: boolean;
buttonText: string;
onCancel: () => void;
}) => (
<form onSubmit={onSubmit}>
<FormControl
label="Auto-renew days before expiry"
isError={Boolean(errors.renewBeforeDays)}
errorText={errors.renewBeforeDays?.message}
className="mb-6"
>
<Controller
control={control}
name="renewBeforeDays"
render={({ field }) => (
<Input
{...field}
type="number"
min={MIN_RENEWAL_BEFORE_DAYS}
max={MAX_RENEWAL_BEFORE_DAYS}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
field.onChange(value);
}}
placeholder="Enter days before expiration"
/>
)}
/>
</FormControl>
<div className="flex justify-end gap-3">
<Button type="button" colorSchema="secondary" variant="plain" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" colorSchema="primary" isLoading={isLoading} isDisabled={isLoading}>
{buttonText}
</Button>
</div>
</form>
);
export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentProject } = useProject();
const { mutateAsync: updateRenewalConfig, isPending: isUpdatingConfig } =
useUpdateRenewalConfig();
const certificateData = popUp.manageRenewal.data as {
certificateId: string;
commonName: string;
profileId: string;
renewBeforeDays?: number;
ttlDays?: number;
notAfter: string;
renewalError?: string;
renewedFromCertificateId?: string;
renewedByCertificateId?: string;
};
const { data: profileData } = useGetCertificateProfileById({
profileId: certificateData?.profileId || ""
});
const defaultRenewalDays = useMemo(() => {
if (certificateData?.renewBeforeDays) {
return certificateData.renewBeforeDays;
}
if (profileData?.apiConfig?.renewBeforeDays) {
return profileData.apiConfig.renewBeforeDays;
}
return DEFAULT_RENEWAL_BEFORE_DAYS;
}, [certificateData?.renewBeforeDays, profileData?.apiConfig?.renewBeforeDays]);
const isAutoRenewalEnabled = Boolean(
certificateData?.renewBeforeDays && certificateData.renewBeforeDays > 0
);
const hasRenewalError = Boolean(certificateData?.renewalError);
const formSchema = createFormSchema(
certificateData?.ttlDays || 365,
certificateData?.notAfter || ""
);
const {
control,
handleSubmit,
formState: { errors },
reset
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
renewBeforeDays: defaultRenewalDays
}
});
useEffect(() => {
if (popUp.manageRenewal.isOpen) {
reset({
renewBeforeDays: defaultRenewalDays
});
}
}, [popUp.manageRenewal.isOpen, defaultRenewalDays, reset]);
const onUpdateRenewal = async (data: FormData) => {
try {
if (!currentProject?.slug) {
createNotification({
text: "Unable to update auto-renewal: Project not found. Please refresh the page and try again.",
type: "error"
});
return;
}
await updateRenewalConfig({
certificateId: certificateData.certificateId,
renewBeforeDays: data.renewBeforeDays,
projectSlug: currentProject.slug
});
createNotification({
text: isAutoRenewalEnabled
? "Auto-renewal configuration updated successfully"
: "Auto-renewal enabled successfully",
type: "success"
});
handlePopUpToggle("manageRenewal", false);
} catch (err) {
console.error(err);
createNotification({
text: isAutoRenewalEnabled
? "Failed to update auto-renewal configuration. Please check your inputs and try again."
: "Failed to enable auto-renewal. Please check your inputs and try again.",
type: "error"
});
}
};
const getModalTitle = () => {
if (hasRenewalError) {
return `Fix Auto-Renewal: ${certificateData?.commonName || ""}`;
}
if (isAutoRenewalEnabled) {
return `Manage Auto-Renewal for ${certificateData?.commonName || ""}`;
}
return `Enable Auto-Renewal for ${certificateData?.commonName || ""}`;
};
if (!certificateData) {
return null;
}
return (
<Modal
isOpen={popUp?.manageRenewal?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("manageRenewal", isOpen);
}}
>
<ModalContent title={getModalTitle()}>
{hasRenewalError && (
<div className="mb-6 rounded-md border border-red-600 bg-red-900/20 p-4">
<div className="flex items-start gap-3">
<div className="mt-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-600">
<span className="text-xs font-bold text-white">!</span>
</div>
<div className="flex-1">
<h3 className="font-medium text-red-400">Automatic Renewal Failed</h3>
<p className="mt-1 text-sm text-red-300">
The last automatic renewal attempt failed: {certificateData.renewalError}
</p>
<p className="mt-2 text-sm text-red-300">
You can reconfigure auto-renewal below or disable it completely.
</p>
</div>
</div>
</div>
)}
{(!isAutoRenewalEnabled || hasRenewalError) && (
<RenewalConfigForm
control={control}
errors={errors}
onSubmit={handleSubmit(onUpdateRenewal)}
isLoading={isUpdatingConfig}
buttonText={isAutoRenewalEnabled ? "Update Configuration" : "Enable Auto-Renewal"}
onCancel={() => handlePopUpToggle("manageRenewal", false)}
/>
)}
{isAutoRenewalEnabled && !hasRenewalError && (
<RenewalConfigForm
control={control}
errors={errors}
onSubmit={handleSubmit(onUpdateRenewal)}
isLoading={isUpdatingConfig}
buttonText="Update Configuration"
onCancel={() => handlePopUpToggle("manageRenewal", false)}
/>
)}
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,178 @@
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, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useProject } from "@app/context";
import { useUpdateRenewalConfig } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const createFormSchema = (ttlDays: number) =>
z.object({
renewBeforeDays: z
.number()
.min(1, "Renewal days must be at least 1")
.max(365, "Renewal days cannot exceed 365")
.refine(
(value) => value < ttlDays,
(value) => ({
message: `Renewal days (${value}) must be less than certificate TTL (${ttlDays} days)`
})
)
});
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
type Props = {
popUp: UsePopUpState<["configureRenewal"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["configureRenewal"]>,
state?: boolean
) => void;
};
export const CertificateRenewalConfigModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentProject } = useProject();
const { mutateAsync: updateRenewalConfig, isPending: isSubmitting } = useUpdateRenewalConfig();
const certificateData = popUp.configureRenewal.data as {
certificateId: string;
commonName: string;
profileId: string;
renewBeforeDays?: number;
ttlDays: number;
};
const formSchema = createFormSchema(certificateData.ttlDays);
const {
control,
handleSubmit,
formState: { errors },
watch
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
renewBeforeDays: certificateData?.renewBeforeDays || 1
}
});
const renewBeforeDays = watch("renewBeforeDays");
const onSubmit = async (data: FormData) => {
try {
if (!currentProject?.slug) {
createNotification({
text: "Project not found",
type: "error"
});
return;
}
await updateRenewalConfig({
certificateId: certificateData.certificateId,
renewBeforeDays: data.renewBeforeDays,
projectSlug: currentProject.slug
});
createNotification({
text: "Successfully updated auto-renewal configuration",
type: "success"
});
handlePopUpToggle("configureRenewal", false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update auto-renewal configuration",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.configureRenewal?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("configureRenewal", isOpen);
}}
>
<ModalContent title={`Configure Auto-Renewal: ${certificateData?.commonName || ""}`}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-4">
<p className="mb-4 text-sm text-mineshaft-300">
Configure when this certificate should be automatically renewed. The certificate will
be renewed when it has the specified number of days remaining before expiration.
</p>
<div className="mb-4 rounded border bg-mineshaft-800 p-3">
<p className="text-sm text-mineshaft-300">
<strong>Certificate TTL:</strong> {certificateData?.ttlDays} days
</p>
<p className="text-sm text-mineshaft-300">
<strong>Current Setting:</strong>{" "}
{certificateData?.renewBeforeDays
? `${certificateData.renewBeforeDays} days before expiration`
: "Disabled"}
</p>
</div>
<Controller
control={control}
name="renewBeforeDays"
render={({ field }) => (
<FormControl
label="Renew Before Days"
isError={Boolean(errors.renewBeforeDays)}
errorText={errors.renewBeforeDays?.message}
>
<Input
{...field}
type="number"
min={1}
max={certificateData?.ttlDays ? certificateData.ttlDays - 1 : undefined}
placeholder="Enter days before expiration"
onChange={(e) => {
const value = parseInt(e.target.value, 10);
field.onChange(Number.isNaN(value) ? 0 : value);
}}
/>
</FormControl>
)}
/>
{renewBeforeDays && certificateData?.ttlDays && (
<div className="mt-2 rounded bg-primary-900/20 p-2">
<p className="text-sm text-primary-300">
{renewBeforeDays >= certificateData.ttlDays
? "⚠️ Renewal days must be less than certificate TTL"
: `✓ Certificate will be renewed ${renewBeforeDays} days before expiration`}
</p>
</div>
)}
</div>
<div className="flex items-center gap-2">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || renewBeforeDays >= (certificateData?.ttlDays || 0)}
>
Update Configuration
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("configureRenewal", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,94 @@
import { createNotification } from "@app/components/notifications";
import { Button, Modal, ModalContent } from "@app/components/v2";
import { useProject } from "@app/context";
import { useUpdateRenewalConfig } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["disableRenewal"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["disableRenewal"]>, state?: boolean) => void;
};
export const CertificateRenewalDisableModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentProject } = useProject();
const { mutateAsync: updateRenewalConfig, isPending: isSubmitting } = useUpdateRenewalConfig();
const certificateData = popUp.disableRenewal.data as {
certificateId: string;
commonName: string;
};
const onDisableConfirm = async () => {
try {
if (!currentProject?.slug) {
createNotification({
text: "Project not found",
type: "error"
});
return;
}
await updateRenewalConfig({
certificateId: certificateData.certificateId,
projectSlug: currentProject.slug,
enableAutoRenewal: false
});
createNotification({
text: "Successfully disabled auto-renewal",
type: "success"
});
handlePopUpToggle("disableRenewal", false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to disable auto-renewal",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.disableRenewal?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("disableRenewal", isOpen);
}}
>
<ModalContent title={`Disable Auto-Renewal: ${certificateData?.commonName || ""}`}>
<div className="mb-4">
<p className="mb-3 text-sm text-mineshaft-300">
Are you sure you want to disable auto-renewal for this certificate?
</p>
<div className="rounded border border-yellow-700/50 bg-yellow-900/20 p-3">
<p className="text-sm text-yellow-300">
<strong>Warning:</strong> Once disabled, this certificate will not be automatically
renewed and may expire without notice. You can re-enable auto-renewal at any time.
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
className="mr-4"
size="sm"
colorSchema="danger"
onClick={onDisableConfirm}
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Disable Auto-Renewal
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("disableRenewal", false)}
>
Cancel
</Button>
</div>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,80 @@
import { faRedo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Button, Modal, ModalContent } from "@app/components/v2";
import { useRenewCertificate } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["renewCertificate"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["renewCertificate"]>,
state?: boolean
) => void;
};
export const CertificateRenewalModal = ({ popUp, handlePopUpToggle }: Props) => {
const { mutateAsync: renewCertificate, isPending: isRenewing } = useRenewCertificate();
const onRenewConfirm = async () => {
try {
const { certificateId } = popUp.renewCertificate.data as { certificateId: string };
await renewCertificate({
certificateId
});
createNotification({
text: "Certificate renewed successfully",
type: "success"
});
handlePopUpToggle("renewCertificate", false);
} catch (err) {
console.error(err);
}
};
const certificateData = popUp.renewCertificate.data as {
certificateId: string;
commonName: string;
profileId: string;
};
return (
<Modal
isOpen={popUp?.renewCertificate?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("renewCertificate", isOpen);
}}
>
<ModalContent title={`Renew Certificate: ${certificateData?.commonName || ""}`}>
<div className="mb-6">
<p className="mb-4 text-sm text-mineshaft-300">
Are you sure you want to renew this certificate now?
</p>
</div>
<div className="flex items-center gap-3">
<Button
onClick={onRenewConfirm}
colorSchema="primary"
isLoading={isRenewing}
isDisabled={isRenewing}
>
<FontAwesomeIcon icon={faRedo} className="mr-2" />
Renew Now
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("renewCertificate", false)}
>
Cancel
</Button>
</div>
</ModalContent>
</Modal>
);
};
@@ -172,10 +172,14 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
.map(([key]) =>
key === CertKeyUsage.CRL_SIGN
? "cRLSign"
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
});
createNotification({
@@ -193,10 +197,14 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
.map(([key]) =>
key === CertKeyUsage.CRL_SIGN
? "cRLSign"
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
});
createNotification({
@@ -104,7 +104,8 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="Managing template enrollment options for EST is only available on Infisical's Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text="Your current plan does not include access to managing template enrollment options for EST. To unlock this feature, please upgrade to Infisical Enterprise plan."
/>
</div>
);
@@ -36,6 +36,7 @@ type Props = {
data?: {
id?: string;
name?: string;
isEnterpriseFeature?: boolean;
}
) => void;
};
@@ -90,7 +91,9 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
<DropdownMenuItem
onClick={() => {
if (!subscription?.pkiEst) {
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
});
return;
}
@@ -7,8 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useProject,
useSubscription
useProject
} from "@app/context";
import { useDeleteCert } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -16,16 +15,19 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { CertificateCertModal } from "./CertificateCertModal";
import { CertificateImportModal } from "./CertificateImportModal";
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
import { CertificateModal } from "./CertificateModal";
import { CertificateRenewalModal } from "./CertificateRenewalModal";
import { CertificateRevocationModal } from "./CertificateRevocationModal";
import { CertificatesTable } from "./CertificatesTable";
export const CertificatesSection = () => {
const { currentProject } = useProject();
const { subscription } = useSubscription();
const { mutateAsync: deleteCert } = useDeleteCert();
const isLegacyTemplatesEnabled = subscription.pkiLegacyTemplates;
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
const isLegacyTemplatesEnabled = true;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"certificateIssuance",
@@ -33,7 +35,10 @@ export const CertificatesSection = () => {
"certificateImport",
"certificateCert",
"deleteCertificate",
"revokeCertificate"
"revokeCertificate",
"manageRenewal",
"renewCertificate",
"managePkiSyncs"
] as const);
const onRemoveCertificateSubmit = async (serialNumber: string) => {
@@ -98,7 +103,13 @@ export const CertificatesSection = () => {
)}
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateManagePkiSyncsModal
popUp={popUp.managePkiSyncs}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteCertificate.isOpen}
title={`Are you sure you want to remove the certificate ${
@@ -2,18 +2,25 @@ import { useMemo, useState } from "react";
import {
faBan,
faCertificate,
faClockRotateLeft,
faEllipsis,
faEye,
faFileExport,
faLink,
faRedo,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
CertificateDisplayName,
getCertificateDisplayName
} from "@app/components/utilities/certificateDisplayUtils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -30,28 +37,53 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useProject
} from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api";
import { useUpdateRenewalConfig } from "@app/hooks/api";
import { caSupportsCapability } from "@app/hooks/api/ca/constants";
import { CaCapability, CaType } from "@app/hooks/api/ca/enums";
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
import { CertStatus } from "@app/hooks/api/certificates/enums";
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
const isExpiringWithinOneDay = (notAfter: string): boolean => {
const expiryDate = new Date(notAfter);
const now = new Date();
const oneDayFromNow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
return expiryDate <= oneDayFromNow;
};
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["certificate", "deleteCertificate", "revokeCertificate", "certificateCert"]
[
"certificate",
"deleteCertificate",
"revokeCertificate",
"certificateCert",
"manageRenewal",
"renewCertificate",
"managePkiSyncs"
]
>,
data?: {
serialNumber?: string;
commonName?: string;
certificateId?: string;
profileId?: string;
renewBeforeDays?: number;
ttlDays?: number;
notAfter?: string;
renewalError?: string;
renewedFromCertificateId?: string;
renewedByCertificateId?: string;
}
) => void;
};
@@ -69,10 +101,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
limit: perPage
});
// Fetch CA data to determine capabilities
const { mutateAsync: updateRenewalConfig } = useUpdateRenewalConfig();
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
const isLegacyTemplatesEnabled = true;
const { data: caData } = useListCasByProjectId(currentProject?.id ?? "");
// Create mapping from caId to CA type for capability checking
const caCapabilityMap = useMemo(() => {
if (!caData) return {};
@@ -83,26 +117,78 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
return map;
}, [caData]);
const handleDisableAutoRenewal = async (certificateId: string, commonName: string) => {
try {
if (!currentProject?.slug) {
createNotification({
text: "Unable to disable auto-renewal: Project not found. Please refresh the page and try again.",
type: "error"
});
return;
}
await updateRenewalConfig({
certificateId,
projectSlug: currentProject.slug,
enableAutoRenewal: false
});
createNotification({
text: `Auto-renewal disabled for ${commonName}`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to disable auto-renewal. Please try again or contact support if the issue persists.",
type: "error"
});
}
};
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Common Name</Th>
<Th>Status</Th>
<Th>Not Before</Th>
<Th>Not After</Th>
<Th />
<Th className="w-1/2">SAN / CN</Th>
<Th className="w-1/6">Status</Th>
<Th className="w-1/6">Not Before</Th>
<Th className="w-1/6">Not After</Th>
<Th className="w-12" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="project-cas" />}
{isPending && <TableSkeleton columns={4} innerKey="project-cas" />}
{!isPending &&
data?.certificates.map((certificate) => {
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
const isRevoked = certificate.status === CertStatus.REVOKED;
const isExpired = new Date(certificate.notAfter) < new Date();
const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter);
const hasFailed = Boolean(certificate.renewalError);
const isAutoRenewalEnabled = Boolean(
certificate.renewBeforeDays && certificate.renewBeforeDays > 0
);
const canShowAutoRenewalIcon = Boolean(
certificate.profileId &&
certificate.hasPrivateKey !== false &&
!certificate.renewedByCertificateId &&
!isRevoked &&
!isExpired &&
!isExpiringWithinDay
);
// Still need originalDisplayName for other uses in the component
const { originalDisplayName } = getCertificateDisplayName(certificate, 64, "—");
return (
<Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.commonName}</Td>
<Tr className="group h-10" key={`certificate-${certificate.id}`}>
<Td className="max-w-0">
<CertificateDisplayName cert={certificate} maxLength={64} fallback="—" />
</Td>
<Td>
{certificate.status === CertStatus.REVOKED ? (
<Badge variant="danger">Revoked</Badge>
@@ -120,7 +206,64 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
: "-"}
</Td>
<Td className="flex justify-end">
<Td className="flex items-center justify-end gap-2">
<div
className={`transition-opacity ${(() => {
if (!canShowAutoRenewalIcon) return "";
if (isAutoRenewalEnabled) return "opacity-100";
return "opacity-0 group-hover:opacity-100";
})()}`}
>
{canShowAutoRenewalIcon && (
<Tooltip
content={(() => {
if (hasFailed && certificate.renewalError) {
return `Auto-renewal failed: ${certificate.renewalError}`;
}
if (isAutoRenewalEnabled) {
const expiryDate = new Date(certificate.notAfter);
const now = new Date();
const daysUntilExpiry = Math.ceil(
(expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
);
const daysUntilRenewal = Math.max(
0,
daysUntilExpiry - (certificate.renewBeforeDays || 0)
);
return `Auto-renews in ${daysUntilRenewal}d`;
}
return "Set auto renewal";
})()}
>
<button
type="button"
className={(() => {
if (hasFailed) return "pr-1 text-red-500 hover:text-red-400";
return "pr-1 text-primary-500 hover:text-primary-400";
})()}
aria-label="Certificate auto-renewal"
onClick={(e) => {
e.stopPropagation();
if (hasFailed) return;
handlePopUpOpen("manageRenewal", {
certificateId: certificate.id,
commonName: originalDisplayName,
profileId: certificate.profileId || "",
renewBeforeDays: certificate.renewBeforeDays || 7,
ttlDays: Math.ceil(
(new Date(certificate.notAfter).getTime() -
new Date(certificate.notBefore).getTime()) /
(24 * 60 * 60 * 1000)
)
});
}}
>
<FontAwesomeIcon icon={faClockRotateLeft} />
</button>
</Tooltip>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -151,31 +294,199 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("certificate", {
serialNumber: certificate.serialNumber
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEye} />}
{isLegacyTemplatesEnabled && (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("certificate", {
serialNumber: certificate.serialNumber
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEye} />}
>
View Details
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
{/* Manage auto renewal option - not shown for failed renewals */}
{(() => {
const canManageRenewal =
certificate.profileId &&
certificate.hasPrivateKey !== false &&
!certificate.renewedByCertificateId &&
!isRevoked &&
!isExpired &&
!hasFailed &&
!isExpiringWithinDay;
if (!canManageRenewal) return null;
return (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates}
>
View Details
</DropdownMenuItem>
{(isAllowed) => {
return (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
const notAfterDate = new Date(certificate.notAfter);
const notBeforeDate = certificate.notBefore
? new Date(certificate.notBefore)
: new Date(
notAfterDate.getTime() - 365 * 24 * 60 * 60 * 1000
);
const ttlDays = Math.max(
1,
Math.ceil(
(notAfterDate.getTime() - notBeforeDate.getTime()) /
(24 * 60 * 60 * 1000)
)
);
handlePopUpOpen("manageRenewal", {
certificateId: certificate.id,
commonName: certificate.commonName,
profileId: certificate.profileId,
renewBeforeDays: certificate.renewBeforeDays,
ttlDays,
notAfter: certificate.notAfter,
renewalError: certificate.renewalError,
renewedFromCertificateId:
certificate.renewedFromCertificateId,
renewedByCertificateId: certificate.renewedByCertificateId
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faRedo} />}
>
{isAutoRenewalEnabled
? "Manage auto renewal"
: "Enable auto renewal"}
</DropdownMenuItem>
);
}}
</ProjectPermissionCan>
);
})()}
{/* Disable auto renewal option - only shown when auto renewal is active */}
{(() => {
const canDisableRenewal =
certificate.profileId &&
certificate.hasPrivateKey !== false &&
!certificate.renewedByCertificateId &&
!isRevoked &&
!isExpired &&
!isExpiringWithinDay &&
isAutoRenewalEnabled;
if (!canDisableRenewal) return null;
return (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
await handleDisableAutoRenewal(
certificate.id,
certificate.commonName
);
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faBan} />}
>
Disable auto renewal
</DropdownMenuItem>
)}
</ProjectPermissionCan>
);
})()}
{/* Manual renewal action for profile-issued certificates that are not revoked/expired (including failed ones) */}
{(() => {
const canRenew =
certificate.profileId &&
certificate.hasPrivateKey !== false &&
!certificate.renewedByCertificateId &&
!isRevoked &&
!isExpired;
if (!canRenew) return null;
return (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("renewCertificate", {
certificateId: certificate.id,
commonName: certificate.commonName
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faRedo} />}
>
Renew Now
</DropdownMenuItem>
)}
</ProjectPermissionCan>
);
})()}
{/* PKI Sync management - only for active certificates that are not renewed */}
{certificate.status === CertStatus.ACTIVE &&
!certificate.renewedByCertificateId && (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("managePkiSyncs", {
certificateId: certificate.id,
commonName: certificate.commonName
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faLink} />}
>
Manage PKI Syncs
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
</ProjectPermissionCan>
{/* Only show revoke button if CA supports revocation */}
{(() => {
const caType = caCapabilityMap[certificate.caId];
// If caId not found in map, assume CA supports revocation to avoid hiding revoke option
const supportsRevocation =
!caType ||
caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES);
@@ -5,13 +5,13 @@ export const getCertValidUntilBadgeDetails = (notAfter: string) => {
const notAfterDate = new Date(notAfter).getTime();
const diffInMs = notAfterDate - currentDate;
let variant: "success" | "primary" | "danger" = "success";
let variant: "success" | "warning" | "danger" = "success";
let label = "Healthy";
if (diffInMs > ms("60d")) {
variant = "success";
} else if (diffInMs > ms("30d")) {
variant = "primary";
variant = "warning";
} else {
variant = "danger";
}
@@ -11,6 +11,26 @@ import {
mapTemplateSignatureAlgorithmToApi
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
const convertTemplateTtlToCertificateTtl = (templateTtl: string): string => {
const match = templateTtl.match(/^(\d+)([dmyh])$/);
if (!match) return templateTtl;
const [, value, unit] = match;
const numValue = parseInt(value, 10);
switch (unit) {
case "m":
return `${numValue * 30}d`;
case "y":
return `${numValue * 365}d`;
case "d":
case "h":
return templateTtl;
default:
return templateTtl;
}
};
export type TemplateConstraints = {
allowedKeyUsages: string[];
allowedExtendedKeyUsages: string[];
@@ -118,7 +138,7 @@ export const useCertificateTemplate = (
// Set TTL if available
if (templateData.validity?.max) {
setValue("ttl", templateData.validity.max);
setValue("ttl", convertTemplateTtlToCertificateTtl(templateData.validity.max));
}
// Handle SAN types
@@ -1,19 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { CertificatesPage } from "./CertificatesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
)({
component: CertificatesPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificates"
}
]
};
}
});
@@ -1,7 +1,6 @@
import { useCallback, useMemo } from "react";
import { subject } from "@casl/ability";
import {
faBan,
faCalendarCheck,
faCheck,
faCopy,
@@ -13,12 +12,12 @@ import {
faToggleOff,
faToggleOn,
faTrash,
faTriangleExclamation,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns";
import { BanIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
@@ -29,7 +28,6 @@ import {
PkiSyncStatusBadge
} from "@app/components/pki-syncs";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -39,6 +37,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
@@ -47,7 +46,6 @@ import { useToggle } from "@app/hooks";
import { PkiSyncStatus, TPkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
import { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
import { PkiSyncTableCell } from "./PkiSyncTableCell";
type Props = {
pkiSync: TPkiSync;
@@ -163,26 +161,6 @@ export const PkiSyncRow = ({
<p className="truncate text-xs leading-4 text-bunker-300">{destinationDetails.name}</p>
</div>
</Td>
{subscriberId ? (
<PkiSyncTableCell
primaryText={pkiSync.subscriber?.name || subscriberId}
secondaryText="PKI Subscriber"
/>
) : (
<Td>
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">
<div className="w-min">
<Badge
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
variant="primary"
>
<FontAwesomeIcon icon={faTriangleExclamation} />
<span>Source Deleted</span>
</Badge>
</div>
</Tooltip>
</Td>
)}
<PkiSyncDestinationCol pkiSync={pkiSync} />
<Td>
<div className="flex items-center gap-1">
@@ -234,12 +212,10 @@ export const PkiSyncRow = ({
className="text-xs"
content="Auto-Sync is disabled. Certificate changes in the PKI subscriber will not be automatically synced to the destination."
>
<div>
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faBan} />
{!syncStatus && "Auto-Sync Disabled"}
</Badge>
</div>
<Badge variant="neutral">
<BanIcon />
{!syncStatus && "Auto-Sync Disabled"}
</Badge>
</Tooltip>
)}
{syncOption?.canImportCertificates && <PkiSyncImportStatusBadge mini pkiSync={pkiSync} />}
@@ -57,7 +57,6 @@ import { PkiSyncRow } from "./PkiSyncRow";
enum PkiSyncsOrderBy {
Destination = "destination",
Source = "source",
Name = "name",
Status = "status"
}
@@ -160,14 +159,6 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
const [syncOne, syncTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
switch (orderBy) {
case PkiSyncsOrderBy.Source:
return (syncOne.subscriber?.name ?? syncOne.subscriberId ?? "")
.toLowerCase()
.localeCompare(
syncTwo.subscriber?.name?.toLowerCase() ??
syncTwo.subscriberId?.toLowerCase() ??
""
);
case PkiSyncsOrderBy.Destination:
return getPkiSyncDestinationColValues(syncOne)
.primaryText.toLowerCase()
@@ -370,7 +361,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
<THead>
<Tr>
<Th className="w-2" />
<Th className="w-1/4">
<Th className="w-1/2">
<div className="flex items-center">
Name
<IconButton
@@ -383,20 +374,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
</IconButton>
</div>
</Th>
<Th className="w-1/3">
<div className="flex items-center">
Source
<IconButton
variant="plain"
className={getClassName(PkiSyncsOrderBy.Source)}
ariaLabel="sort"
onClick={() => handleSort(PkiSyncsOrderBy.Source)}
>
<FontAwesomeIcon icon={getColSortIcon(PkiSyncsOrderBy.Source)} />
</IconButton>
</div>
</Th>
<Th className="w-1/3">
<Th className="w-1/4">
<div className="flex items-center">
Destination
<IconButton
@@ -409,7 +387,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
</IconButton>
</div>
</Th>
<Th className="min-w-42">
<Th className="w-1/4 min-w-42">
<div className="flex items-center">
Status
<IconButton
@@ -1,23 +1,25 @@
import { useCallback, useEffect, useMemo } from "react";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { CreatePkiSyncModal } from "@app/components/pki-syncs";
import { Button, Spinner } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub, useProject } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks";
import { useListPkiSyncs } from "@app/hooks/api/pkiSyncs";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import { PkiSyncsTable } from "./PkiSyncTable";
export const PkiSyncsTab = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const);
const { addSync, ...search } = useSearch({
const { addSync, connectionId, connectionName, ...search } = useSearch({
from: ROUTE_PATHS.CertManager.IntegrationsListPage.id
});
@@ -44,6 +46,42 @@ export const PkiSyncsTab = () => {
navigateToBase();
}, [addSync, handlePopUpOpen, navigateToBase]);
useEffect(() => {
const storedFormData = localStorage.getItem("pkiSyncFormData");
if (storedFormData && !popUp.addSync.isOpen) {
try {
const parsedData = JSON.parse(storedFormData);
if (connectionId && connectionName) {
const initialData = {
...parsedData,
connection: { id: connectionId, name: connectionName }
};
handlePopUpOpen("addSync", { destination: parsedData.destination, initialData });
navigate({
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
params: { projectId: currentProject?.id },
search: { selectedTab: IntegrationsListPageTabs.PkiSyncs },
replace: true
});
} else {
handlePopUpOpen("addSync", { destination: parsedData.destination });
}
localStorage.removeItem("pkiSyncFormData");
} catch (error) {
console.error("Failed to parse stored PKI sync form data:", error);
localStorage.removeItem("pkiSyncFormData");
handlePopUpOpen("addSync");
}
}
}, [
handlePopUpOpen,
popUp.addSync.isOpen,
connectionId,
connectionName,
navigate,
currentProject?.id
]);
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
currentProject?.id || "",
{
@@ -64,21 +102,10 @@ export const PkiSyncsTab = () => {
<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">Certificate Syncs</p>
<a
href="https://infisical.com/docs/integrations/pki-syncs/overview"
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 className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Certificate Syncs</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/pki/certificate-syncs/overview" />
</div>
</div>
<p className="text-sm text-bunker-300">
Use App Connections to sync certificates to third-party services.
@@ -104,7 +131,8 @@ export const PkiSyncsTab = () => {
<PkiSyncsTable pkiSyncs={pkiSyncs} />
</div>
<CreatePkiSyncModal
selectSync={popUp.addSync.data}
selectSync={popUp.addSync.data?.destination || popUp.addSync.data}
initialData={popUp.addSync.data?.initialData}
isOpen={popUp.addSync.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addSync", isOpen)}
/>
@@ -9,7 +9,9 @@ import { IntegrationsListPage } from "./IntegrationsListPage";
const IntegrationsListPageQuerySchema = z.object({
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(),
addSync: z.nativeEnum(PkiSync).optional()
addSync: z.nativeEnum(PkiSync).optional(),
connectionId: z.string().optional(),
connectionName: z.string().optional()
});
export const Route = createFileRoute(
@@ -59,9 +59,9 @@ export const PkiCollectionPage = () => {
});
handlePopUpClose("deletePkiCollection");
navigate({
to: "/projects/cert-management/$projectId/certificates",
to: "/projects/cert-management/$projectId/policies",
params: {
projectId
projectId: params.projectId
}
});
} catch {
@@ -77,9 +77,9 @@ export const PkiCollectionPage = () => {
{data && (
<div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to="/projects/cert-management/$projectId/certificates"
to="/projects/cert-management/$projectId/policies"
params={{
projectId
projectId: params.projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
@@ -5,13 +5,9 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useProject } from "@app/context";
import {
CaStatus,
useAddItemToPkiCollection,
useListWorkspaceCas,
useListWorkspaceCertificates
} from "@app/hooks/api";
import { CaStatus, useAddItemToPkiCollection, useListWorkspaceCas } from "@app/hooks/api";
import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
{
label: "Certificate Collections",
link: linkOptions({
to: "/projects/cert-management/$projectId/certificates",
to: "/projects/cert-management/$projectId/policies",
params: {
projectId: params.projectId
}
@@ -7,7 +7,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -24,6 +23,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub,
@@ -93,7 +93,7 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
}
if (daysUntilExpiry < 30) {
return <Badge variant="primary">Expiring Soon</Badge>;
return <Badge variant="warning">Expiring Soon</Badge>;
}
return <Badge variant="success">Valid</Badge>;
@@ -351,8 +351,14 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
commonName,
subjectAlternativeNames: subjectAlternativeNamesList,
ttl,
keyUsages: keyUsagesList,
extendedKeyUsages: extendedKeyUsagesList,
keyUsages: keyUsagesList.map((key) =>
key === CertKeyUsage.CRL_SIGN
? "cRLSign"
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
extendedKeyUsages: extendedKeyUsagesList.map((key) =>
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
enableAutoRenewal,
autoRenewalPeriodInDays,
properties: Object.keys(properties).length > 0 ? properties : undefined
@@ -365,8 +371,14 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
commonName,
subjectAlternativeNames: subjectAlternativeNamesList,
ttl,
keyUsages: keyUsagesList,
extendedKeyUsages: extendedKeyUsagesList,
keyUsages: keyUsagesList.map((key) =>
key === CertKeyUsage.CRL_SIGN
? "cRLSign"
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
extendedKeyUsages: extendedKeyUsagesList.map((key) =>
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
enableAutoRenewal,
autoRenewalPeriodInDays,
properties: Object.keys(properties).length > 0 ? properties : undefined
@@ -7,8 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub,
useProject,
useSubscription
useProject
} from "@app/context";
import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api";
import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types";
@@ -19,10 +18,10 @@ import { PkiSubscribersTable } from "./PkiSubscribersTable";
export const PkiSubscriberSection = () => {
const { currentProject } = useProject();
const { subscription } = useSubscription();
const projectId = currentProject.id;
const canCreateLegacySubscribers = subscription.pkiLegacyTemplates;
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
const canCreateLegacySubscribers = true;
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
@@ -11,7 +11,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -27,6 +26,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub,
@@ -18,10 +18,10 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
import {
PkiSyncActionTriggers,
PkiSyncAuditLogsSection,
PkiSyncCertificatesSection,
PkiSyncDestinationSection,
PkiSyncDetailsSection,
PkiSyncOptionsSection,
PkiSyncSourceSection
PkiSyncOptionsSection
} from "./components";
const PageContent = () => {
@@ -62,7 +62,6 @@ const PageContent = () => {
const destinationDetails = PKI_SYNC_MAP[pkiSync.destination];
const handleEditDetails = () => handlePopUpOpen("editSync", PkiSyncEditFields.Details);
const handleEditSource = () => handlePopUpOpen("editSync", PkiSyncEditFields.Source);
const handleEditOptions = () => handlePopUpOpen("editSync", PkiSyncEditFields.Options);
const handleEditDestination = () => handlePopUpOpen("editSync", PkiSyncEditFields.Destination);
@@ -103,7 +102,6 @@ const PageContent = () => {
<div className="flex justify-center">
<div className="mr-4 flex w-72 flex-col gap-4">
<PkiSyncDetailsSection pkiSync={pkiSync} onEditDetails={handleEditDetails} />
<PkiSyncSourceSection pkiSync={pkiSync} onEditSource={handleEditSource} />
<PkiSyncOptionsSection pkiSync={pkiSync} onEditOptions={handleEditOptions} />
</div>
<div className="flex flex-1 flex-col gap-4">
@@ -111,6 +109,7 @@ const PageContent = () => {
pkiSync={pkiSync}
onEditDestination={handleEditDestination}
/>
<PkiSyncCertificatesSection pkiSync={pkiSync} />
<PkiSyncAuditLogsSection pkiSync={pkiSync} />
</div>
</div>
@@ -1,7 +1,6 @@
import { useCallback } from "react";
import { subject } from "@casl/ability";
import {
faBan,
faCheck,
faCopy,
faDownload,
@@ -15,6 +14,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { BanIcon, RefreshCwIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
@@ -26,7 +26,6 @@ import {
PkiSyncRemoveStatusBadge
} from "@app/components/pki-syncs";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
@@ -35,6 +34,7 @@ import {
IconButton,
Tooltip
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
@@ -136,24 +136,19 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
{syncOption?.canImportCertificates && <PkiSyncImportStatusBadge pkiSync={pkiSync} />}
<PkiSyncRemoveStatusBadge pkiSync={pkiSync} />
{pkiSync.isAutoSyncEnabled ? (
<Badge
variant="success"
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faRotate} />
<span>Auto-Sync Enabled</span>
<Badge variant="info">
<RefreshCwIcon />
Auto-Sync Enabled
</Badge>
) : (
<Tooltip
className="text-xs"
content="Auto-Sync is disabled. Certificate changes in the PKI subscriber will not be automatically synced to the destination."
>
<div>
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faBan} />
<span>Auto-Sync Disabled</span>
</Badge>
</div>
<Badge variant="neutral">
<BanIcon />
Auto-Sync Disabled
</Badge>
</Tooltip>
)}
<div>
@@ -26,7 +26,7 @@ export const PkiSyncAuditLogsSection = ({ pkiSync }: Props) => {
return (
<div className="flex max-h-full w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-medium text-mineshaft-100">Sync Logs</h3>
<h3 className="text-lg font-medium text-mineshaft-100">Sync Logs</h3>
{subscription.auditLogs && (
<p className="text-xs text-bunker-300">
Displaying audit logs from the last {Math.min(auditLogsRetentionDays, 60)} days
@@ -0,0 +1,328 @@
import { useState } from "react";
import { subject } from "@casl/ability";
import {
faCertificate,
faClockRotateLeft,
faEdit,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { CertificateManagementModal } from "@app/components/pki-syncs/CertificateManagementModal";
import {
CertificateDisplayName,
getCertificateDisplayName
} from "@app/components/utilities/certificateDisplayUtils";
import {
DeleteActionModal,
EmptyState,
IconButton,
Pagination,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { useListPkiSyncCertificates, useRemoveCertificatesFromPkiSync } from "@app/hooks/api";
import { CertificateSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: TPkiSync;
};
const getSyncStatusVariant = (status?: CertificateSyncStatus | null) => {
if (status === CertificateSyncStatus.Succeeded) return "success";
if (status === CertificateSyncStatus.Failed) return "danger";
if (status === CertificateSyncStatus.Syncing) return "neutral";
return "project";
};
const getSyncStatusText = (status?: CertificateSyncStatus | null) => {
if (status === CertificateSyncStatus.Succeeded) return "Synced";
if (status === CertificateSyncStatus.Failed) return "Failed";
if (status === CertificateSyncStatus.Syncing) return "Syncing";
if (status === CertificateSyncStatus.Pending) return "Pending";
return "Unknown";
};
const getCertificateStatusVariant = (isExpired: boolean, isRevoked: boolean) => {
if (isRevoked) return "danger";
if (isExpired) return "danger";
return "success";
};
const getCertificateStatusText = (isExpired: boolean, isRevoked: boolean) => {
if (isRevoked) return "Revoked";
if (isExpired) return "Expired";
return "Active";
};
export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
const [isManageModalOpen, setIsManageModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [certificateToDelete, setCertificateToDelete] = useState<{
id: string;
displayName: string;
} | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;
const { data, refetch: refetchSyncCertificates } = useListPkiSyncCertificates(pkiSync.id, {
offset: (currentPage - 1) * pageSize,
limit: pageSize
});
const syncCertificates = data?.certificates || [];
const totalCount = data?.totalCount || 0;
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: pkiSync.subscriberId || ""
});
const handleRemoveCertificate = async (certificateId: string) => {
try {
await removeCertificatesFromSync.mutateAsync({
pkiSyncId: pkiSync.id,
certificateIds: [certificateId]
});
await refetchSyncCertificates();
createNotification({
text: "Certificate removed from sync",
type: "success"
});
setIsDeleteModalOpen(false);
setCertificateToDelete(null);
} catch {
createNotification({
text: "Failed to remove certificate from sync",
type: "error"
});
}
};
const handleDeleteClick = (certificateId: string, displayName: string) => {
setCertificateToDelete({ id: certificateId, displayName });
setIsDeleteModalOpen(true);
};
const totalPages = Math.ceil(totalCount / pageSize);
return (
<div>
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="text-lg font-medium text-mineshaft-100">Certificates</h3>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit certificates"
onClick={() => setIsManageModalOpen(true)}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div>
<div className="space-y-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-2/8">SAN / CN</Th>
<Th className="w-3/16">Certificate Status</Th>
<Th className="w-3/16">Serial Number</Th>
<Th className="w-1/8">Sync Status</Th>
<Th className="w-1/8">Expires At</Th>
<Th className="w-1/8" />
</Tr>
</THead>
<TBody>
{syncCertificates.map((syncCert) => {
const isExpired = syncCert.certificateNotAfter
? new Date(syncCert.certificateNotAfter) < new Date()
: false;
const isRevoked = syncCert.certificateStatus === "revoked";
// Calculate auto-renewal timeline
const hasAutoRenewal = Boolean(
syncCert.certificateRenewBeforeDays &&
syncCert.certificateRenewBeforeDays > 0 &&
!syncCert.certificateRenewalError &&
syncCert.certificateNotAfter
);
const daysUntilRenewal =
hasAutoRenewal && syncCert.certificateNotAfter
? (() => {
const expiryDate = new Date(syncCert.certificateNotAfter);
const renewalDate = new Date(
expiryDate.getTime() -
syncCert.certificateRenewBeforeDays! * 24 * 60 * 60 * 1000
);
const now = new Date();
const diffInMs = renewalDate.getTime() - now.getTime();
return Math.max(0, Math.ceil(diffInMs / (24 * 60 * 60 * 1000)));
})()
: null;
const { originalDisplayName } = getCertificateDisplayName(
{
altNames: syncCert.certificateAltNames,
commonName: syncCert.certificateCommonName
},
34,
"Unknown"
);
return (
<Tr key={syncCert.id}>
<Td className="max-w-0">
<CertificateDisplayName
cert={{
altNames: syncCert.certificateAltNames,
commonName: syncCert.certificateCommonName
}}
maxLength={34}
fallback="Unknown"
/>
</Td>
<Td>
<Badge variant={getCertificateStatusVariant(isExpired, isRevoked)}>
{getCertificateStatusText(isExpired, isRevoked)}
</Badge>
</Td>
<Td className="max-w-0">
<div
className="truncate text-xs"
title={syncCert.certificateSerialNumber || "Unknown"}
>
{(() => {
const serial = syncCert.certificateSerialNumber;
if (!serial || serial === "Unknown") return "Unknown";
if (serial.length <= 8) return serial;
return `${serial.substring(0, 4)}...${serial.substring(serial.length - 4)}`;
})()}
</div>
</Td>
<Td>
{syncCert.lastSyncMessage &&
syncCert.syncStatus === CertificateSyncStatus.Failed ? (
<Tooltip content={syncCert.lastSyncMessage}>
<Badge variant="danger">Failed</Badge>
</Tooltip>
) : (
<Badge variant={getSyncStatusVariant(syncCert.syncStatus)}>
{getSyncStatusText(syncCert.syncStatus)}
</Badge>
)}
</Td>
<Td>
<span
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
>
{syncCert.certificateNotAfter
? new Date(syncCert.certificateNotAfter).toLocaleDateString()
: "Unknown"}
</span>
</Td>
<Td className="flex items-center justify-end gap-2 pr-4">
{hasAutoRenewal && daysUntilRenewal !== null && (
<Tooltip content={`Auto-renews in ${daysUntilRenewal}d`}>
<div className="text-primary-500">
<FontAwesomeIcon icon={faClockRotateLeft} size="sm" />
</div>
</Tooltip>
)}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Edit}
a={permissionSubject}
>
{(isAllowed) => (
<IconButton
size="xs"
variant="plain"
colorSchema="danger"
ariaLabel="Remove certificate"
isDisabled={!isAllowed}
onClick={() =>
handleDeleteClick(syncCert.certificateId, originalDisplayName)
}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</ProjectPermissionCan>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{syncCertificates.length === 0 && (
<EmptyState
title="No certificates are part of this certificate sync"
icon={faCertificate}
/>
)}
</TableContainer>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center">
<Pagination
count={totalCount}
page={currentPage}
perPage={pageSize}
onChangePage={(page: number) => setCurrentPage(page)}
onChangePerPage={() => {}}
/>
</div>
)}
</div>
</div>
</div>
<CertificateManagementModal
pkiSync={pkiSync}
isOpen={isManageModalOpen}
onClose={() => setIsManageModalOpen(false)}
onCertificatesUpdated={() => {
refetchSyncCertificates();
}}
/>
<DeleteActionModal
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false);
setCertificateToDelete(null);
}}
title="Remove Certificate from Sync"
subTitle={`Are you sure you want to remove "${certificateToDelete?.displayName}" from this PKI sync?`}
deleteKey="confirm"
onDeleteApproved={async () => {
if (certificateToDelete) {
await handleRemoveCertificate(certificateToDelete.id);
}
}}
buttonText="Remove Certificate"
/>
</div>
);
};
@@ -11,12 +11,15 @@ import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionC
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
import { AzureKeyVaultPkiSyncDestinationSection } from "./PkiSyncDestinationSection/index";
import {
AwsCertificateManagerPkiSyncDestinationSection,
AzureKeyVaultPkiSyncDestinationSection
} from "./PkiSyncDestinationSection/index";
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
<div>
<label className="text-sm text-bunker-300">{label}</label>
<div className="mt-1">{children}</div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
<div className="text-sm text-mineshaft-300">{children}</div>
</div>
);
@@ -32,6 +35,9 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
let DestinationComponents: ReactNode;
switch (destination) {
case PkiSync.AwsCertificateManager:
DestinationComponents = <AwsCertificateManagerPkiSyncDestinationSection pkiSync={pkiSync} />;
break;
case PkiSync.AzureKeyVault:
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
break;
@@ -47,7 +53,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-medium text-mineshaft-100">Destination Configuration</h3>
<h3 className="text-lg font-medium text-mineshaft-100">Destination Configuration</h3>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
@@ -62,7 +68,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
)}
</ProjectPermissionCan>
</div>
<div className="flex w-full flex-wrap gap-8">
<div className="flex w-full flex-wrap gap-8 pt-2">
<GenericFieldLabel label={`${destinationDetails.name} Connection`}>
{pkiSync.appConnectionName || "Default Connection"}
</GenericFieldLabel>
@@ -0,0 +1,21 @@
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
<div className="text-sm text-mineshaft-300">{children}</div>
</div>
);
type Props = {
pkiSync: TPkiSync;
};
export const AwsCertificateManagerPkiSyncDestinationSection = ({ pkiSync }: Props) => {
const region =
pkiSync.destinationConfig && "region" in pkiSync.destinationConfig
? pkiSync.destinationConfig.region
: undefined;
return <GenericFieldLabel label="AWS Region">{region || "Not specified"}</GenericFieldLabel>;
};
@@ -2,9 +2,9 @@
import { TAzureKeyVaultPkiSync } from "@app/hooks/api/pkiSyncs/types/azure-key-vault-sync";
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
<div>
<label className="text-sm text-bunker-300">{label}</label>
<div className="mt-1">{children}</div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
<div className="text-sm text-mineshaft-300">{children}</div>
</div>
);
@@ -1 +1,2 @@
export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection";
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
@@ -21,9 +21,9 @@ const GenericFieldLabel = ({
children: React.ReactNode;
labelClassName?: string;
}) => (
<div>
<label className={`text-sm text-bunker-300 ${labelClassName || ""}`}>{label}</label>
<div className="mt-1">{children}</div>
<div className="mb-4">
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
<div className="text-sm text-mineshaft-300">{children}</div>
</div>
);
@@ -57,7 +57,7 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-medium text-mineshaft-100">Details</h3>
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
@@ -72,31 +72,27 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
)}
</ProjectPermissionCan>
</div>
<div>
<div className="space-y-3">
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
<GenericFieldLabel label="Description">{description || "None"}</GenericFieldLabel>
<GenericFieldLabel label="Source Subscriber">
{subscriber ? subscriber.name : "Subscriber deleted"}
<div className="pt-2">
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
<GenericFieldLabel label="Description">{description || "None"}</GenericFieldLabel>
{subscriber && (
<GenericFieldLabel label="Source Subscriber">{subscriber.name}</GenericFieldLabel>
)}
{syncStatus && (
<GenericFieldLabel label="Status">
<PkiSyncStatusBadge status={syncStatus} />
</GenericFieldLabel>
{syncStatus && (
<GenericFieldLabel label="Status">
<PkiSyncStatusBadge status={syncStatus} />
</GenericFieldLabel>
)}
{lastSyncedAt && (
<GenericFieldLabel label="Last Synced">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
</GenericFieldLabel>
)}
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
{failureMessage}
</p>
</GenericFieldLabel>
)}
</div>
)}
{lastSyncedAt && (
<GenericFieldLabel label="Last Synced">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
</GenericFieldLabel>
)}
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">{failureMessage}</p>
</GenericFieldLabel>
)}
</div>
</div>
);
@@ -3,12 +3,27 @@ import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { Badge, IconButton } from "@app/components/v2";
import { IconButton } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({
label,
children,
labelClassName
}: {
label: string;
children: React.ReactNode;
labelClassName?: string;
}) => (
<div className="mb-4">
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
<div className="text-sm text-mineshaft-300">{children}</div>
</div>
);
type Props = {
pkiSync: TPkiSync;
onEditOptions: VoidFunction;
@@ -27,7 +42,7 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
<div>
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-medium text-mineshaft-100">Sync Options</h3>
<h3 className="text-lg font-medium text-mineshaft-100">Sync Options</h3>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
@@ -42,21 +57,19 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
)}
</ProjectPermissionCan>
</div>
<div>
<div className="space-y-3">
{/* Hidden for now - Import certificates functionality disabled
<div className="pt-1">
{/* Hidden for now - Import certificates functionality disabled
<GenericFieldLabel label="Certificate Import">
<Badge variant={canImportCertificates ? "success" : "danger"}>
{canImportCertificates ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
*/}
<GenericFieldLabel label="Certificate Removal">
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
{canRemoveCertificates ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
</div>
<GenericFieldLabel label="Inactive Certificate Removal" labelClassName="mb-1">
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
{canRemoveCertificates ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
</div>
</div>
</div>
@@ -1,10 +1,12 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { subject } from "@casl/ability";
import { faEdit, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AlertTriangleIcon } from "lucide-react";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Badge, IconButton, Tooltip } from "@app/components/v2";
import { IconButton, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
@@ -36,15 +38,10 @@ export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
<div>
{!subscriberId && (
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">
<div className="mr-1 inline-block w-min">
<Badge
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
variant="primary"
>
<FontAwesomeIcon icon={faTriangleExclamation} />
<span>Source Deleted</span>
</Badge>
</div>
<Badge variant="danger" className="mr-1">
<AlertTriangleIcon />
Source Deleted
</Badge>
</Tooltip>
)}
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
@@ -1,5 +1,6 @@
export { PkiSyncActionTriggers } from "./PkiSyncActionTriggers";
export { PkiSyncAuditLogsSection } from "./PkiSyncAuditLogsSection";
export { PkiSyncCertificatesSection } from "./PkiSyncCertificatesSection";
export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection";
export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection";
export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection";
@@ -114,30 +114,29 @@ export const PkiTemplateListPage = () => {
/>
</div>
<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>
<div className="flex w-full justify-end">
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("certificateTemplate")}
isDisabled={!isAllowed}
className="ml-4"
>
Add Template
</Button>
)}
</ProjectPermissionCan>
</div>
{/* TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation */}
<div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
<div className="flex w-full justify-end">
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("certificateTemplate")}
isDisabled={!isAllowed}
className="ml-4"
>
Add Template
</Button>
)}
</ProjectPermissionCan>
</div>
)}
</div>
<TableContainer>
<Table>
<THead>
@@ -203,7 +202,9 @@ export const PkiTemplateListPage = () => {
onClick={(e) => {
e.stopPropagation();
if (!subscription.pkiEst) {
handlePopUpOpen("estUpgradePlan");
handlePopUpOpen("estUpgradePlan", {
isEnterpriseFeature: true
});
return;
}
handlePopUpOpen("enrollmentOptions", {
@@ -295,7 +296,8 @@ export const PkiTemplateListPage = () => {
<UpgradePlanModal
isOpen={popUp.estUpgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("estUpgradePlan", isOpen)}
text="You can only configure template enrollment methods if you switch to Infisical's Enterprise plan."
text="Your current plan does not include access to configuring template enrollment methods. To unlock this feature, please upgrade to Infisical Enterprise plan."
isEnterpriseFeature={popUp.estUpgradePlan.data?.isEnterpriseFeature}
/>
</>
);
@@ -140,10 +140,14 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
.map(([key]) =>
key === CertKeyUsage.CRL_SIGN
? "cRLSign"
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
});
createNotification({
@@ -160,10 +164,14 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
.map(([key]) =>
key === CertKeyUsage.CRL_SIGN
? "cRLSign"
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
});
createNotification({
@@ -2,17 +2,20 @@ import { useState } from "react";
import { Helmet } from "react-helmet";
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 { useProject } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
import { CertificatesTab } from "./components/CertificatesTab";
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
import { PkiCollectionsTab } from "./components/PkiCollectionsTab";
enum TabSections {
CertificateProfiles = "profiles",
CertificateTemplatesV2 = "templates-v2"
CertificateTemplatesV2 = "templates-v2",
Certificates = "certificates",
PkiCollections = "pki-collections"
}
export const PoliciesPage = () => {
@@ -25,59 +28,54 @@ export const PoliciesPage = () => {
}
return (
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => {
if (!isAllowed) {
return (
<div className="mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl text-center">
<p>You don&apos;t have permission to access certificate policies.</p>
</div>
</div>
);
}
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificate Management" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope={ProjectType.CertificateManager}
title="Certificate Management"
description="Streamline certificate management by creating and maintaining templates, profiles, and certificates in one place"
/>
return (
<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-8xl">
<PageHeader
scope={ProjectType.CertificateManager}
title="Certificate Policies"
description="Manage certificate templates and profiles for unified certificate issuance"
/>
<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>
<Tab variant="project" value={TabSections.Certificates}>
Certificates
</Tab>
<Tab variant="project" value={TabSections.PkiCollections}>
Certificate Collections
</Tab>
</TabList>
<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}>
<CertificateProfilesTab />
</TabPanel>
<TabPanel value={TabSections.CertificateProfiles}>
<CertificateProfilesTab />
</TabPanel>
<TabPanel value={TabSections.CertificateTemplatesV2}>
<CertificateTemplatesV2Tab />
</TabPanel>
<TabPanel value={TabSections.CertificateTemplatesV2}>
<CertificateTemplatesV2Tab />
</TabPanel>
</Tabs>
</div>
</div>
);
}}
</ProjectPermissionCan>
<TabPanel value={TabSections.Certificates}>
<CertificatesTab />
</TabPanel>
<TabPanel value={TabSections.PkiCollections}>
<PkiCollectionsTab />
</TabPanel>
</Tabs>
</div>
</div>
);
};
@@ -1,5 +1,7 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -13,7 +15,8 @@ import {
ModalContent,
Select,
SelectItem,
TextArea
TextArea,
Tooltip
} from "@app/components/v2";
import { useProject } from "@app/context";
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
@@ -67,7 +70,7 @@ const createSchema = z
apiConfig: z
.object({
autoRenew: z.boolean().optional(),
autoRenewDays: z.number().min(1).max(365).optional()
renewBeforeDays: z.number().min(1).max(365).optional()
})
.optional()
})
@@ -115,7 +118,7 @@ const editSchema = z
apiConfig: z
.object({
autoRenew: z.boolean().optional(),
autoRenewDays: z.number().min(1).max(365).optional()
renewBeforeDays: z.number().min(1).max(365).optional()
})
.optional()
})
@@ -183,7 +186,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
profile.enrollmentType === "api"
? {
autoRenew: profile.apiConfig?.autoRenew || false,
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
}
: undefined
}
@@ -195,7 +198,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
certificateTemplateId: "",
apiConfig: {
autoRenew: false,
autoRenewDays: 30
renewBeforeDays: 30
}
}
});
@@ -225,7 +228,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
profile.enrollmentType === "api"
? {
autoRenew: profile.apiConfig?.autoRenew || false,
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
}
: undefined
});
@@ -389,7 +392,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
} else {
setValue("apiConfig", {
autoRenew: false,
autoRenewDays: 30
renewBeforeDays: 30
});
setValue("estConfig", undefined);
}
@@ -415,7 +418,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
name="enrollmentType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Enrollment Type"
label="Enrollment Method"
isRequired
isError={Boolean(error)}
errorText={error?.message}
@@ -433,7 +436,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
setValue("estConfig", undefined);
setValue("apiConfig", {
autoRenew: false,
autoRenewDays: 30
renewBeforeDays: 30
});
}
onChange(value);
@@ -535,9 +538,18 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
name="apiConfig.autoRenew"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Checkbox id="autoRenew" isChecked={value} onCheckedChange={onChange}>
Enable Auto-Renewal
</Checkbox>
<div className="flex items-center gap-2">
<Checkbox id="autoRenew" isChecked={value} onCheckedChange={onChange}>
Enable Auto-Renewal By Default
</Checkbox>
<Tooltip content="If enabled, certificates issued against this profile will auto-renew at specified days before expiration.">
<FontAwesomeIcon
icon={faQuestionCircle}
className="cursor-help text-mineshaft-400 hover:text-mineshaft-300"
size="sm"
/>
</Tooltip>
</div>
</FormControl>
)}
/>
@@ -548,10 +560,10 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
<div className="mb-4 space-y-4">
<Controller
control={control}
name="apiConfig.autoRenewDays"
name="apiConfig.renewBeforeDays"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Auto-Renewal Days"
label="Auto-Renewal Days Before Expiration"
isError={Boolean(error)}
errorText={error?.message}
>
@@ -29,8 +29,7 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
projectId: currentProject?.id || "",
limit: 100,
offset: 0,
includeConfigs: true,
includeMetrics: true
includeConfigs: true
});
const profiles = data?.certificateProfiles || [];
@@ -42,10 +41,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
<THead>
<Tr>
<Th>Name</Th>
<Th>Enrollment Type</Th>
<Th>Enrollment Method</Th>
<Th>Issuing CA</Th>
<Th>Certificate Template</Th>
<Th>Certificates</Th>
<Th className="w-5" />
</Tr>
</THead>
@@ -67,10 +65,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
<THead>
<Tr>
<Th>Name</Th>
<Th>Enrollment Type</Th>
<Th>Enrollment Method</Th>
<Th>Issuing CA</Th>
<Th>Certificate Template</Th>
<Th>Certificates</Th>
<Th className="w-5" />
</Tr>
</THead>
@@ -12,7 +12,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -21,6 +20,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useProjectPermission } from "@app/context";
import {
ProjectPermissionActions,
@@ -33,59 +33,6 @@ import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
const MetricsBadges = ({
metrics
}: {
metrics?: {
totalCertificates: number;
activeCertificates: number;
expiringCertificates: number;
expiredCertificates: number;
revokedCertificates: number;
};
}) => {
if (!metrics) {
return (
<Badge variant="primary" className="text-xs">
No metrics
</Badge>
);
}
if (metrics.totalCertificates === 0) {
return (
<Badge variant="primary" className="text-xs">
No certificates
</Badge>
);
}
return (
<>
{metrics.activeCertificates > 0 && (
<Badge variant="success" className="text-xs">
{metrics.activeCertificates} active
</Badge>
)}
{metrics.expiringCertificates > 0 && (
<Badge variant="primary" className="text-xs">
{metrics.expiringCertificates} expiring
</Badge>
)}
{metrics.expiredCertificates > 0 && (
<Badge variant="danger" className="text-xs">
{metrics.expiredCertificates} expired
</Badge>
)}
{metrics.revokedCertificates > 0 && (
<Badge variant="danger" className="text-xs">
{metrics.revokedCertificates} revoked
</Badge>
)}
</>
);
};
interface Props {
profile: TCertificateProfile;
onEditProfile: (profile: TCertificateProfile) => void;
@@ -134,8 +81,8 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
const getEnrollmentTypeBadge = (enrollmentType: string) => {
const config = {
api: { variant: "success" as const, label: "API" },
est: { variant: "primary" as const, label: "EST" }
api: { variant: "ghost" as const, label: "API" },
est: { variant: "ghost" as const, label: "EST" }
} as const;
const configKey = Object.keys(config).includes(enrollmentType)
@@ -169,11 +116,6 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
{templateData?.name || profile.certificateTemplateId}
</span>
</Td>
<Td>
<div className="flex flex-wrap gap-1">
<MetricsBadges metrics={profile.metrics} />
</div>
</Td>
<Td className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
@@ -0,0 +1,5 @@
import { CertificatesSection } from "../../../CertificatesPage/components/CertificatesSection";
export const CertificatesTab = () => {
return <CertificatesSection />;
};
@@ -0,0 +1 @@
export { CertificatesTab } from "./CertificatesTab";
@@ -0,0 +1,5 @@
import { PkiCollectionSection } from "../../../AlertingPage/components/PkiCollectionSection";
export const PkiCollectionsTab = () => {
return <PkiCollectionSection />;
};
@@ -0,0 +1 @@
export { PkiCollectionsTab } from "./PkiCollectionsTab";
@@ -1,2 +1,4 @@
export { CertificateProfilesTab } from "./CertificateProfilesTab";
export { CertificatesTab } from "./CertificatesTab";
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
export { PkiCollectionsTab } from "./PkiCollectionsTab";
@@ -170,7 +170,9 @@ export const KmipClientTable = () => {
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (subscription && !subscription.kmip) {
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
});
return;
}
@@ -342,7 +344,8 @@ export const KmipClientTable = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="KMIP requires an enterprise plan."
text="Your current plan does not include access to KMIP. To unlock this feature, please upgrade to Infisical Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
</motion.div>
@@ -23,7 +23,6 @@ import { motion } from "framer-motion";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
@@ -44,7 +43,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { Badge, TBadgeProps } from "@app/components/v3";
import {
ProjectPermissionActions,
ProjectPermissionCmekActions,
@@ -72,7 +71,7 @@ import { DeleteCmekModal } from "./DeleteCmekModal";
const getStatusBadgeProps = (
isDisabled: boolean
): { variant: BadgeProps["variant"]; label: string } => {
): { variant: TBadgeProps["variant"]; label: string } => {
if (isDisabled) {
return {
variant: "danger",
@@ -7,7 +7,6 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { decodeBase64 } from "@app/components/utilities/cryptography/crypto";
import {
Badge,
Button,
FormControl,
Modal,
@@ -19,6 +18,7 @@ import {
TextArea,
Tooltip
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { SigningAlgorithm, TCmek, useCmekVerify } from "@app/hooks/api/cmeks";
import { isBase64 } from "@app/lib/fn/base64";
@@ -112,30 +112,22 @@ const VerifyForm = ({ cmek }: FormProps) => {
<div className="mb-6 flex flex-col gap-2">
<div className="flex items-center justify-between space-x-2">
<span className="text-sm opacity-60">Signature Status:</span>
<Badge variant={signatureValid ? "success" : "danger"}>
<Tooltip
content={
signatureValid
? "The signature is valid. signature was created using the same signing algorithm and key as the one used to sign the data."
: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with."
}
>
{signatureValid ? (
<div className="flex items-center justify-center gap-2">
<p>Valid</p>
</div>
) : (
<div className="flex items-center justify-center gap-2">
<p>Invalid</p>
</div>
)}
</Tooltip>
</Badge>
<Tooltip
content={
signatureValid
? "The signature is valid. signature was created using the same signing algorithm and key as the one used to sign the data."
: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with."
}
>
<Badge variant={signatureValid ? "success" : "danger"}>
{signatureValid ? "Valid" : "Invalid"}
</Badge>
</Tooltip>
</div>
<div className="flex items-center justify-between gap-2">
<span className="text-sm opacity-60">Signing Algorithm:</span>
<Badge variant="primary">{signingAlgorithm}</Badge>
<Badge variant="info">{signingAlgorithm}</Badge>
</div>
<div className="mt-3">
<span className="text-sm opacity-60">Signature:</span>{" "}
@@ -5,6 +5,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { useDeleteGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -26,8 +27,8 @@ export const OrgGroupsSection = () => {
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description:
"You can manage users more efficiently with groups if you upgrade your Infisical plan to an Enterprise license."
text: "Your current plan does not allow adding groups. To unlock this feature, please upgrade to Infisical Enterprise plan.",
isEnterpriseFeature: true
});
} else {
handlePopUpOpen("group");
@@ -57,7 +58,10 @@ export const OrgGroupsSection = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-medium text-mineshaft-100">Groups</p>
<div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Groups</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
</div>
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Button
@@ -92,7 +96,8 @@ export const OrgGroupsSection = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text={popUp.upgradePlan?.data?.text}
/>
</div>
);
@@ -55,7 +55,10 @@ const schema = z
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -277,7 +280,9 @@ export const IdentityAliCloudAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -292,7 +297,9 @@ export const IdentityAliCloudAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -315,7 +322,9 @@ export const IdentityAliCloudAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -4,7 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { Badge, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -277,7 +278,7 @@ export const IdentityAuthMethodModalContent = ({
>
{label}{" "}
{alreadyConfigured && !isSelectedAuthAlreadyConfigured && (
<Badge>Configured</Badge>
<Badge variant="info">Configured</Badge>
)}
</SelectItem>
</Tooltip>
@@ -291,7 +292,8 @@ export const IdentityAuthMethodModalContent = ({
<UpgradePlanModal
isOpen={popUp?.upgradePlan?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
text={`Your current plan does not include access to ${popUp.upgradePlan.data?.featureName}. To unlock this feature, please upgrade to Infisical ${popUp.upgradePlan.data?.isEnterpriseFeature ? "Enterprise" : "Pro"} plan.`}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</>
);
@@ -57,7 +57,10 @@ const schema = z
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -315,7 +318,9 @@ export const IdentityAwsAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -330,7 +335,9 @@ export const IdentityAwsAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -353,7 +360,9 @@ export const IdentityAwsAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -52,7 +52,10 @@ const schema = z
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -311,7 +314,9 @@ export const IdentityAzureAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -326,7 +331,9 @@ export const IdentityAzureAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -349,7 +356,9 @@ export const IdentityAzureAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -55,7 +55,10 @@ const schema = z
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -350,7 +353,9 @@ export const IdentityGcpAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -365,7 +370,9 @@ export const IdentityGcpAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -388,7 +395,9 @@ export const IdentityGcpAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -88,7 +88,10 @@ const schema = z.discriminatedUnion("configurationType", [
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -619,7 +622,9 @@ export const IdentityJwtAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -634,7 +639,9 @@ export const IdentityJwtAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -657,7 +664,9 @@ export const IdentityJwtAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -22,11 +22,12 @@ import {
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { useOrganization, useOrgPermission, useSubscription } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { OrgMembershipRole } from "@app/helpers/roles";
import {
gatewaysQueryKeys,
useAddIdentityKubernetesAuth,
@@ -95,7 +96,10 @@ const schema = z
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -129,6 +133,8 @@ export const IdentityKubernetesAuthForm = ({
] as const);
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
const { hasOrgRole } = useOrgPermission();
const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin);
const {
control,
@@ -409,20 +415,29 @@ export const IdentityKubernetesAuthForm = ({
<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"
/>
<Tooltip
content={
!isOrgAdmin
? "Only organization admins can import configurations from HashiCorp Vault"
: undefined
}
onClick={() => handleImportPopUpToggle("importFromVault", true)}
>
Load from Vault
</Button>
<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)}
isDisabled={!isOrgAdmin}
>
Load from Vault
</Button>
</Tooltip>
</div>
)}
<div className="flex w-full items-center gap-2">
@@ -695,7 +710,9 @@ export const IdentityKubernetesAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -710,7 +727,9 @@ export const IdentityKubernetesAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -733,7 +752,9 @@ export const IdentityKubernetesAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -147,7 +147,10 @@ const schema = z
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { isEnterpriseFeature?: boolean; featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -304,7 +307,10 @@ export const IdentityLdapAuthForm = ({
useEffect(() => {
if (!subscription?.ldap) {
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true,
featureName: "LDAP authentication"
});
handlePopUpToggle("identityAuthMethod", false);
}
}, [subscription, handlePopUpOpen, handlePopUpToggle]);
@@ -831,7 +837,9 @@ export const IdentityLdapAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -846,7 +854,9 @@ export const IdentityLdapAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -869,7 +879,9 @@ export const IdentityLdapAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -63,7 +63,10 @@ const schema = z
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -297,7 +300,9 @@ export const IdentityOciAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -312,7 +317,9 @@ export const IdentityOciAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -335,7 +342,9 @@ export const IdentityOciAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -75,7 +75,10 @@ const schema = z.object({
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -606,7 +609,9 @@ export const IdentityOidcAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -621,7 +626,9 @@ export const IdentityOidcAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -644,7 +651,9 @@ export const IdentityOidcAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -1,15 +1,11 @@
import {
faArrowUpRightFromSquare,
faBookOpen,
faLink,
faPlus
} from "@fortawesome/free-solid-svg-icons";
import { faLink, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
@@ -113,22 +109,9 @@ export const IdentitySection = withPermission(
<div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex w-full items-center gap-4">
<div className="flex flex-1 items-center gap-1">
<div className="flex flex-1 items-center gap-x-2">
<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>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
</div>
{isSubOrganization && (
<OrgPermissionCan
@@ -162,8 +145,7 @@ export const IdentitySection = withPermission(
onClick={() => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description:
"You can add more identities if you upgrade your Infisical plan."
text: "You have reached the maximum number of identities allowed on your current plan. Upgrade to Infisical Pro plan to add more identities."
});
return;
}
@@ -181,22 +163,9 @@ export const IdentitySection = withPermission(
{/* Identity Auth Templates Section */}
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Identity Auth Templates</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/auth-templates"
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>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/auth-templates" />
</div>
<OrgPermissionCan
I={OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates}
@@ -209,7 +178,10 @@ export const IdentitySection = withPermission(
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (subscription && !subscription.machineIdentityAuthTemplates) {
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true,
text: "Your current plan does not include access to creating Identity Auth Templates. To unlock this feature, please upgrade to Infisical Enterprise plan."
});
return;
}
handlePopUpOpen("createTemplate");
@@ -279,7 +251,8 @@ export const IdentitySection = withPermission(
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use Identity Auth Templates if you switch to Infisical's Enterprise plan."
text={popUp.upgradePlan.data?.text}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
);
@@ -50,7 +50,10 @@ const schema = z.object({
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -286,7 +289,9 @@ export const IdentityTlsCertAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -301,7 +306,9 @@ export const IdentityTlsCertAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -324,7 +331,9 @@ export const IdentityTlsCertAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -49,7 +49,10 @@ const schema = z
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -248,7 +251,9 @@ export const IdentityTokenAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -263,7 +268,9 @@ export const IdentityTokenAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -286,7 +293,9 @@ export const IdentityTokenAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -87,7 +87,10 @@ const schema = z
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["upgradePlan"]>,
data?: { featureName?: string }
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
@@ -409,7 +412,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -424,7 +429,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -447,7 +454,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -477,7 +486,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -492,7 +503,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -515,7 +528,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -1,13 +1,13 @@
import { useState } from "react";
import { faBan, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { BanIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DeleteActionModal,
EmailServiceSetupModal,
@@ -15,6 +15,7 @@ import {
ModalContent,
Tooltip
} from "@app/components/v2";
import { Badge, DocumentationLinkBadge } from "@app/components/v3";
import {
OrgPermissionActions,
OrgPermissionSubjects,
@@ -75,7 +76,7 @@ export const OrgMembersSection = () => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description: "You can add more members if you upgrade your Infisical plan."
text: "You have reached the maximum number of members allowed on your current plan. Upgrade to Infisical Pro plan to add more members."
});
return;
}
@@ -207,7 +208,10 @@ export const OrgMembersSection = () => {
</div>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-medium text-mineshaft-100">Users</p>
<div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Users</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<Button
@@ -303,15 +307,10 @@ export const OrgMembersSection = () => {
</p>
{userId === member.user.id && (
<Tooltip content="You cannot remove yourself from this organization">
<div className="inline-block">
<Badge
variant="danger"
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faBan} />
<span>Ignored</span>
</Badge>
</div>
<Badge variant="danger" className="ml-2">
<BanIcon />
Ignored
</Badge>
</Tooltip>
)}
</div>
@@ -324,7 +323,7 @@ export const OrgMembersSection = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
text={popUp.upgradePlan?.data?.text}
/>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
@@ -11,19 +11,18 @@ import {
faMagnifyingGlass,
faSearch,
faUsers,
faUserShield,
faUserSlash,
faUserXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { UserCogIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { LastLoginSection } from "@app/components/organization/LastLoginSection";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
Checkbox,
DropdownMenu,
@@ -50,6 +49,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
OrgPermissionActions,
OrgPermissionSubjects,
@@ -82,7 +82,7 @@ type Props = {
data?: {
orgMembershipId?: string;
username?: string;
description?: string;
text?: string;
selectedOrgMemberships?: OrgUser[];
}
) => void;
@@ -133,7 +133,7 @@ export const OrgMembersTable = ({
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description: "You can assign custom roles to members if you upgrade your Infisical plan."
text: "Your current plan does not include access to assigning custom roles to members. To unlock this feature, please upgrade to Infisical Pro plan."
});
return;
}
@@ -519,17 +519,17 @@ export const OrgMembersTable = ({
<Td
className={twMerge("group max-w-0", isActive ? "" : "text-mineshaft-400")}
>
<div className="flex items-center">
<div className="flex items-center gap-x-2">
<p className="truncate">
{name ?? <span className="text-mineshaft-400">Not Set</span>}
</p>
{u.superAdmin && (
<Badge variant="primary" className="ml-2 w-min whitespace-nowrap">
<span className="mr-1 hidden xl:inline">Server Admin</span>
<Tooltip content="Server Admin">
<FontAwesomeIcon className="xl:hidden" icon={faUserShield} />
</Tooltip>
</Badge>
<Tooltip content="Server Admin">
<Badge variant="info">
<UserCogIcon />
<span className="hidden xl:inline">Server Admin</span>
</Badge>
</Tooltip>
)}
{lastLoginAuthMethod && lastLoginTime && (
<Tooltip
@@ -14,13 +14,13 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { ServerIcon, WrenchIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
@@ -41,13 +41,14 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge, DocumentationLinkBadge } from "@app/components/v3";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { isCustomOrgRole, isCustomProjectRole } from "@app/helpers/roles";
import { isCustomOrgRole } from "@app/helpers/roles";
import {
getUserTablePreference,
PreferenceKey,
@@ -103,8 +104,7 @@ export const OrgRoleTable = () => {
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description:
"You can set the default org role to a custom role if you upgrade your Infisical plan."
text: "Your current plan does not include access to set a custom default organization role. To unlock this feature, please upgrade to Infisical Pro plan."
});
return;
}
@@ -200,9 +200,12 @@ export const OrgRoleTable = () => {
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-medium text-mineshaft-100">
{isSubOrganization ? "Sub-" : ""}Organization Roles
</p>
<div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">
{isSubOrganization ? "Sub-" : ""}Organization Roles
</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/organization#roles-and-access-control" />
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<Button
@@ -295,17 +298,13 @@ export const OrgRoleTable = () => {
}
>
<Td className="max-w-md">
<div className="flex">
<div className="flex gap-x-1.5">
<p className="overflow-hidden text-ellipsis whitespace-nowrap">{name}</p>
{isDefaultOrgRole && (
<Tooltip
content={`Members joining your organization will be assigned the ${name} role unless otherwise specified.`}
>
<div>
<Badge variant="success" className="ml-1">
Default
</Badge>
</div>
<Badge variant="info">Default</Badge>
</Tooltip>
)}
</div>
@@ -314,8 +313,18 @@ export const OrgRoleTable = () => {
{slug}
</Td>
<Td>
<Badge className="w-min bg-mineshaft-400/50 whitespace-nowrap text-bunker-200">
{isCustomProjectRole(slug) ? "Custom" : "Default"}
<Badge variant="ghost">
{isCustomOrgRole(slug) ? (
<>
<WrenchIcon />
Custom
</>
) : (
<>
<ServerIcon />
Platform
</>
)}
</Badge>
</Td>
<Td>
@@ -461,7 +470,7 @@ export const OrgRoleTable = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
text={popUp.upgradePlan?.data?.text}
/>
<DuplicateOrgRoleModal
isOpen={popUp.duplicateRole.isOpen}
@@ -20,6 +20,7 @@ import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
import { BitbucketConnectionForm } from "./BitbucketConnectionForm";
import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { ChecklyConnectionForm } from "./ChecklyConnectionForm";
import { ChefConnectionForm } from "./ChefConnectionForm";
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
@@ -36,6 +37,7 @@ import { LdapConnectionForm } from "./LdapConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { MySqlConnectionForm } from "./MySqlConnectionForm";
import { NetlifyConnectionForm } from "./NetlifyConnectionForm";
import { NorthflankConnectionForm } from "./NorthflankConnectionForm";
import { OCIConnectionForm } from "./OCIConnectionForm";
import { OktaConnectionForm } from "./OktaConnectionForm";
import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
@@ -96,7 +98,7 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
case AppConnection.AWS:
return <AwsConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm projectId={projectId} />;
return <GitHubConnectionForm projectId={projectId} onSubmit={onSubmit} />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm projectId={projectId} />;
case AppConnection.GCP:
@@ -163,12 +165,16 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
return <RailwayConnectionForm onSubmit={onSubmit} />;
case AppConnection.Checkly:
return <ChecklyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Chef:
return <ChefConnectionForm onSubmit={onSubmit} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} />;
case AppConnection.DigitalOcean:
return <DigitalOceanConnectionForm onSubmit={onSubmit} />;
case AppConnection.Netlify:
return <NetlifyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Northflank:
return <NorthflankConnectionForm onSubmit={onSubmit} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} />;
case AppConnection.Redis:
@@ -213,7 +219,11 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.GitHub:
return (
<GitHubConnectionForm appConnection={appConnection} projectId={appConnection.projectId} />
<GitHubConnectionForm
appConnection={appConnection}
projectId={appConnection.projectId}
onSubmit={onSubmit}
/>
);
case AppConnection.GitHubRadar:
return (
@@ -322,10 +332,14 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Checkly:
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Chef:
return <ChefConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.DigitalOcean:
return <DigitalOceanConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Northflank:
return <NorthflankConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Redis:
@@ -0,0 +1,195 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { ChefConnectionMethod, TChefConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TChefConnection;
onSubmit: (formData: FormData) => Promise<void>;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Chef)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(ChefConnectionMethod.UserKey),
credentials: z.object({
serverUrl: z.string().trim().url("Valid Chef Server URL required").optional(),
orgName: z.string().trim().min(1, "Organization name required"),
userName: z.string().trim().min(1, "User name required"),
privateKey: z.string().trim().min(1, "Private key required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const ChefConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Chef,
method: ChefConnectionMethod.UserKey
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="credentials.serverUrl"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Chef Server URL (optional)"
tooltipText="Will default to Chef Cloud if not specified."
>
<Input
placeholder="https://api.chef.io"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Chef].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(ChefConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.orgName"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Organization Short Name"
>
<Input
className="border border-mineshaft-500 bg-mineshaft-900"
placeholder="your-org"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
name="credentials.userName"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="User Name"
>
<Input
className="border border-mineshaft-500 bg-mineshaft-900"
placeholder="your-username"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
name="credentials.privateKey"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Private Key"
tooltipText="Your Chef user's private key (.pem file)"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Chef"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -16,6 +16,7 @@ import {
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem,
Tooltip
@@ -48,28 +49,58 @@ import {
type Props = {
appConnection?: TGitHubConnection;
projectId: string | undefined | null;
onSubmit: (formData: PatSchemaForm) => Promise<void>;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.GitHub),
method: z.nativeEnum(GitHubConnectionMethod),
credentials: z
.union([
z.object({
instanceType: z.literal("cloud").optional(),
host: z.string().optional()
}),
z.object({
instanceType: z.literal("server"),
host: z.string().min(1, "Required")
})
])
.optional()
method: z.nativeEnum(GitHubConnectionMethod)
});
const baseCredentialsSchema = z.union([
z.object({
instanceType: z.literal("server"),
host: z.string().min(1, "Host is required for server instance type")
}),
z.object({
instanceType: z.literal("cloud").optional(),
host: z.string().optional()
})
]);
const appSchema = rootSchema.extend({
method: z.literal(GitHubConnectionMethod.App),
credentials: baseCredentialsSchema
});
const oauthSchema = rootSchema.extend({
method: z.literal(GitHubConnectionMethod.OAuth),
credentials: baseCredentialsSchema
});
const patSchema = rootSchema.extend({
method: z.literal(GitHubConnectionMethod.Pat),
credentials: z.union([
z.object({
instanceType: z.literal("server"),
host: z.string().min(1, "Host is required for server instance type"),
personalAccessToken: z.string().min(1, "Personal Access Token is required")
}),
z.object({
instanceType: z.literal("cloud").optional(),
host: z.string().optional(),
personalAccessToken: z.string().min(1, "Personal Access Token is required")
})
])
});
type PatSchemaForm = z.infer<typeof patSchema>;
const formSchema = z.discriminatedUnion("method", [appSchema, oauthSchema, patSchema]);
type FormData = z.infer<typeof formSchema>;
export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
export const GitHubConnectionForm = ({ appConnection, projectId, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -106,7 +137,12 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
const returnUrl = useGetAppConnectionOauthReturnUrl();
const onSubmit = (formData: FormData) => {
const submitHandler = async (formData: FormData) => {
if (formData.method === GitHubConnectionMethod.Pat) {
await onSubmit(formData);
return;
}
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
@@ -151,15 +187,26 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
case GitHubConnectionMethod.App:
isMissingConfig = !appClientSlug;
break;
case GitHubConnectionMethod.Pat:
isMissingConfig = false;
break;
default:
throw new Error(`Unhandled GitHub Connection method: ${selectedMethod}`);
}
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
const getButtonText = () => {
if (selectedMethod === GitHubConnectionMethod.Pat) {
return isUpdate ? "Update Connection" : "Create Connection";
}
return isUpdate ? "Reconnect to GitHub" : "Connect to GitHub";
};
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(submitHandler)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
@@ -201,6 +248,26 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
</FormControl>
)}
/>
{selectedMethod === GitHubConnectionMethod.Pat && (
<Controller
name="credentials.personalAccessToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Personal Access Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
)}
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="enterprise-options" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
@@ -310,7 +377,7 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
isLoading={isSubmitting || isRedirecting}
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
>
{isUpdate ? "Reconnect to GitHub" : "Connect to GitHub"}
{getButtonText()}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
@@ -0,0 +1,135 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
NorthflankConnectionMethod,
TNorthflankConnection
} from "@app/hooks/api/appConnections/types/northflank-connection";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TNorthflankConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Northflank)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(NorthflankConnectionMethod.ApiToken),
credentials: z.object({
apiToken: z.string().trim().min(1, "API Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const NorthflankConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Northflank,
method: NorthflankConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Northflank].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(NorthflankConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:border-primary-400/50! border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Northflank"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -1,6 +1,6 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentationLinkBadge } from "@app/components/v3";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
@@ -29,23 +29,11 @@ export const AppConnectionHeader = ({ app, isConnected, onBack }: Props) => {
)}
</div>
<div>
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
{appDetails.name}
<a
<DocumentationLinkBadge
href={`https://infisical.com/docs/integrations/app-connections/${app}`}
target="_blank"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="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 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
/>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConnected ? `${appDetails.name} Connection` : `Connect to ${appDetails.name}`}
@@ -72,10 +72,13 @@ export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
return (
<button
key={option.app}
type="button"
onClick={() =>
enterprise && !subscription.enterpriseAppConnections
? handlePopUpOpen("upgradePlan")
? handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
})
: onSelect(option.app)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
@@ -166,7 +169,8 @@ export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use every App Connection if you switch to Infisical's Enterprise plan."
text="All App Connections can be unlocked if you switch to Infisical Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
);
@@ -8,18 +8,17 @@ import {
faEdit,
faEllipsisV,
faInfoCircle,
faServer,
faTable,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { ServerIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { VariablePermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -29,6 +28,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
@@ -157,9 +157,9 @@ export const AppConnectionRow = ({
{isPlatformManagedCredentials && (
<Tooltip side="left" content="This connection's credentials are managed by Infisical.">
<div>
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap">
<FontAwesomeIcon icon={faServer} />
<span>Platform Managed Credentials</span>
<Badge variant="info">
<ServerIcon />
Platform Managed Credentials
</Badge>
</div>
</Tooltip>
@@ -2,8 +2,6 @@ import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faBookOpen,
faCheckCircle,
faFilter,
faMagnifyingGlass,
@@ -34,6 +32,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
@@ -212,22 +211,9 @@ export const AppConnectionsTable = ({ projectId, projectType }: Props) => {
<div className="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-center gap-1">
<div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">App Connections</p>
<a
href="https://infisical.com/docs/integrations/app-connections/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-[0.12rem] 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>
<DocumentationLinkBadge href="https://infisical.com/docs/integrations/app-connections/overview" />
</div>
<p className="text-sm text-bunker-300">
Create and configure connections with third-party apps for re-use across your project
@@ -633,7 +633,9 @@ export const OAuthCallbackPage = () => {
connectionName: data.connection.name,
...(data.returnUrl.includes("integrations")
? {
selectedTab: IntegrationsListPageTabs.SecretSyncs
selectedTab: localStorage.getItem("pkiSyncFormData")
? IntegrationsListPageTabs.PkiSyncs
: IntegrationsListPageTabs.SecretSyncs
}
: {})
}
@@ -8,7 +8,6 @@ import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
@@ -20,6 +19,7 @@ import {
Select,
SelectItem
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useOrganization } from "@app/context";
import { useGetUserProjects } from "@app/hooks/api";
import {
@@ -124,7 +124,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
<Button variant="outline_bg" colorSchema="primary" className="relative">
<FontAwesomeIcon icon={faFilterCircleXmark} />
{activeFilterCount > 0 && (
<Badge className="absolute right-0 bottom-0" variant="primary">
<Badge className="absolute -top-2 -right-2" variant="info">
{activeFilterCount}
</Badge>
)}
@@ -137,7 +137,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2">
<span>Filters</span>
<Badge className="px-1.5 py-0.5" variant="primary">
<Badge isSquare variant="info">
{activeFilterCount}
</Badge>
</div>
@@ -1,9 +1,8 @@
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { DocumentationLinkBadge } from "@app/components/v3";
import {
OrgPermissionAuditLogsActions,
OrgPermissionSubjects,
@@ -76,22 +75,9 @@ const LogsSectionComponent = ({
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-y-2">
<div>
<div className="flex items-center gap-1 whitespace-nowrap">
<div className="flex items-center gap-x-2 whitespace-nowrap">
<p className="text-xl font-medium text-mineshaft-100">Audit History</p>
<a
href="https://infisical.com/docs/documentation/platform/audit-logs"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-[0.1rem] 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>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/audit-logs" />
</div>
</div>
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
@@ -137,7 +123,7 @@ const LogsSectionComponent = ({
onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use audit logs if you switch to a paid Infisical plan."
text="Your current plan does not include access to audit logs. To unlock this feature, please upgrade to Infisical Pro plan."
/>
</div>
</div>
@@ -181,7 +167,7 @@ const LogsSectionComponent = ({
onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use audit logs if you switch to a paid Infisical plan."
text="Your current plan does not include access to audit logs. To unlock this feature, please upgrade to Infisical Pro plan."
/>
</div>
);
@@ -5,7 +5,6 @@ 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";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
@@ -51,8 +50,7 @@ const Page = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"groupCreateUpdate",
"deleteGroup",
"upgradePlan"
"deleteGroup"
] as const);
const onDeleteGroupSubmit = async ({ name, id }: { name: string; id: string }) => {
@@ -184,11 +182,6 @@ const Page = () => {
onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; id: string })
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};
@@ -120,7 +120,8 @@ const Page = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
text={`Your current plan does not include access to ${popUp.upgradePlan.data?.featureName}. To unlock this feature, please upgrade to Infisical ${popUp.upgradePlan.data?.isEnterpriseFeature ? "Enterprise" : "Pro"} plan.`}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
@@ -183,7 +183,7 @@ export const Content = ({
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
text={`Your current plan does not include access to ${popUp.upgradePlan.data?.featureName}. To unlock this feature, please upgrade to Infisical ${popUp.upgradePlan.data?.isEnterpriseFeature ? "Enterprise" : "Pro"} plan.`}
/>
</>
);
@@ -1,7 +1,8 @@
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EyeIcon } from "lucide-react";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useGetIdentityJwtAuth } from "@app/hooks/api";
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
import { IdentityJwtAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm";
@@ -78,12 +79,10 @@ export const ViewIdentityJwtAuthContent = ({
<p className="rounded-sm bg-mineshaft-600 p-2 break-words">{data.jwksCaCert}</p>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
<Badge variant="neutral">
<EyeIcon />
Reveal
</Badge>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
@@ -103,12 +102,10 @@ export const ViewIdentityJwtAuthContent = ({
</p>
}
>
<div className="inline-block w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Key {index + 1}</span>
</Badge>
</div>
<Badge variant="neutral">
<EyeIcon />
Key {index + 1}
</Badge>
</Tooltip>
))}
</div>
@@ -138,12 +135,10 @@ export const ViewIdentityJwtAuthContent = ({
</pre>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
<Badge variant="neutral">
<EyeIcon />
Reveal
</Badge>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
@@ -1,9 +1,10 @@
import { useMemo } from "react";
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { useQuery } from "@tanstack/react-query";
import { EyeIcon } from "lucide-react";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { gatewaysQueryKeys, useGetIdentityKubernetesAuth } from "@app/hooks/api";
import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm";
@@ -89,12 +90,10 @@ export const ViewIdentityKubernetesAuthContent = ({
</p>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
<Badge variant="neutral">
<EyeIcon />
Reveal
</Badge>
</Tooltip>
) : (
<p className="text-base leading-4 text-bunker-400 italic">Not set</p>
@@ -122,12 +121,10 @@ export const ViewIdentityKubernetesAuthContent = ({
className="max-w-xl p-2"
content={<p className="rounded-sm bg-mineshaft-600 p-2 break-words">{data.caCert}</p>}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
<Badge variant="neutral">
<EyeIcon />
Reveal
</Badge>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
@@ -1,7 +1,8 @@
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EyeIcon } from "lucide-react";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useClearIdentityLdapAuthLockouts, useGetIdentityLdapAuth } from "@app/hooks/api";
import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm";
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
@@ -72,12 +73,10 @@ export const ViewIdentityLdapAuthContent = ({
className="max-w-xl p-2"
content={<p className="rounded-sm bg-mineshaft-600 p-2 break-words">{data.bindPass}</p>}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
<Badge variant="neutral">
<EyeIcon />
Reveal
</Badge>
</Tooltip>
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Search Base / DN">
@@ -95,12 +94,10 @@ export const ViewIdentityLdapAuthContent = ({
</p>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
<Badge variant="neutral">
<EyeIcon />
Reveal
</Badge>
</Tooltip>
)}
</IdentityAuthFieldDisplay>

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