UI improvements on PKI product

This commit is contained in:
Carlos Monastyrski
2025-12-04 18:55:26 -03:00
parent 003aa1cd59
commit 90c73def68
26 changed files with 243 additions and 418 deletions
@@ -13,6 +13,7 @@ export {
ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiSyncActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions,
ProjectPermissionSub
+1
View File
@@ -24,6 +24,7 @@ export {
ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiSyncActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions,
ProjectPermissionSub,
@@ -72,8 +72,8 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
data &&
data.length > 0 &&
data.map((ca) => {
const canReadCa = permission.can(
ProjectPermissionCertificateAuthorityActions.Read,
const canEditCa = permission.can(
ProjectPermissionCertificateAuthorityActions.Edit,
subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})
@@ -83,12 +83,12 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
<Tr
className={twMerge(
"h-10 transition-colors duration-100",
canReadCa && "cursor-pointer hover:bg-mineshaft-700",
!canReadCa && "cursor-not-allowed opacity-60"
canEditCa && "cursor-pointer hover:bg-mineshaft-700",
!canEditCa && "cursor-not-allowed opacity-60"
)}
key={`ca-${ca.id}`}
onClick={() =>
canReadCa &&
canEditCa &&
handlePopUpOpen("ca", {
caId: ca.id,
name: ca.name,
@@ -6,6 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import {
ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub,
useProject
} from "@app/context";
@@ -104,12 +105,12 @@ export const CertificatesSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Certificates</p>
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Create}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<div className="flex gap-2">
<div className="flex gap-2">
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Import}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faArrowRight} />}
@@ -118,6 +119,13 @@ export const CertificatesSection = () => {
>
Import
</Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionCertificateProfileActions.IssueCert}
a={ProjectPermissionSub.CertificateProfiles}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
@@ -127,9 +135,9 @@ export const CertificatesSection = () => {
>
Request
</Button>
</div>
)}
</ProjectPermissionCan>
)}
</ProjectPermissionCan>
</div>
</div>
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -41,8 +41,10 @@ import {
import { Badge } from "@app/components/v3";
import {
ProjectPermissionCertificateActions,
ProjectPermissionPkiSyncActions,
ProjectPermissionSub,
useProject
useProject,
useProjectPermission
} from "@app/context";
import { useUpdateRenewalConfig } from "@app/hooks/api";
import { caSupportsCapability } from "@app/hooks/api/ca/constants";
@@ -97,6 +99,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { currentProject } = useProject();
const { permission } = useProjectPermission();
const { data, isPending } = useListWorkspaceCertificates({
projectId: currentProject?.id ?? "",
offset: (page - 1) * perPage,
@@ -208,55 +211,72 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
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;
{canShowAutoRenewalIcon &&
(() => {
const canEditCertificate = permission.can(
ProjectPermissionCertificateActions.Edit,
subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber
})
);
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>
)}
return (
<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 (!canEditCertificate) return;
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)
),
notAfter: certificate.notAfter,
renewalError: certificate.renewalError,
renewedFromCertificateId: certificate.renewedFromCertificateId,
renewedByCertificateId: certificate.renewedByCertificateId
});
}}
>
<FontAwesomeIcon icon={faClockRotateLeft} />
</button>
</Tooltip>
);
})()}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
@@ -481,13 +501,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{certificate.status === CertStatus.ACTIVE &&
!certificate.renewedByCertificateId && (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit}
a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
I={ProjectPermissionPkiSyncActions.Edit}
a={ProjectPermissionSub.PkiSyncs}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -509,14 +524,14 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
</ProjectPermissionCan>
)}
{/* Only show revoke button if CA supports revocation */}
{/* Only show revoke button if CA supports revocation and certificate is not already revoked */}
{(() => {
const caType = caCapabilityMap[certificate.caId];
const supportsRevocation =
!caType ||
caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES);
if (!supportsRevocation) {
if (!supportsRevocation || isRevoked) {
return null;
}
@@ -2,8 +2,15 @@ import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PermissionDeniedBanner } from "@app/components/permissions";
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useProject } from "@app/context";
import { useProject, useProjectPermission } from "@app/context";
import {
ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
@@ -20,8 +27,22 @@ enum TabSections {
export const PoliciesPage = () => {
const { t } = useTranslation();
const { currentProject } = useProject();
const { permission } = useProjectPermission();
const [activeTab, setActiveTab] = useState(TabSections.CertificateProfiles);
const canReadCertificateProfiles = permission.can(
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles
);
const canReadCertificateTemplates = permission.can(
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates
);
const canReadCertificates = permission.can(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
if (!currentProject) {
return <ContentLoader />;
}
@@ -56,15 +77,19 @@ export const PoliciesPage = () => {
</TabList>
<TabPanel value={TabSections.CertificateProfiles}>
<CertificateProfilesTab />
{canReadCertificateProfiles ? <CertificateProfilesTab /> : <PermissionDeniedBanner />}
</TabPanel>
<TabPanel value={TabSections.CertificateTemplatesV2}>
<CertificateTemplatesV2Tab />
{canReadCertificateTemplates ? (
<CertificateTemplatesV2Tab />
) : (
<PermissionDeniedBanner />
)}
</TabPanel>
<TabPanel value={TabSections.Certificates}>
<CertificatesTab />
{canReadCertificates ? <CertificatesTab /> : <PermissionDeniedBanner />}
</TabPanel>
</Tabs>
</div>
@@ -258,7 +258,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
};
};
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<
const { control, handleSubmit, reset, watch, setValue, formState, trigger } = useForm<
FormData & { preset: TemplatePresetId }
>({
resolver: zodResolver(templateSchema),
@@ -286,10 +286,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
};
const watchedPreset = watch("preset") || TEMPLATE_PRESET_IDS.CUSTOM;
const handlePresetChange = (presetId: TemplatePresetId) => {
const handlePresetChange = async (presetId: TemplatePresetId) => {
setValue("preset", presetId);
if (presetId === TEMPLATE_PRESET_IDS.CUSTOM) {
await trigger();
return;
}
@@ -313,6 +314,8 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
if (selectedPreset.formData.keyAlgorithm) {
setValue("keyAlgorithm", selectedPreset.formData.keyAlgorithm);
}
await trigger();
}
};