mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 15:43:12 +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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user