mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 16:28:40 +00:00
PKI UI improvements
This commit is contained in:
@@ -17,9 +17,9 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
CertStatus,
|
||||
@@ -299,119 +299,129 @@ export const CertificateManagementModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{allCertificates.length === 0 ? (
|
||||
<EmptyState title="No certificates found">
|
||||
{searchTerm
|
||||
? "No certificates match your search criteria."
|
||||
: "No certificates available for sync."}
|
||||
</EmptyState>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-12">
|
||||
<Checkbox
|
||||
id="select-all-certificates"
|
||||
isChecked={
|
||||
allCertificates.length > 0 &&
|
||||
allCertificates.every((cert) => selectedIds.includes(cert.id))
|
||||
}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</Th>
|
||||
<Th className="w-1/3">Common Name</Th>
|
||||
<Th className="w-1/3">Serial Number</Th>
|
||||
<Th className="w-1/6">Status</Th>
|
||||
<Th className="w-2/6">Expires</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{allCertificates.map((cert) => {
|
||||
const isExpired = new Date(cert.notAfter) < new Date();
|
||||
const isRevoked = cert.status === CertStatus.REVOKED;
|
||||
const cannotBeAdded = isExpired || isRevoked;
|
||||
const isAlreadySynced = syncedCertificateIds.includes(cert.id);
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-12">
|
||||
<Checkbox
|
||||
id="select-all-certificates"
|
||||
isChecked={
|
||||
allCertificates.length > 0 &&
|
||||
allCertificates.every((cert) => selectedIds.includes(cert.id))
|
||||
}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</Th>
|
||||
<Th className="w-1/3">SAN / CN</Th>
|
||||
<Th className="w-1/4">Serial Number</Th>
|
||||
<Th className="w-1/6">Issued At</Th>
|
||||
<Th className="w-1/6">Expires At</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{allCertificates.map((cert) => {
|
||||
const isExpired = new Date(cert.notAfter) < new Date();
|
||||
const isRevoked = cert.status === CertStatus.REVOKED;
|
||||
const cannotBeAdded = isExpired || isRevoked;
|
||||
const isAlreadySynced = syncedCertificateIds.includes(cert.id);
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={cert.id}
|
||||
className={`cursor-pointer hover:bg-mineshaft-700 ${
|
||||
cannotBeAdded && !isAlreadySynced ? "opacity-50" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
let originalDisplayName = "—";
|
||||
if (cert.altNames && cert.altNames.trim()) {
|
||||
originalDisplayName = cert.altNames.trim();
|
||||
} else if (cert.commonName && cert.commonName.trim()) {
|
||||
originalDisplayName = cert.commonName.trim();
|
||||
}
|
||||
|
||||
let displayName = originalDisplayName;
|
||||
let isTruncated = false;
|
||||
if (originalDisplayName.length > 34) {
|
||||
displayName = `${originalDisplayName.substring(0, 34)}...`;
|
||||
isTruncated = true;
|
||||
}
|
||||
|
||||
const truncatedSerial =
|
||||
cert.serialNumber.length > 8
|
||||
? `${cert.serialNumber.slice(0, 4)}...${cert.serialNumber.slice(-4)}`
|
||||
: cert.serialNumber;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={cert.id}
|
||||
className={`cursor-pointer hover:bg-mineshaft-700 ${
|
||||
cannotBeAdded && !isAlreadySynced ? "opacity-50" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!cannotBeAdded || isAlreadySynced) {
|
||||
handleToggleSelection(cert.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Td className="max-w-0" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
id={cert.id}
|
||||
isChecked={selectedIds.includes(cert.id)}
|
||||
onCheckedChange={() => {
|
||||
if (!cannotBeAdded || isAlreadySynced) {
|
||||
handleToggleSelection(cert.id);
|
||||
}
|
||||
}}
|
||||
isDisabled={cannotBeAdded && !isAlreadySynced}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
{isTruncated ? (
|
||||
<Tooltip content={originalDisplayName} className="max-w-lg">
|
||||
<div className="truncate">{displayName}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="truncate">{displayName}</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="font-mono text-xs text-bunker-300"
|
||||
title={cert.serialNumber}
|
||||
>
|
||||
<Td className="max-w-0">
|
||||
<Checkbox
|
||||
id={cert.id}
|
||||
isChecked={selectedIds.includes(cert.id)}
|
||||
onCheckedChange={() => {
|
||||
if (!cannotBeAdded || isAlreadySynced) {
|
||||
handleToggleSelection(cert.id);
|
||||
}
|
||||
}}
|
||||
isDisabled={cannotBeAdded && !isAlreadySynced}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div className="truncate" title={cert.commonName}>
|
||||
{cert.commonName}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate font-mono text-xs text-bunker-300"
|
||||
title={cert.serialNumber}
|
||||
>
|
||||
{cert.serialNumber}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<Badge
|
||||
variant={
|
||||
cert.status === CertStatus.ACTIVE && !isExpired
|
||||
? "success"
|
||||
: "danger"
|
||||
}
|
||||
>
|
||||
{(() => {
|
||||
if (isRevoked) return "Revoked";
|
||||
if (isExpired) return "Expired";
|
||||
return cert.status === CertStatus.ACTIVE ? "Active" : cert.status;
|
||||
})()}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{new Date(cert.notAfter).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{truncatedSerial}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span className="text-sm text-bunker-300">
|
||||
{new Date(cert.notBefore).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{new Date(cert.notAfter).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{allCertificates.length === 0 && (
|
||||
<EmptyState title="No certificates found">
|
||||
{searchTerm
|
||||
? "No certificates match your search criteria."
|
||||
: "No certificates available for sync."}
|
||||
</EmptyState>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={currentPage}
|
||||
perPage={pageSize}
|
||||
onChangePage={(page: number) => setCurrentPage(page)}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={currentPage}
|
||||
perPage={pageSize}
|
||||
onChangePage={(page: number) => setCurrentPage(page)}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCertificate, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
@@ -76,25 +77,71 @@ export const PkiSyncCertificatesFields = () => {
|
||||
>
|
||||
Add Certificates
|
||||
</Button>
|
||||
{selectedCertificates.length === 0 ? (
|
||||
<EmptyState title="No certificates selected" icon={faPlus} />
|
||||
) : (
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2/5">Common Name</Th>
|
||||
<Th className="w-2/5">Serial Number</Th>
|
||||
<Th className="w-1/5">Remove</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{selectedCertificates.map((cert) => (
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">SAN / CN</Th>
|
||||
<Th className="w-1/4">Serial Number</Th>
|
||||
<Th className="w-1/6">Issued At</Th>
|
||||
<Th className="w-1/6">Expires At</Th>
|
||||
<Th className="w-12">Remove</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{selectedCertificates.map((cert) => {
|
||||
let originalDisplayName = "—";
|
||||
if (cert.altNames && cert.altNames.trim()) {
|
||||
originalDisplayName = cert.altNames.trim();
|
||||
} else if (cert.commonName && cert.commonName.trim()) {
|
||||
originalDisplayName = cert.commonName.trim();
|
||||
}
|
||||
|
||||
let displayName = originalDisplayName;
|
||||
let isTruncated = false;
|
||||
if (originalDisplayName.length > 34) {
|
||||
displayName = `${originalDisplayName.substring(0, 34)}...`;
|
||||
isTruncated = true;
|
||||
}
|
||||
|
||||
const truncatedSerial =
|
||||
cert.serialNumber.length > 8
|
||||
? `${cert.serialNumber.slice(0, 4)}...${cert.serialNumber.slice(-4)}`
|
||||
: cert.serialNumber;
|
||||
|
||||
const isExpired = new Date(cert.notAfter) < new Date();
|
||||
|
||||
return (
|
||||
<Tr key={cert.id}>
|
||||
<Td className="max-w-xs truncate">{cert.commonName}</Td>
|
||||
<Td className="font-mono text-xs text-bunker-300">
|
||||
{cert.serialNumber}
|
||||
<Td className="max-w-0">
|
||||
{isTruncated ? (
|
||||
<Tooltip content={originalDisplayName} className="max-w-lg">
|
||||
<div className="truncate">{displayName}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="truncate">{displayName}</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="font-mono text-xs text-bunker-300"
|
||||
title={cert.serialNumber}
|
||||
>
|
||||
{truncatedSerial}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span className="text-sm text-bunker-300">
|
||||
{new Date(cert.notBefore).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{new Date(cert.notAfter).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<Button
|
||||
@@ -112,12 +159,15 @@ export const PkiSyncCertificatesFields = () => {
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{selectedCertificates.length === 0 && (
|
||||
<EmptyState title="No certificates selected" icon={faCertificate} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import {
|
||||
GenericFieldLabel,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useProject } from "@app/context";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
@@ -45,20 +55,71 @@ export const PkiSyncReviewFields = () => {
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Certificates</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<div>
|
||||
{selectedCertificates.length === 0 ? (
|
||||
<span className="text-bunker-400">No certificates selected</span>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{selectedCertificates.map((cert) => (
|
||||
<div key={cert.id} className="text-sm">
|
||||
{cert.commonName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{selectedCertificates.length === 0 ? (
|
||||
<span className="text-bunker-400">No certificates selected</span>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/2">SAN / CN</Th>
|
||||
<Th className="w-1/4">Serial Number</Th>
|
||||
<Th className="w-1/4">Expires At</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{selectedCertificates.map((cert) => {
|
||||
let originalDisplayName = "—";
|
||||
if (cert.altNames && cert.altNames.trim()) {
|
||||
originalDisplayName = cert.altNames.trim();
|
||||
} else if (cert.commonName && cert.commonName.trim()) {
|
||||
originalDisplayName = cert.commonName.trim();
|
||||
}
|
||||
|
||||
let displayName = originalDisplayName;
|
||||
let isTruncated = false;
|
||||
if (originalDisplayName.length > 34) {
|
||||
displayName = `${originalDisplayName.substring(0, 34)}...`;
|
||||
isTruncated = true;
|
||||
}
|
||||
|
||||
const truncatedSerial =
|
||||
cert.serialNumber.length > 8
|
||||
? `${cert.serialNumber.slice(0, 4)}...${cert.serialNumber.slice(-4)}`
|
||||
: cert.serialNumber;
|
||||
|
||||
return (
|
||||
<Tr key={cert.id}>
|
||||
<Td className="max-w-0">
|
||||
{isTruncated ? (
|
||||
<Tooltip content={originalDisplayName} className="max-w-lg">
|
||||
<div className="truncate">{displayName}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="truncate">{displayName}</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="font-mono text-xs text-bunker-300"
|
||||
title={cert.serialNumber}
|
||||
>
|
||||
{truncatedSerial}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span className="text-sm text-bunker-300">
|
||||
{new Date(cert.notAfter).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -79,11 +140,13 @@ export const PkiSyncReviewFields = () => {
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-3">
|
||||
<GenericFieldLabel label="Auto-Sync">
|
||||
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
|
||||
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
<div className="mt-1">
|
||||
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
|
||||
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</div>
|
||||
</GenericFieldLabel>
|
||||
{/* Hidden for now - Import certificates functionality disabled
|
||||
{syncOptions?.canImportCertificates !== undefined && (
|
||||
@@ -96,9 +159,11 @@ export const PkiSyncReviewFields = () => {
|
||||
*/}
|
||||
{syncOptions?.canRemoveCertificates !== undefined && (
|
||||
<GenericFieldLabel label="Remove Certificates">
|
||||
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>
|
||||
{syncOptions.canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
<div className="mt-1">
|
||||
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>
|
||||
{syncOptions.canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</div>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
|
||||
interface CertificateNameData {
|
||||
altNames?: string | null;
|
||||
commonName?: string | null;
|
||||
certificateAltNames?: string | null;
|
||||
certificateCommonName?: string | null;
|
||||
}
|
||||
|
||||
interface DisplayNameResult {
|
||||
originalDisplayName: string;
|
||||
displayName: string;
|
||||
isTruncated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and formats the display name for a certificate from SAN/CN data
|
||||
* @param cert - Certificate object with potential altNames/commonName fields
|
||||
* @param maxLength - Maximum length before truncating (default: 64)
|
||||
* @param fallback - Fallback text when no name is found (default: "—")
|
||||
* @returns Object with original name, truncated name, and truncation flag
|
||||
*/
|
||||
export const getCertificateDisplayName = (
|
||||
cert: CertificateNameData,
|
||||
maxLength: number = 64,
|
||||
fallback: string = "—"
|
||||
): DisplayNameResult => {
|
||||
// Extract original display name - prioritize SAN over CN
|
||||
let originalDisplayName = fallback;
|
||||
|
||||
// Handle different property name variations
|
||||
const altNames = cert.altNames || cert.certificateAltNames;
|
||||
const commonName = cert.commonName || cert.certificateCommonName;
|
||||
|
||||
if (altNames && altNames.trim()) {
|
||||
originalDisplayName = altNames.trim();
|
||||
} else if (commonName && commonName.trim()) {
|
||||
originalDisplayName = commonName.trim();
|
||||
}
|
||||
|
||||
// Handle truncation
|
||||
let displayName = originalDisplayName;
|
||||
let isTruncated = false;
|
||||
|
||||
if (originalDisplayName.length > maxLength) {
|
||||
displayName = `${originalDisplayName.substring(0, maxLength)}...`;
|
||||
isTruncated = true;
|
||||
}
|
||||
|
||||
return {
|
||||
originalDisplayName,
|
||||
displayName,
|
||||
isTruncated
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a certificate display name with optional tooltip for truncated names
|
||||
* @param cert - Certificate object with potential altNames/commonName fields
|
||||
* @param maxLength - Maximum length before truncating (default: 64)
|
||||
* @param fallback - Fallback text when no name is found (default: "—")
|
||||
* @param className - Optional CSS class for the display element
|
||||
* @param tooltipClassName - Optional CSS class for the tooltip (default: "max-w-lg")
|
||||
* @returns JSX element with certificate name and optional tooltip
|
||||
*/
|
||||
export const CertificateDisplayName = ({
|
||||
cert,
|
||||
maxLength = 64,
|
||||
fallback = "—",
|
||||
className = "truncate",
|
||||
tooltipClassName = "max-w-lg"
|
||||
}: {
|
||||
cert: CertificateNameData;
|
||||
maxLength?: number;
|
||||
fallback?: string;
|
||||
className?: string;
|
||||
tooltipClassName?: string;
|
||||
}): ReactNode => {
|
||||
const { originalDisplayName, displayName, isTruncated } = getCertificateDisplayName(
|
||||
cert,
|
||||
maxLength,
|
||||
fallback
|
||||
);
|
||||
|
||||
if (isTruncated) {
|
||||
return (
|
||||
<Tooltip content={originalDisplayName} className={tooltipClassName}>
|
||||
<div className={className}>{displayName}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} title={originalDisplayName}>
|
||||
{displayName}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -152,7 +152,7 @@ export const useCreateCertificate = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateCertificateV3 = () => {
|
||||
export const useCreateCertificateV3 = (options?: { projectId?: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCreateCertificateV3Response, object, TCreateCertificateV3DTO>({
|
||||
mutationFn: async (body) => {
|
||||
@@ -167,6 +167,12 @@ export const useCreateCertificateV3 = () => {
|
||||
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||
});
|
||||
|
||||
if (options?.projectId) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: projectKeys.forProjectCertificates(options.projectId)
|
||||
});
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["certificate-profiles"]
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ export {
|
||||
useGetCertificateProfileById,
|
||||
useGetCertificateProfileBySlug,
|
||||
useGetProfileCertificates,
|
||||
useGetProfileMetrics,
|
||||
useListCertificateProfiles
|
||||
} from "./queries";
|
||||
export type * from "./types";
|
||||
|
||||
@@ -4,7 +4,6 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
TCertificateProfile,
|
||||
TCertificateProfileMetrics,
|
||||
TCertificateProfileWithDetails,
|
||||
TGetCertificateProfileByIdDTO,
|
||||
TGetCertificateProfileBySlugDTO,
|
||||
@@ -20,7 +19,6 @@ export const certificateProfileKeys = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
includeMetrics?: boolean;
|
||||
includeConfigs?: boolean;
|
||||
enrollmentType?: string;
|
||||
expiringDays?: number;
|
||||
@@ -51,10 +49,8 @@ export const useListCertificateProfiles = ({
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
search,
|
||||
includeMetrics = false,
|
||||
includeConfigs = false,
|
||||
enrollmentType,
|
||||
expiringDays = 7
|
||||
enrollmentType
|
||||
}: TListCertificateProfilesDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certificateProfileKeys.list({
|
||||
@@ -62,10 +58,8 @@ export const useListCertificateProfiles = ({
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
includeMetrics,
|
||||
includeConfigs,
|
||||
enrollmentType,
|
||||
expiringDays
|
||||
enrollmentType
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
@@ -77,10 +71,8 @@ export const useListCertificateProfiles = ({
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
includeMetrics,
|
||||
includeConfigs,
|
||||
enrollmentType,
|
||||
expiringDays
|
||||
enrollmentType
|
||||
}
|
||||
});
|
||||
return data;
|
||||
@@ -145,18 +137,3 @@ export const useGetProfileCertificates = ({
|
||||
enabled: Boolean(profileId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProfileMetrics = ({ profileId, expiringDays = 7 }: TGetProfileMetricsDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certificateProfileKeys.getMetrics(profileId, { expiringDays }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
metrics: TCertificateProfileMetrics;
|
||||
}>(`/api/v1/pki/certificate-profiles/${profileId}/metrics`, {
|
||||
params: { expiringDays }
|
||||
});
|
||||
return data.metrics;
|
||||
},
|
||||
enabled: Boolean(profileId)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ export type TCertificateProfile = {
|
||||
apiConfigId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
metrics?: TCertificateProfileMetrics;
|
||||
};
|
||||
|
||||
export type TCertificateProfileWithDetails = TCertificateProfile & {
|
||||
@@ -81,10 +80,8 @@ export type TListCertificateProfilesDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
includeMetrics?: boolean;
|
||||
includeConfigs?: boolean;
|
||||
enrollmentType?: "api" | "est";
|
||||
expiringDays?: number;
|
||||
};
|
||||
|
||||
export type TGetCertificateProfileByIdDTO = {
|
||||
@@ -96,15 +93,6 @@ export type TGetCertificateProfileBySlugDTO = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type TCertificateProfileMetrics = {
|
||||
profileId: string;
|
||||
totalCertificates: number;
|
||||
activeCertificates: number;
|
||||
expiredCertificates: number;
|
||||
expiringCertificates: number;
|
||||
revokedCertificates: number;
|
||||
};
|
||||
|
||||
export type TProfileCertificate = {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
@@ -126,5 +114,4 @@ export type TGetProfileCertificatesDTO = {
|
||||
|
||||
export type TGetProfileMetricsDTO = {
|
||||
profileId: string;
|
||||
expiringDays?: number;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export type TCertificate = {
|
||||
friendlyName: string;
|
||||
commonName: string;
|
||||
subjectAltNames: string;
|
||||
altNames?: string;
|
||||
serialNumber: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
|
||||
@@ -57,6 +57,7 @@ export type TPkiSyncCertificate = {
|
||||
updatedAt: string;
|
||||
certificateSerialNumber?: string;
|
||||
certificateCommonName?: string;
|
||||
certificateAltNames?: string;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
|
||||
@@ -52,9 +52,7 @@ export const PkiManagerLayout = () => {
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>Certificate Management</Tab>
|
||||
)}
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Certificates</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-authorities"
|
||||
|
||||
+3
-2
@@ -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));
|
||||
|
||||
+93
-122
@@ -2,6 +2,7 @@ import { useMemo, useState } from "react";
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
faClockRotateLeft,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFileExport,
|
||||
@@ -11,11 +12,14 @@ import {
|
||||
} 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,
|
||||
@@ -45,7 +49,6 @@ 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";
|
||||
|
||||
@@ -58,93 +61,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<
|
||||
@@ -236,20 +152,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();
|
||||
@@ -258,9 +172,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>
|
||||
@@ -278,22 +207,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">
|
||||
@@ -370,20 +341,20 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
const notAfterDate = new Date(certificate.notAfter);
|
||||
const notBeforeDate = certificate.notBefore
|
||||
? new Date(certificate.notBefore)
|
||||
: new Date(
|
||||
notAfterDate.getTime() - 365 * 24 * 60 * 60 * 1000
|
||||
);
|
||||
notAfterDate.getTime() - 365 * 24 * 60 * 60 * 1000
|
||||
);
|
||||
const ttlDays = Math.max(
|
||||
1,
|
||||
Math.ceil(
|
||||
(notAfterDate.getTime() - notBeforeDate.getTime()) /
|
||||
(24 * 60 * 60 * 1000)
|
||||
(24 * 60 * 60 * 1000)
|
||||
)
|
||||
);
|
||||
handlePopUpOpen("manageRenewal", {
|
||||
@@ -433,7 +404,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
await handleDisableAutoRenewal(
|
||||
@@ -470,7 +441,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("renewCertificate", {
|
||||
@@ -532,7 +503,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
|
||||
-19
@@ -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">
|
||||
|
||||
+3
-25
@@ -57,7 +57,6 @@ import { PkiSyncRow } from "./PkiSyncRow";
|
||||
|
||||
enum PkiSyncsOrderBy {
|
||||
Destination = "destination",
|
||||
Source = "source",
|
||||
Name = "name",
|
||||
Status = "status"
|
||||
}
|
||||
@@ -160,14 +159,6 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
const [syncOne, syncTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case PkiSyncsOrderBy.Source:
|
||||
return (syncOne.subscriber?.name ?? syncOne.subscriberId ?? "")
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
syncTwo.subscriber?.name?.toLowerCase() ??
|
||||
syncTwo.subscriberId?.toLowerCase() ??
|
||||
""
|
||||
);
|
||||
case PkiSyncsOrderBy.Destination:
|
||||
return getPkiSyncDestinationColValues(syncOne)
|
||||
.primaryText.toLowerCase()
|
||||
@@ -370,7 +361,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2" />
|
||||
<Th className="w-1/4">
|
||||
<Th className="w-1/2">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
@@ -383,20 +374,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Source
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PkiSyncsOrderBy.Source)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PkiSyncsOrderBy.Source)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PkiSyncsOrderBy.Source)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<Th className="w-1/4">
|
||||
<div className="flex items-center">
|
||||
Destination
|
||||
<IconButton
|
||||
@@ -409,7 +387,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="min-w-42">
|
||||
<Th className="w-1/4 min-w-42">
|
||||
<div className="flex items-center">
|
||||
Status
|
||||
<IconButton
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+148
-101
@@ -1,12 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCertificate, 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,
|
||||
@@ -46,6 +51,11 @@ const getSyncStatusText = (status?: CertificateSyncStatus | null) => {
|
||||
|
||||
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;
|
||||
|
||||
@@ -74,6 +84,9 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
text: "Certificate removed from sync",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
setIsDeleteModalOpen(false);
|
||||
setCertificateToDelete(null);
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to remove certificate from sync",
|
||||
@@ -82,13 +95,18 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
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="font-medium text-mineshaft-100">Certificates ({totalCount})</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Certificates</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -105,108 +123,120 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{syncCertificates.length === 0 ? (
|
||||
<EmptyState title="No certificates" icon={faPlus}>
|
||||
No certificates are currently synced with this PKI destination.
|
||||
</EmptyState>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Common Name</Th>
|
||||
<Th className="w-1/3">Serial Number</Th>
|
||||
<Th className="w-1/9">Status</Th>
|
||||
<Th className="w-1/9">Expires</Th>
|
||||
<Th className="w-1/9">Actions</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{syncCertificates.map((syncCert) => {
|
||||
const isExpired = syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter) < new Date()
|
||||
: false;
|
||||
<div className="space-y-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">SAN / CN</Th>
|
||||
<Th className="w-1/3">Serial Number</Th>
|
||||
<Th className="w-1/9">Status</Th>
|
||||
<Th className="w-1/9">Expires</Th>
|
||||
<Th className="w-1/9">Actions</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{syncCertificates.map((syncCert) => {
|
||||
const isExpired = syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter) < new Date()
|
||||
: false;
|
||||
|
||||
return (
|
||||
<Tr key={syncCert.id}>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate"
|
||||
title={syncCert.certificateCommonName || "Unknown"}
|
||||
>
|
||||
{syncCert.certificateCommonName || "Unknown"}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate text-xs"
|
||||
title={syncCert.certificateSerialNumber || "Unknown"}
|
||||
>
|
||||
{syncCert.certificateSerialNumber || "Unknown"}
|
||||
</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>
|
||||
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 className="max-w-0">
|
||||
<div
|
||||
className="truncate text-xs"
|
||||
title={syncCert.certificateSerialNumber || "Unknown"}
|
||||
>
|
||||
{syncCert.certificateSerialNumber || "Unknown"}
|
||||
</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">
|
||||
<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>
|
||||
)}
|
||||
</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">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSyncActions.Edit}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="Remove certificate"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handleRemoveCertificate(syncCert.certificateId)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={currentPage}
|
||||
perPage={pageSize}
|
||||
onChangePage={(page: number) => setCurrentPage(page)}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{syncCertificates.length === 0 && (
|
||||
<EmptyState
|
||||
title="No certificates are part of this certificate sync"
|
||||
icon={faCertificate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
|
||||
@@ -218,6 +248,23 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-6
@@ -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>
|
||||
|
||||
+21
@@ -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>;
|
||||
};
|
||||
+3
-3
@@ -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 +1,2 @@
|
||||
export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection";
|
||||
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
|
||||
|
||||
+24
-28
@@ -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>
|
||||
);
|
||||
|
||||
+23
-11
@@ -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="Certificate Removal" labelClassName="mb-1">
|
||||
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
|
||||
{canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const PoliciesPage = () => {
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Management"
|
||||
description="Manage certificate templates, profiles, certificates, and PKI collections for unified certificate issuance"
|
||||
description="Streamline certificate management by creating and maintaining templates, profiles, and certificates in one place"
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
|
||||
+3
-6
@@ -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>
|
||||
|
||||
+2
-44
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user