mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 11:28:43 +00:00
PKI Syncs revamp
This commit is contained in:
+253
@@ -0,0 +1,253 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { faPlus, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
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 { useProject } from "@app/context";
|
||||
import {
|
||||
useAddCertificatesToPkiSync,
|
||||
useListPkiSyncsWithCertificate,
|
||||
useRemoveCertificatesFromPkiSync
|
||||
} from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
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 { 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);
|
||||
};
|
||||
|
||||
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="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}>
|
||||
Create a PKI sync first to manage certificate syncing.
|
||||
</EmptyState>
|
||||
)}
|
||||
{!isPending && pkiSyncs.length > 0 && filteredSyncs.length === 0 && searchTerm && (
|
||||
<EmptyState title="No PKI syncs found" icon={faSearch}>
|
||||
No PKI syncs match your search criteria. Try a different search term.
|
||||
</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={sync.destination.replace(/-/g, " ")}
|
||||
>
|
||||
{sync.destination.replace(/-/g, " ")}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,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";
|
||||
@@ -37,7 +38,8 @@ export const CertificatesSection = () => {
|
||||
"deleteCertificate",
|
||||
"revokeCertificate",
|
||||
"manageRenewal",
|
||||
"renewCertificate"
|
||||
"renewCertificate",
|
||||
"managePkiSyncs"
|
||||
] as const);
|
||||
|
||||
const onRemoveCertificateSubmit = async (serialNumber: string) => {
|
||||
@@ -105,6 +107,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 ${
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFileExport,
|
||||
faLink,
|
||||
faQuestionCircle,
|
||||
faRedo,
|
||||
faTrash
|
||||
@@ -39,12 +40,13 @@ import {
|
||||
useProject,
|
||||
useSubscription
|
||||
} 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";
|
||||
@@ -152,7 +154,8 @@ type Props = {
|
||||
"revokeCertificate",
|
||||
"certificateCert",
|
||||
"manageRenewal",
|
||||
"renewCertificate"
|
||||
"renewCertificate",
|
||||
"managePkiSyncs"
|
||||
]
|
||||
>,
|
||||
data?: {
|
||||
@@ -488,6 +491,31 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
</ProjectPermissionCan>
|
||||
);
|
||||
})()}
|
||||
{/* PKI Sync management - only for active certificates */}
|
||||
{certificate.status === CertStatus.ACTIVE && (
|
||||
<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} />}
|
||||
>
|
||||
PKI Syncs
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
{/* Only show revoke button if CA supports revocation */}
|
||||
{(() => {
|
||||
const caType = caCapabilityMap[certificate.caId];
|
||||
|
||||
Reference in New Issue
Block a user