Merge branch 'main' into fix/remove-duplicate-error-notifications

This commit is contained in:
Victor Santos
2025-11-03 18:41:54 -03:00
283 changed files with 6603 additions and 1582 deletions
@@ -411,7 +411,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}
@@ -55,7 +55,7 @@ export const EncryptionPageForm = () => {
if (!subscription.hsm) {
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true,
description: "Hardware Security Module's (HSM's), are only available on Enterprise plans."
text: "Your current plan does not include access to Hardware Security Module (HSM). To unlock this feature, please upgrade to Infisical Enterprise plan."
});
return;
}
@@ -137,7 +137,7 @@ 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}
/>
</>
@@ -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;
@@ -260,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;
}
@@ -288,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;
}
@@ -502,7 +499,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}
@@ -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) => {
@@ -109,11 +107,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>
);
};
@@ -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) => {
@@ -116,11 +114,6 @@ export const ExternalCaSection = () => {
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};
@@ -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>
);
};
@@ -97,7 +97,7 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text="Managing template enrollment options for EST is only available on Infisical's Enterprise plan."
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>
);
@@ -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,6 +15,7 @@ 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";
@@ -24,10 +24,10 @@ 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",
@@ -37,7 +37,8 @@ export const CertificatesSection = () => {
"deleteCertificate",
"revokeCertificate",
"manageRenewal",
"renewCertificate"
"renewCertificate",
"managePkiSyncs"
] as const);
const onRemoveCertificateSubmit = async (serialNumber: string) => {
@@ -97,6 +98,10 @@ export const CertificatesSection = () => {
<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,19 +2,24 @@ 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 { CircleQuestionMarkIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
CertificateDisplayName,
getCertificateDisplayName
} from "@app/components/utilities/certificateDisplayUtils";
import {
DropdownMenu,
DropdownMenuContent,
@@ -36,15 +41,14 @@ import { Badge } from "@app/components/v3";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useProject,
useSubscription
useProject
} from "@app/context";
import { useListWorkspaceCertificates, useUpdateRenewalConfig } 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 { TCertificate } from "@app/hooks/api/certificates/types";
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
@@ -56,93 +60,6 @@ const isExpiringWithinOneDay = (notAfter: string): boolean => {
return expiryDate <= oneDayFromNow;
};
const getAutoRenewalInfo = (certificate: TCertificate) => {
if (certificate.renewedByCertificateId) {
return { text: "Renewed", variant: "neutral" as const };
}
const isRevoked = certificate.status === CertStatus.REVOKED;
const isExpired = new Date(certificate.notAfter) < new Date();
const hasNoProfile = !certificate.profileId;
const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter);
if (isRevoked) {
return {
text: "Not Available",
variant: "neutral" as const,
tooltip: "Renewal is not available for revoked certificates"
};
}
if (isExpired) {
return {
text: "Not Available",
variant: "neutral" as const,
tooltip: "Renewal is not available for expired certificates"
};
}
if (hasNoProfile) {
return {
text: "Not Available",
variant: "neutral" as const,
tooltip: "Renewal requires a certificate profile"
};
}
if (certificate.hasPrivateKey === false) {
return {
text: "Not Available",
variant: "neutral" as const,
tooltip: "Renewal is not available for certificates with externally generated private keys"
};
}
if (isExpiringWithinDay) {
return {
text: "Not Available",
variant: "neutral" as const,
tooltip: "Auto-renewal is not available for certificates expiring within 24 hours"
};
}
if (certificate.renewalError) {
return {
text: "Failed",
variant: "danger" as const,
tooltip: certificate.renewalError
};
}
if (!certificate.renewBeforeDays) {
return { text: "Auto-Renewal Disabled", variant: "warning" as const };
}
const notAfterDate = new Date(certificate.notAfter);
const renewalDate = new Date(
notAfterDate.getTime() - certificate.renewBeforeDays * 24 * 60 * 60 * 1000
);
const now = new Date();
if (renewalDate <= now) {
return { text: "Due Now", variant: "danger" as const };
}
const daysUntilRenewal = Math.floor(
(renewalDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
);
if (daysUntilRenewal === 0) {
return { text: "Renews today", variant: "warning" as const };
}
if (daysUntilRenewal <= 7) {
return { text: `Renews in ${daysUntilRenewal}d`, variant: "warning" as const };
}
return { text: `Renews in ${daysUntilRenewal}d`, variant: "success" as const };
};
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
@@ -152,7 +69,8 @@ type Props = {
"revokeCertificate",
"certificateCert",
"manageRenewal",
"renewCertificate"
"renewCertificate",
"managePkiSyncs"
]
>,
data?: {
@@ -175,7 +93,6 @@ const PER_PAGE_INIT = 25;
export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { subscription } = useSubscription();
const { currentProject } = useProject();
const { data, isPending } = useListWorkspaceCertificates({
@@ -185,7 +102,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
});
const { mutateAsync: updateRenewalConfig } = useUpdateRenewalConfig();
const isLegacyTemplatesEnabled = subscription.pkiLegacyTemplates;
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
const isLegacyTemplatesEnabled = true;
const { data: caData } = useListCasByProjectId(currentProject?.id ?? "");
@@ -225,20 +143,18 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<Table>
<THead>
<Tr>
<Th>Common Name</Th>
<Th>Status</Th>
<Th>Not Before</Th>
<Th>Not After</Th>
<Th>Renewal Status</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={5} innerKey="project-cas" />}
{isPending && <TableSkeleton columns={4} innerKey="project-cas" />}
{!isPending &&
data?.certificates.map((certificate) => {
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
const autoRenewalInfo = getAutoRenewalInfo(certificate);
const isRevoked = certificate.status === CertStatus.REVOKED;
const isExpired = new Date(certificate.notAfter) < new Date();
@@ -247,9 +163,24 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
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>
@@ -267,22 +198,64 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
: "-"}
</Td>
<Td>
{autoRenewalInfo &&
(autoRenewalInfo.tooltip ? (
<div className="flex items-center gap-2">
<Tooltip content={autoRenewalInfo.tooltip}>
<Badge variant={autoRenewalInfo.variant}>
{autoRenewalInfo.text}
<CircleQuestionMarkIcon />
</Badge>
</Tooltip>
</div>
) : (
<Badge variant={autoRenewalInfo.variant}>{autoRenewalInfo.text}</Badge>
))}
</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">
@@ -476,6 +449,33 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
</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>
)}
{/* Only show revoke button if CA supports revocation */}
{(() => {
const caType = caCapabilityMap[certificate.caId];
@@ -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"
}
]
};
}
});
@@ -12,7 +12,6 @@ import {
faToggleOff,
faToggleOn,
faTrash,
faTriangleExclamation,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -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,23 +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 variant="warning">
<FontAwesomeIcon icon={faTriangleExclamation} />
<span>Source Deleted</span>
</Badge>
</div>
</Tooltip>
</Td>
)}
<PkiSyncDestinationCol pkiSync={pkiSync} />
<Td>
<div className="flex items-center gap-1">
@@ -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()
@@ -356,7 +347,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
@@ -369,20 +360,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
@@ -395,7 +373,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
@@ -12,13 +12,14 @@ 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
});
@@ -45,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 || "",
{
@@ -94,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(
@@ -58,9 +58,9 @@ export const PkiCollectionPage = () => {
});
handlePopUpClose("deletePkiCollection");
navigate({
to: "/projects/cert-management/$projectId/certificates",
to: "/projects/cert-management/$projectId/policies",
params: {
projectId
projectId: params.projectId
}
});
};
@@ -70,9 +70,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
}
@@ -350,8 +350,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
@@ -364,8 +370,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();
@@ -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>
@@ -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,13 +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 { 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;
@@ -28,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
@@ -43,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,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";
@@ -106,30 +106,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>
@@ -289,7 +288,7 @@ 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}
/>
</>
@@ -139,10 +139,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({
@@ -159,10 +163,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>
);
};
@@ -410,7 +410,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}
@@ -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>
@@ -33,43 +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="warning">No metrics</Badge>;
}
if (metrics.totalCertificates === 0) {
return <Badge variant="warning">No certificates</Badge>;
}
return (
<>
{metrics.activeCertificates > 0 && (
<Badge variant="success">{metrics.activeCertificates} active</Badge>
)}
{metrics.expiringCertificates > 0 && (
<Badge variant="warning">{metrics.expiringCertificates} expiring</Badge>
)}
{metrics.expiredCertificates > 0 && (
<Badge variant="danger">{metrics.expiredCertificates} expired</Badge>
)}
{metrics.revokedCertificates > 0 && (
<Badge variant="danger">{metrics.revokedCertificates} revoked</Badge>
)}
</>
);
};
interface Props {
profile: TCertificateProfile;
onEditProfile: (profile: TCertificateProfile) => void;
@@ -118,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: "warning" 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)
@@ -153,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";
@@ -344,7 +344,7 @@ 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>
@@ -27,8 +27,7 @@ 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 {
@@ -90,7 +89,7 @@ export const OrgGroupsSection = () => {
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text={(popUp.upgradePlan?.data as { description: string })?.description}
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
@@ -270,7 +273,9 @@ export const IdentityAliCloudAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -285,7 +290,9 @@ export const IdentityAliCloudAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -308,7 +315,9 @@ export const IdentityAliCloudAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -292,7 +292,7 @@ export const IdentityAuthMethodModalContent = ({
<UpgradePlanModal
isOpen={popUp?.upgradePlan?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={`You can use ${popUp.upgradePlan.data?.featureName ?? "IP allowlisting"} if you switch to Infisical's ${popUp.upgradePlan.data?.isEnterpriseFeature ? "Enterprise" : "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
@@ -308,7 +311,9 @@ export const IdentityAwsAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -323,7 +328,9 @@ export const IdentityAwsAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -346,7 +353,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
@@ -304,7 +307,9 @@ export const IdentityAzureAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -319,7 +324,9 @@ export const IdentityAzureAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -342,7 +349,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
@@ -343,7 +346,9 @@ export const IdentityGcpAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -358,7 +363,9 @@ export const IdentityGcpAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -381,7 +388,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
@@ -612,7 +615,9 @@ export const IdentityJwtAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -627,7 +632,9 @@ export const IdentityJwtAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -650,7 +657,9 @@ export const IdentityJwtAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -96,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
@@ -700,7 +703,9 @@ export const IdentityKubernetesAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -715,7 +720,9 @@ export const IdentityKubernetesAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -738,7 +745,9 @@ export const IdentityKubernetesAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -830,7 +830,9 @@ export const IdentityLdapAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -845,7 +847,9 @@ export const IdentityLdapAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -868,7 +872,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
@@ -290,7 +293,9 @@ export const IdentityOciAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -305,7 +310,9 @@ export const IdentityOciAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -328,7 +335,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
@@ -599,7 +602,9 @@ export const IdentityOidcAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -614,7 +619,9 @@ export const IdentityOidcAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -637,7 +644,9 @@ export const IdentityOidcAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -123,8 +123,7 @@ export const IdentitySection = withPermission(
onClick={() => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description:
"You can add more identities if you upgrade your Infisical Pro 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;
}
@@ -159,8 +158,7 @@ export const IdentitySection = withPermission(
if (subscription && !subscription.machineIdentityAuthTemplates) {
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true,
description:
"You can use Identity Auth Templates if you switch to Infisical's Enterprise plan."
text: "Your current plan does not include access to creating Identity Auth Templates. To unlock this feature, please upgrade to Infisical Enterprise plan."
});
return;
}
@@ -231,7 +229,7 @@ export const IdentitySection = withPermission(
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={popUp.upgradePlan.data?.description}
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
@@ -279,7 +282,9 @@ export const IdentityTlsCertAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -294,7 +299,9 @@ export const IdentityTlsCertAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -317,7 +324,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
@@ -241,7 +244,9 @@ export const IdentityTokenAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -256,7 +261,9 @@ export const IdentityTokenAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -279,7 +286,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
@@ -400,7 +403,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -415,7 +420,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -438,7 +445,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -468,7 +477,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
placeholder="123.456.789.0"
/>
@@ -483,7 +494,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
size="lg"
colorSchema="danger"
@@ -506,7 +519,9 @@ export const IdentityUniversalAuthForm = ({
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
featureName: "IP allowlisting"
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
@@ -76,7 +76,7 @@ export const OrgMembersSection = () => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description: "You can add more members if you switch to Infisical's Pro 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;
}
@@ -300,7 +300,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}
@@ -82,7 +82,7 @@ type Props = {
data?: {
orgMembershipId?: string;
username?: string;
description?: string;
text?: string;
selectedOrgMemberships?: OrgUser[];
}
) => void;
@@ -132,7 +132,7 @@ export const OrgMembersTable = ({
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description: "You can assign custom roles to members if you switch to Infisical's Pro 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;
}
@@ -99,8 +99,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 switch to Infisical's Pro 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;
}
@@ -461,7 +460,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";
@@ -155,6 +156,8 @@ 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:
@@ -311,6 +314,8 @@ 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:
@@ -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>
);
};
@@ -169,7 +169,7 @@ 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>
@@ -593,7 +593,9 @@ export const OAuthCallbackPage = () => {
connectionName: data.connection.name,
...(data.returnUrl.includes("integrations")
? {
selectedTab: IntegrationsListPageTabs.SecretSyncs
selectedTab: localStorage.getItem("pkiSyncFormData")
? IntegrationsListPageTabs.PkiSyncs
: IntegrationsListPageTabs.SecretSyncs
}
: {})
}
@@ -165,7 +165,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
<LogFilterItem
label="Events"
onClear={() => {
resetField("eventType");
setValue("eventType", [], { shouldDirty: true });
}}
>
<Controller
@@ -250,7 +250,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
<LogFilterItem
label="Source"
onClear={() => {
resetField("userAgentType");
setValue("userAgentType", undefined, { shouldDirty: true });
}}
>
<Controller
@@ -289,10 +289,10 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
<LogFilterItem
label="Project"
onClear={() => {
resetField("project");
resetField("environment");
setValue("secretPath", "");
setValue("secretKey", "");
setValue("project", null, { shouldDirty: true });
setValue("environment", undefined, { shouldDirty: true });
setValue("secretPath", "", { shouldDirty: true });
setValue("secretKey", "", { shouldDirty: true });
}}
>
<Controller
@@ -350,7 +350,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
}
className={twMerge(!selectedProject && "opacity-50")}
onClear={() => {
resetField("environment");
setValue("environment", undefined, { shouldDirty: true });
}}
>
<Controller
@@ -391,7 +391,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
}
className={twMerge(!selectedProject && "opacity-50")}
onClear={() => {
setValue("secretPath", "");
setValue("secretPath", "", { shouldDirty: true });
}}
>
<Controller
@@ -422,7 +422,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
className={twMerge(!selectedProject && "opacity-50")}
label="Secret Key"
onClear={() => {
setValue("secretKey", "");
setValue("secretKey", "", { shouldDirty: true });
}}
>
<Controller
@@ -123,7 +123,7 @@ const LogsSectionComponent = ({
onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use audit logs if you switch to Infisical's Pro plan."
text="Your current plan does not include access to audit logs. To unlock this feature, please upgrade to Infisical Pro plan."
/>
</div>
</div>
@@ -167,7 +167,7 @@ const LogsSectionComponent = ({
onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use audit logs if you switch to Infisical's Pro 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 }) => {
@@ -176,11 +174,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>
);
};
@@ -109,7 +109,7 @@ 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
@@ -175,7 +175,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.`}
/>
</>
);
@@ -92,7 +92,7 @@ export const ProjectsPage = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You have exceeded the number of projects allowed on the free plan. You can upgrade to Infisical's Pro plan to add more projects."
text="You have reached the maximum number of projects allowed on your current plan. Upgrade to Infisical Pro plan to add more projects."
/>
</div>
);
@@ -57,7 +57,7 @@ export const AuditLogStreamsTab = withPermission(
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add audit log streams if you switch to Infisical's Enterprise plan."
text="Your current plan does not include access to audit log streams. To unlock this feature, please upgrade to Infisical Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
@@ -111,7 +111,7 @@ export const LogStreamProviderSelect = ({ onSelect }: Props) => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="This audit log stream provider requires an enterprise license."
text="Your current plan does not include access to this audit log stream provider. To unlock this feature, please upgrade to Infisical Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
@@ -252,7 +252,7 @@ const OrgConfigSection = ({
<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}
/>
</>
@@ -121,7 +121,7 @@ export const OrgEncryptionTab = withPermission(
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can configure external KMS if you switch to Infisical's Enterprise plan."
text="Your current plan does not include access to external KMS. To unlock this feature, please upgrade to Infisical Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
<AddExternalKmsForm
@@ -181,7 +181,7 @@ export const OrgGithubSyncSection = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use GitHub Organization Plan if you switch to Infisical's Enterprise plan."
text="Your current plan does not include access to GitHub Organization Sync. To unlock this feature, please upgrade to Infisical Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
@@ -110,7 +110,7 @@ export const OrgScimSection = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use SCIM Provisioning if you switch to Infisical's Enterprise plan."
text="Your current plan does not include access to SCIM Provisioning. To unlock this feature, please upgrade to Infisical Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
@@ -94,7 +94,7 @@ export const OrgGenericAuthSection = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can enforce user MFA if you switch to Infisical's Pro plan."
text="Your current plan does not include access to enforce user MFA. To unlock this feature, please upgrade to Infisical Pro plan."
/>
</div>
);
@@ -304,7 +304,7 @@ export const OrgGeneralAuthSection = ({
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can enforce SAML SSO if you switch to Infisical's Pro plan."
text="Your current plan does not include access to enforce SAML SSO. To unlock this feature, please upgrade to Infisical Pro plan."
/>
</div>
);
@@ -183,7 +183,7 @@ export const OrgLDAPSection = (): JSX.Element => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use LDAP authentication if you switch to Infisical's Enterprise plan."
text="Your current plan does not include access to LDAP authentication. To unlock this feature, please upgrade to Infisical Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
@@ -199,7 +199,7 @@ export const OrgOIDCSection = (): JSX.Element => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use OIDC SSO if you switch to Infisical's Pro plan."
text="Your current plan does not include access to OIDC SSO. To unlock this feature, please upgrade to Infisical Pro plan."
/>
</div>
);
@@ -27,7 +27,6 @@ export const OrgSSOSection = (): JSX.Element => {
const { mutateAsync } = useUpdateSSOConfig();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"upgradePlan",
// "upgradeEnterprisePlan",
"addSSO"
] as const);
@@ -38,7 +37,7 @@ export const OrgSSOSection = (): JSX.Element => {
if (!subscription?.samlSSO) {
handlePopUpOpen("upgradePlan", {
description: "You can use SAML SSO if you switch to Infisical's Pro plan."
text: "Your current plan does not include access to SAML SSO. To unlock this feature, please upgrade to Infisical Pro plan."
});
return;
}
@@ -60,7 +59,7 @@ export const OrgSSOSection = (): JSX.Element => {
if (!subscription?.samlSSO || !subscription?.groups) {
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true,
description: "You can use SAML group mapping if you switch to Infisical's Enterprise plan."
text: "Your current plan does not include access to SAML group mapping. To unlock this feature, please upgrade to Infisical Enterprise plan."
});
return;
}
@@ -95,7 +94,7 @@ export const OrgSSOSection = (): JSX.Element => {
handlePopUpOpen("addSSO");
} else {
handlePopUpOpen("upgradePlan", {
description: "You can use SAML SSO if you switch to Infisical's Pro plan."
text: "Your current plan does not include access to SAML SSO. To unlock this feature, please upgrade to Infisical Pro plan."
});
}
} catch (err) {
@@ -225,7 +224,7 @@ export const OrgSSOSection = (): JSX.Element => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={popUp.upgradePlan.data?.description}
text={popUp.upgradePlan.data?.text}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
@@ -93,7 +93,7 @@ export const OrgSsoTab = withPermission(
colorSchema="secondary"
onClick={() => {
if (!subscription?.samlSSO) {
handlePopUpOpen("upgradePlan", { feature: "SAML SSO" });
handlePopUpOpen("upgradePlan", { featureName: "SAML SSO" });
return;
}
@@ -116,7 +116,7 @@ export const OrgSsoTab = withPermission(
colorSchema="secondary"
onClick={() => {
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan", { feature: "OIDC SSO" });
handlePopUpOpen("upgradePlan", { featureName: "OIDC SSO" });
return;
}
@@ -135,7 +135,7 @@ export const OrgSsoTab = withPermission(
onClick={() => {
if (!subscription?.ldap) {
handlePopUpOpen("upgradePlan", {
feature: "LDAP",
featureName: "LDAP",
isEnterpriseFeature: true
});
return;
@@ -208,16 +208,8 @@ export const OrgSsoTab = withPermission(
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={`You can use ${
(popUp.upgradePlan.data as { feature: string })?.feature
} if you switch to Infisical's ${
(popUp.upgradePlan.data as { isEnterpriseFeature: boolean })?.isEnterpriseFeature
? "Enterprise"
: "Pro"
} plan.`}
isEnterpriseFeature={
(popUp.upgradePlan.data as { isEnterpriseFeature: boolean })?.isEnterpriseFeature
}
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}
/>
</>
);
@@ -73,7 +73,7 @@ export const ProjectTemplatesSection = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can create project templates if you switch to Infisical's Enterprise plan."
text="Your current plan does not include access to project templates. To unlock this feature, please upgrade to Infisical Enterprise plan."
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
@@ -287,7 +287,7 @@ const Page = withPermission(
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
text={popUp.upgradePlan?.data?.text}
/>
<UserOrgMembershipModal
popUp={popUp}
@@ -137,8 +137,7 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description:
"You can assign custom roles to members if you switch to Infisical's Pro plan."
text: "Your current plan does not include access to assig custom roles to members. To unlock this feature, please upgrade to Infisical Pro plan."
});
return;
}
@@ -55,7 +55,7 @@ export const ResourceTypeSelect = ({ onSelect }: Props) => {
const handleResourceSelect = (resource: PamResourceType) => {
if (!subscription.pam) {
handlePopUpOpen("upgradePlan", {
description: "PAM (Privileged Access Management) requires an enterprise plan.",
text: "Your current plan does not include access to Infisical PAM. To unlock this feature, please upgrade to Infisical Enterprise plan.",
isEnterpriseFeature: true
});
return;
@@ -68,8 +68,7 @@ export const ResourceTypeSelect = ({ onSelect }: Props) => {
resource === PamResourceType.Kubernetes
) {
handlePopUpOpen("upgradePlan", {
description:
"This resource type requires a special license add-on to be enabled in your enterprise plan.",
text: "Your current plan does not include access to this resource type. To unlock this feature, please upgrade to Infisical Enterprise plan.",
isEnterpriseFeature: true
});
return;
@@ -180,7 +179,7 @@ export const ResourceTypeSelect = ({ onSelect }: Props) => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={popUp.upgradePlan.data?.description}
text={popUp.upgradePlan.data?.text}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</div>
@@ -0,0 +1,116 @@
import { useState } from "react";
import { HighlightText } from "@app/components/v2/HighlightText";
import { PamResourceType } from "@app/hooks/api/pam";
type TableLog = {
command?: string;
data_rows: Record<string, string | number | null>[];
total_rows?: number;
};
export const PamSessionLogOutput = ({
content,
resourceType,
search
}: {
content: string;
resourceType: PamResourceType;
search: string;
}) => {
const [isRawView, setIsRawView] = useState(false);
let parsedContent: TableLog | null = null;
if (resourceType === PamResourceType.Postgres || resourceType === PamResourceType.MySQL) {
try {
const parsed = JSON.parse(content);
if (
parsed &&
typeof parsed === "object" &&
!Array.isArray(parsed) &&
parsed.data_rows &&
Array.isArray(parsed.data_rows) &&
parsed.data_rows.length > 0 &&
typeof parsed.data_rows[0] === "object" &&
parsed.data_rows[0] !== null
) {
parsedContent = parsed;
}
} catch {
// Not a valid JSON or doesn't match structure, will render as plain text
}
}
if (parsedContent) {
const headers = Object.keys(parsedContent.data_rows[0]);
return (
<div className="font-sans">
{isRawView ? (
<div className="font-mono break-all whitespace-pre-wrap">
<HighlightText text={content} highlight={search} />
</div>
) : (
<>
{parsedContent.command && (
<div className="mb-2 font-mono">{`> ${parsedContent.command}`}</div>
)}
<div className="overflow-x-auto rounded-md border border-mineshaft-600">
<table className="w-full min-w-max text-left">
<thead className="bg-mineshaft-800">
<tr className="border-b border-mineshaft-600">
{headers.map((header) => (
<th key={header} className="p-2 font-semibold text-mineshaft-200 capitalize">
<HighlightText text={header.replace(/_/g, " ")} highlight={search} />
</th>
))}
</tr>
</thead>
<tbody>
{parsedContent.data_rows.map((row, rowIndex) => (
<tr
// eslint-disable-next-line react/no-array-index-key
key={`row-${rowIndex}`}
className="border-b border-mineshaft-700 bg-mineshaft-900 last:border-b-0 hover:bg-mineshaft-800/50"
>
{headers.map((header) => (
<td key={header} className="p-2">
<HighlightText text={String(row[header] ?? "")} highlight={search} />
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</>
)}
<div className="mt-2 flex items-center">
<button
type="button"
className="cursor-pointer text-sm text-bunker-400 underline"
onClick={(e) => {
e.stopPropagation();
setIsRawView((v) => !v);
}}
>
{isRawView ? "View Formatted" : "View Raw"}
</button>
{parsedContent.total_rows !== undefined && (
<div className="ml-auto text-right text-sm text-bunker-400">
Total rows: {parsedContent.total_rows}
</div>
)}
</div>
</div>
);
}
return (
<div className="font-mono break-all whitespace-pre-wrap">
<HighlightText text={content} highlight={search} />
</div>
);
};
@@ -1,42 +1,76 @@
import { useState } from "react";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { useMemo, useState } from "react";
import { faChevronRight, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Input } from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { TPamSession } from "@app/hooks/api/pam";
import { PamSessionLogOutput } from "./PamSessionLogOutput";
import { formatLogContent } from "./PamSessionLogsSection.utils";
type Props = {
session: TPamSession;
};
export const PamSessionLogsSection = ({ session }: Props) => {
const [expandedLogTimestamps, setExpandedLogTimestamps] = useState<Set<string>>(new Set());
const [search, setSearch] = useState("");
const toggleExpand = (timestamp: string) => {
setExpandedLogTimestamps((prev) => {
const newSet = new Set(prev);
if (newSet.has(timestamp)) {
newSet.delete(timestamp);
} else {
newSet.add(timestamp);
if (prev.has(timestamp)) {
return new Set();
}
return newSet;
return new Set([timestamp]);
});
};
const filteredLogs = useMemo(
() =>
session.commandLogs.filter((log) => {
const { input, output } = log;
const searchValue = search.trim().toLowerCase();
return (
input.toLowerCase().includes(searchValue) || output.toLowerCase().includes(searchValue)
);
}),
[session.commandLogs, search]
);
return (
<div className="flex h-full w-full flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-medium text-mineshaft-100">Session Logs</h3>
</div>
<div className="flex flex-col gap-2 overflow-y-auto text-xs">
{session.commandLogs.length > 0 ? (
session.commandLogs.map((log) => {
const isExpanded = expandedLogTimestamps.has(log.timestamp);
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => {
const newSearch = e.target.value;
setSearch(newSearch);
}}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search logs..."
className="flex-1 bg-mineshaft-800"
containerClassName="bg-transparent"
/>
</div>
<div className="flex grow flex-col gap-2 overflow-y-auto text-xs">
{filteredLogs.length > 0 ? (
filteredLogs.map((log) => {
const isExpanded = search.length || expandedLogTimestamps.has(log.timestamp);
const formattedInput = formatLogContent(log.input);
return (
<button
type="button"
key={log.timestamp}
className={`flex w-full flex-col rounded-md border border-mineshaft-700 p-3 text-left focus:ring-2 focus:ring-mineshaft-400 focus:outline-hidden ${
className={`flex w-full flex-col rounded-md border border-mineshaft-700 p-3 text-left focus:inset-ring-2 focus:inset-ring-mineshaft-400 focus:outline-hidden ${
isExpanded ? "bg-mineshaft-700" : "bg-mineshaft-800 hover:bg-mineshaft-700"
}`}
onClick={() => toggleExpand(log.timestamp)}
@@ -44,8 +78,11 @@ export const PamSessionLogsSection = ({ session }: Props) => {
<div className="flex items-center justify-between text-bunker-400">
<div className="flex items-center gap-2 select-none">
<FontAwesomeIcon
icon={isExpanded ? faChevronDown : faChevronRight}
className="size-3 transition-transform duration-200"
icon={faChevronRight}
className={twMerge(
"size-3 transition-transform duration-100 ease-in-out",
isExpanded && "rotate-90"
)}
/>
<span>{new Date(log.timestamp).toLocaleString()}</span>
</div>
@@ -56,20 +93,44 @@ export const PamSessionLogsSection = ({ session }: Props) => {
isExpanded ? "break-all whitespace-pre-wrap" : "truncate"
}`}
>
{log.input}
<HighlightText text={formattedInput} highlight={search} />
</div>
{isExpanded && log.output && (
<div className="mt-2 border-t border-mineshaft-700 pt-2 font-mono break-all whitespace-pre-wrap text-bunker-300">
{log.output}
<div
className={twMerge(
"grid transition-all duration-100 ease-in-out",
isExpanded && log.output ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
)}
>
<div className="overflow-hidden">
{log.output && (
<>
<div className="mt-2 flex items-center gap-2">
<div className="h-px w-full bg-mineshaft-400" />
<span className="text-xs text-mineshaft-400">OUTPUT</span>
<div className="h-px w-full bg-mineshaft-400" />
</div>
<div className="pt-2 text-bunker-300">
<PamSessionLogOutput
content={log.output}
resourceType={session.resourceType}
search={search}
/>
</div>
</>
)}
</div>
)}
</div>
</button>
);
})
) : (
<div className="flex w-full grow items-center justify-center text-bunker-300">
{session.startedAt && session.endedAt ? (
<div className="flex grow items-center justify-center text-bunker-300">
{search.length ? (
<div className="text-center">
<div className="mb-2">No logs match search criteria</div>
</div>
) : (
<div className="text-center">
<div className="mb-2">Session logs are not yet available</div>
<div className="text-xs text-bunker-400">
@@ -78,8 +139,6 @@ export const PamSessionLogsSection = ({ session }: Props) => {
If logs do not appear after some time, please contact your Gateway administrators.
</div>
</div>
) : (
"No session logs"
)}
</div>
)}
@@ -0,0 +1,46 @@
// This function trims top and bottom empty padding, as well as moves all relative text to the left while still respecting indentation
export const formatLogContent = (text: string | null | undefined): string => {
if (!text) return "";
let lines = text.split("\n");
// Find the first and last non-empty lines to trim vertical padding
let firstLineIndex = -1;
for (let i = 0; i < lines.length; i += 1) {
if (lines[i].trim() !== "") {
firstLineIndex = i;
break;
}
}
if (firstLineIndex === -1) {
return "";
}
let lastLineIndex = -1;
for (let i = lines.length - 1; i >= 0; i -= 1) {
if (lines[i].trim() !== "") {
lastLineIndex = i;
break;
}
}
lines = lines.slice(firstLineIndex, lastLineIndex + 1);
// Determine the minimum indentation of non-empty lines
const indentations = lines
.filter((line) => line.trim() !== "")
.map((line) => {
const match = line.match(/^\s*/);
return match ? match[0].length : 0;
});
const minIndentation = indentations.length > 0 ? Math.min(...indentations) : 0;
// Remove the common indentation from all lines
if (minIndentation > 0) {
lines = lines.map((line) => line.substring(minIndentation));
}
return lines.join("\n");
};
@@ -27,6 +27,7 @@ import { HighlightText } from "@app/components/v2/HighlightText";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { PAM_RESOURCE_TYPE_MAP, TPamSession } from "@app/hooks/api/pam";
import { formatLogContent } from "../../PamSessionsByIDPage/components/PamSessionLogsSection.utils";
import { PamSessionStatusBadge } from "./PamSessionStatusBadge";
type Props = {
@@ -159,21 +160,28 @@ export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) =
{filteredCommandLogs.length > 0 && (
<Tr>
<Td colSpan={5} className="py-3 text-xs">
{logsToShow.map((log) => (
<div key={`${id}-log-${log.timestamp}`} className="mb-4 flex flex-col last:mb-0">
<div className="flex items-center gap-1.5 text-bunker-400">
<FontAwesomeIcon icon={faTerminal} className="size-3" />
<span>{new Date(log.timestamp).toLocaleString()}</span>
</div>
{logsToShow.map((log) => {
const formattedInput = formatLogContent(log.input);
<div className="font-mono">
<HighlightText text={log.input.trim()} highlight={search} />
return (
<div
key={`${id}-log-${log.timestamp}`}
className="mb-4 flex flex-col gap-1 last:mb-0"
>
<div className="flex items-center gap-1.5 text-bunker-400">
<FontAwesomeIcon icon={faTerminal} className="size-3" />
<span>{new Date(log.timestamp).toLocaleString()}</span>
</div>
<div className="font-mono break-all whitespace-pre-wrap">
<HighlightText text={formattedInput} highlight={search} />
</div>
<div className="font-mono text-bunker-300">
<HighlightText text={log.output.trim()} highlight={search} />
</div>
</div>
<div className="font-mono text-bunker-300">
<HighlightText text={log.output.trim()} highlight={search} />
</div>
</div>
))}
);
})}
{filteredCommandLogs.length > LOGS_TO_SHOW && (
<div className="mt-2">
<Button
@@ -33,8 +33,7 @@ export const GroupsSection = () => {
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description:
"You can manage users more efficiently with groups if you switch to Infisical's Enterprise plan.",
text: "Managing groups can be unlocked if you upgrade to Infisical Enterprise plan.",
isEnterpriseFeature: true
});
} else {
@@ -93,10 +92,8 @@ export const GroupsSection = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
isEnterpriseFeature={
(popUp.upgradePlan?.data as { isEnterpriseFeature: boolean })?.isEnterpriseFeature
}
text={popUp.upgradePlan?.data?.text}
isEnterpriseFeature={popUp.upgradePlan?.data?.isEnterpriseFeature}
/>
</div>
);
@@ -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";
@@ -26,8 +25,7 @@ export const MembersSection = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addMember",
"removeMember",
"upgradePlan"
"removeMember"
] as const);
const handleRemoveUser = async () => {
@@ -77,11 +75,6 @@ export const MembersSection = () => {
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
onDeleteApproved={handleRemoveUser}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};
@@ -60,10 +60,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeMember", "upgradePlan"]>,
data?: object
) => void;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeMember"]>, data?: object) => void;
};
enum MembersOrderBy {
@@ -182,8 +182,7 @@ export const Page = () => {
isMembershipDetailsLoading={isMembershipDetailsLoading}
onOpenUpgradeModal={() =>
handlePopUpOpen("upgradePlan", {
description:
"You can assign custom roles to members if you switch to Infisical's Pro plan."
text: "Assigning custom roles to members can be unlocked if you upgrade to Infisical Pro plan."
})
}
/>
@@ -207,7 +206,7 @@ export const Page = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
text={popUp.upgradePlan?.data?.text}
/>
</>
) : (
@@ -41,8 +41,7 @@ export const AuditLogsRetentionSection = () => {
const handleAuditLogsRetentionSubmit = async ({ auditLogsRetentionDays }: TForm) => {
if (!subscription?.auditLogs) {
handlePopUpOpen("upgradePlan", {
description:
"You can only configure audit logs retention if you switch to Infisical's Pro plan."
text: "Configuring audit logs retention can be unlocked if you upgrade to Infisical Pro plan."
});
return;
@@ -50,8 +49,7 @@ export const AuditLogsRetentionSection = () => {
if (subscription && auditLogsRetentionDays > subscription?.auditLogsRetentionDays) {
handlePopUpOpen("upgradePlan", {
description:
"To update your audit logs retention period to a higher value, switch to Infisical's Pro plan."
text: "Updating audit logs retention period to a higher value can be unlocked if you upgrade to Infisical Pro plan."
});
return;
@@ -116,7 +114,7 @@ export const AuditLogsRetentionSection = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
text={popUp.upgradePlan?.data?.text}
/>
</>
);
@@ -97,7 +97,7 @@ export const IPAllowlistSection = () => {
<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 IP allowlisting. To unlock this feature, please upgrade to Infisical Pro plan."
/>
</div>
);

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