Add Certificate Syncs

This commit is contained in:
Carlos Monastyrski
2025-09-16 01:36:06 -03:00
parent ca877cb225
commit 5bfff46c16
100 changed files with 8044 additions and 31 deletions
@@ -0,0 +1,76 @@
import { useEffect, useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
import { CreatePkiSyncForm } from "./forms";
import { PkiSyncModalHeader } from "./PkiSyncModalHeader";
import { PkiSyncSelect } from "./PkiSyncSelect";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
selectSync?: PkiSync | null;
};
type ContentProps = {
onComplete: (pkiSync: TPkiSync) => void;
selectedSync: PkiSync | null;
setSelectedSync: (selectedSync: PkiSync | null) => void;
};
const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => {
if (selectedSync) {
return (
<CreatePkiSyncForm
onComplete={onComplete}
onCancel={() => setSelectedSync(null)}
destination={selectedSync}
/>
);
}
return <PkiSyncSelect onSelect={setSelectedSync} />;
};
export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => {
const [selectedSync, setSelectedSync] = useState<PkiSync | null>(selectSync);
useEffect(() => {
setSelectedSync(selectSync);
}, [selectSync]);
return (
<Modal
{...props}
onOpenChange={(isOpen) => {
if (!isOpen) setSelectedSync(null);
onOpenChange(isOpen);
}}
>
<ModalContent
title={
selectedSync ? (
<PkiSyncModalHeader isConfigured={false} destination={selectedSync} />
) : (
"Add Sync"
)
}
className="max-w-2xl"
bodyClassName="overflow-visible"
subTitle={
selectedSync ? undefined : "Select a third-party service to sync certificates to."
}
>
<Content
onComplete={() => {
setSelectedSync(null);
onOpenChange(false);
}}
selectedSync={selectedSync}
setSelectedSync={setSelectedSync}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,60 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync, useDeletePkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync?: TPkiSync;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onComplete?: () => void;
};
export const DeletePkiSyncModal = ({ isOpen, onOpenChange, pkiSync, onComplete }: Props) => {
const deleteSync = useDeletePkiSync();
if (!pkiSync) return null;
const { id: syncId, name, destination, projectId } = pkiSync;
const handleDeletePkiSync = async () => {
const destinationName = PKI_SYNC_MAP[destination].name;
try {
await deleteSync.mutateAsync({
syncId,
projectId
});
createNotification({
text: `Successfully deleted ${destinationName} PKI Sync`,
type: "success"
});
if (onComplete) onComplete();
onOpenChange(false);
} catch (err) {
console.error(err);
createNotification({
text: `Failed to delete ${destinationName} PKI Sync`,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeletePkiSync}
>
<p className="mt-4 text-sm text-bunker-300">
This action will also remove all certificates that were synced by this configuration from
the {PKI_SYNC_MAP[destination].name} destination.
</p>
</DeleteActionModal>
);
};
@@ -0,0 +1,29 @@
import { PkiSyncEditFields } from "@app/components/pki-syncs/types";
import { Modal, ModalContent } from "@app/components/v2";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
import { EditPkiSyncForm } from "./forms";
import { PkiSyncModalHeader } from "./PkiSyncModalHeader";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
pkiSync?: TPkiSync;
fields: PkiSyncEditFields;
};
export const EditPkiSyncModal = ({ pkiSync, onOpenChange, fields, ...props }: Props) => {
if (!pkiSync) return null;
return (
<Modal {...props} onOpenChange={onOpenChange}>
<ModalContent
title={<PkiSyncModalHeader isConfigured destination={pkiSync.destination} />}
className="max-w-2xl"
bodyClassName="overflow-visible"
>
<EditPkiSyncForm onComplete={() => onOpenChange(false)} fields={fields} pkiSync={pkiSync} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,94 @@
import { createNotification } from "@app/components/notifications";
import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync, useTriggerPkiSyncImportCertificates } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync?: TPkiSync;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
pkiSync: TPkiSync;
onComplete: () => void;
};
const Content = ({ pkiSync, onComplete }: ContentProps) => {
const { id: syncId, destination, projectId } = pkiSync;
const destinationName = PKI_SYNC_MAP[destination].name;
const triggerImportCertificates = useTriggerPkiSyncImportCertificates();
const handleTriggerImportCertificates = async () => {
try {
await triggerImportCertificates.mutateAsync({
syncId,
projectId
});
createNotification({
text: `Successfully triggered certificate import for ${destinationName} Sync`,
type: "success"
});
onComplete();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to trigger certificate import for ${destinationName} Sync`,
type: "error"
});
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleTriggerImportCertificates();
}}
>
<p className="mb-8 text-sm text-mineshaft-200">
Are you sure you want to import certificates from this {destinationName} destination into
Infisical?
</p>
<p className="mb-6 text-xs text-bunker-300">
This operation will retrieve certificates from {destinationName} and make them available in
your PKI collection. Only certificates that are not already imported will be processed.
</p>
<div className="mt-8 flex w-full items-center justify-between gap-2">
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
<Button
type="submit"
isLoading={triggerImportCertificates.isPending}
colorSchema="secondary"
>
Import Certificates
</Button>
</div>
</form>
);
};
export const PkiSyncImportCertificatesModal = ({ isOpen, onOpenChange, pkiSync }: Props) => {
if (!pkiSync) return null;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Import Certificates"
subTitle={`Import certificates into Infisical from this ${destinationName} Sync destination.`}
>
<Content pkiSync={pkiSync} onComplete={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,112 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faDownload,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncData, PkiSyncStatus } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: PkiSyncData;
className?: string;
mini?: boolean;
};
export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) => {
const { importStatus, lastImportMessage, lastImportedAt, destination } = pkiSync;
const [hide, setHide] = useState(importStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name;
useEffect(() => {
if (importStatus === PkiSyncStatus.Succeeded) {
setTimeout(() => setHide(true), 3000);
} else {
setHide(false);
}
}, [importStatus]);
const failureMessage = useMemo(() => {
if (importStatus === PkiSyncStatus.Failed) {
if (lastImportMessage)
try {
return JSON.stringify(JSON.parse(lastImportMessage), null, 2);
} catch {
return lastImportMessage;
}
return "An Unknown Error Occurred.";
}
return null;
}, [importStatus, lastImportMessage]);
if (!importStatus || hide) return null;
let variant: BadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let tooltipContent: ReactNode;
switch (importStatus) {
case PkiSyncStatus.Pending:
case PkiSyncStatus.Running:
variant = "primary";
label = "Importing Certificates...";
tooltipContent = `Importing certificates from ${destinationName}. This may take a moment.`;
icon = faDownload;
break;
case PkiSyncStatus.Failed:
variant = "danger";
label = "Failed to Import Certificates";
icon = faTriangleExclamation;
tooltipContent = (
<div className="flex flex-col gap-2 whitespace-normal py-1">
{failureMessage && (
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
<div className="text-xs">
{mini ? "Failed to Import Certificates" : "Failure Reason"}
</div>
</div>
<div className="rounded bg-mineshaft-600 p-2 text-xs">{failureMessage}</div>
</div>
)}
</div>
);
break;
case PkiSyncStatus.Succeeded:
default:
// only show success for a bit...
if (lastImportedAt && differenceInSeconds(new Date(), lastImportedAt) > 15) return null;
tooltipContent = "Successfully imported certificates.";
variant = "success";
label = "Certificates Imported";
icon = faCheck;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
</Tooltip>
);
};
@@ -0,0 +1,49 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
destination: PkiSync;
isConfigured: boolean;
};
export const PkiSyncModalHeader = ({ destination, isConfigured }: Props) => {
const destinationDetails = PKI_SYNC_MAP[destination];
return (
<div className="flex w-full items-start gap-2">
<img
alt={`${destinationDetails.name} logo`}
src={`/images/integrations/${destinationDetails.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
{destinationDetails.name} Certificate Sync
<a
target="_blank"
href={`https://infisical.com/docs/integrations/pki-syncs/${destination}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured
? `Edit ${destinationDetails.name} Certificate Sync`
: `Sync certificates to ${destinationDetails.name}`}
</p>
</div>
</div>
);
};
@@ -0,0 +1,86 @@
import { createNotification } from "@app/components/notifications";
import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync, useTriggerPkiSyncRemoveCertificates } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync?: TPkiSync;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
pkiSync: TPkiSync;
onComplete: () => void;
};
const Content = ({ pkiSync, onComplete }: ContentProps) => {
const { id: syncId, destination, projectId } = pkiSync;
const destinationName = PKI_SYNC_MAP[destination].name;
const triggerRemoveCertificates = useTriggerPkiSyncRemoveCertificates();
const handleTriggerRemoveCertificates = async () => {
try {
await triggerRemoveCertificates.mutateAsync({
syncId,
projectId
});
createNotification({
text: `Successfully triggered certificate removal for ${destinationName} Sync`,
type: "success"
});
onComplete();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to trigger certificate removal for ${destinationName} Sync`,
type: "error"
});
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleTriggerRemoveCertificates();
}}
>
<p className="mb-8 text-sm text-mineshaft-200">
Are you sure you want to remove certificates synced by Infisical from this {destinationName}{" "}
destination?
</p>
<div className="mt-8 flex w-full items-center justify-between gap-2">
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
<Button type="submit" isLoading={triggerRemoveCertificates.isPending} colorSchema="danger">
Remove Certificates
</Button>
</div>
</form>
);
};
export const PkiSyncRemoveCertificatesModal = ({ isOpen, onOpenChange, pkiSync }: Props) => {
if (!pkiSync) return null;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Remove Certificates"
subTitle={`Remove certificates synced by Infisical from this ${destinationName} Sync destination.`}
>
<Content pkiSync={pkiSync} onComplete={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,112 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faEraser,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncData, PkiSyncStatus } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: PkiSyncData;
className?: string;
mini?: boolean;
};
export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) => {
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = pkiSync;
const [hide, setHide] = useState(removeStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name;
useEffect(() => {
if (removeStatus === PkiSyncStatus.Succeeded) {
setTimeout(() => setHide(true), 3000);
} else {
setHide(false);
}
}, [removeStatus]);
const failureMessage = useMemo(() => {
if (removeStatus === PkiSyncStatus.Failed) {
if (lastRemoveMessage)
try {
return JSON.stringify(JSON.parse(lastRemoveMessage), null, 2);
} catch {
return lastRemoveMessage;
}
return "An Unknown Error Occurred.";
}
return null;
}, [removeStatus, lastRemoveMessage]);
if (!removeStatus || hide) return null;
let variant: BadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let tooltipContent: ReactNode;
switch (removeStatus) {
case PkiSyncStatus.Pending:
case PkiSyncStatus.Running:
variant = "primary";
label = "Removing Certificates...";
tooltipContent = `Removing certificates from ${destinationName}. This may take a moment.`;
icon = faEraser;
break;
case PkiSyncStatus.Failed:
variant = "danger";
label = "Failed to Remove Certificates";
icon = faTriangleExclamation;
tooltipContent = (
<div className="flex flex-col gap-2 whitespace-normal py-1">
{failureMessage && (
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
<div className="text-xs">
{mini ? "Failed to Remove Certificates" : "Failure Reason"}
</div>
</div>
<div className="rounded bg-mineshaft-600 p-2 text-xs">{failureMessage}</div>
</div>
)}
</div>
);
break;
case PkiSyncStatus.Succeeded:
default:
// only show success for a bit...
if (lastRemovedAt && differenceInSeconds(new Date(), lastRemovedAt) > 15) return null;
tooltipContent = "Successfully removed certificates.";
variant = "success";
label = "Certificates Removed";
icon = faCheck;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
</Tooltip>
);
};
@@ -0,0 +1,155 @@
import { useMemo } from "react";
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { PkiSync, usePkiSyncOptions } from "@app/hooks/api/pkiSyncs";
import { UpgradePlanModal } from "../license/UpgradePlanModal";
type Props = {
onSelect: (destination: PkiSync) => void;
};
export const PkiSyncSelect = ({ onSelect }: Props) => {
const { subscription } = useSubscription();
const { isPending, data: pkiSyncOptions } = usePkiSyncOptions();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
initPerPage: 16
});
const filteredOptions = useMemo(
() =>
pkiSyncOptions?.filter(({ destination }) => {
const { name } = PKI_SYNC_MAP[destination];
return (
name?.toLowerCase().includes(search.trim().toLowerCase()) ||
destination.toLowerCase().includes(search.toLowerCase())
);
}) ?? [],
[pkiSyncOptions, search]
);
useResetPageHelper({
totalCount: filteredOptions.length,
offset,
setPage
});
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search options..."
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
/>
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
{filteredOptions.slice(offset, perPage * page)?.map(({ destination, enterprise }) => {
const { image, name } = PKI_SYNC_MAP[destination];
return (
<button
key={name}
type="button"
onClick={() =>
enterprise && !subscription.enterpriseSecretSyncs
? handlePopUpOpen("upgradePlan")
: onSelect(destination)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
height={40}
width={40}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
{!filteredOptions?.length && (
<EmptyState
className="col-span-full mt-40"
title="No Certificate Syncs match search"
icon={faSearch}
/>
)}
</div>
{Boolean(filteredOptions.length) && (
<Pagination
startAdornment={
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more services.</p>
<p>
{`If you don't see the third-party
service you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
<span className="text-xs">
Don&#39;t see the third-party service you&#39;re looking for?
</span>
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
</div>
</Tooltip>
}
count={filteredOptions.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
perPageList={[16]}
/>
)}
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use every Certificate Sync if you switch to Infisical's Enterprise plan."
/>
</div>
);
};
@@ -0,0 +1,55 @@
import {
faCheck,
faExclamationTriangle,
faHourglass,
faRotate,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge";
import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs";
type Props = {
status: PkiSyncStatus;
} & Omit<BadgeProps, "children" | "variant">;
export const PkiSyncStatusBadge = ({ status }: Props) => {
let variant: BadgeProps["variant"];
let text: string;
let icon: IconDefinition;
switch (status) {
case PkiSyncStatus.Failed:
variant = "danger";
text = "Failed to Sync";
icon = faExclamationTriangle;
break;
case PkiSyncStatus.Succeeded:
variant = "success";
text = "Synced";
icon = faCheck;
break;
case PkiSyncStatus.Pending:
variant = "primary";
text = "Queued";
icon = faHourglass;
break;
case PkiSyncStatus.Running:
default:
variant = "primary";
text = "Syncing";
icon = faRotate;
break;
}
return (
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
<FontAwesomeIcon
icon={icon}
className={[PkiSyncStatus.Running].includes(status) ? "animate-spin" : ""}
/>
<span>{text}</span>
</Badge>
);
};
@@ -0,0 +1,121 @@
import { faPlug, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Badge,
EmptyState,
IconButton,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { PkiSyncData } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSyncs: PkiSyncData[];
onEdit: (pkiSync: PkiSyncData) => void;
onDelete: (pkiSync: PkiSyncData) => void;
onTrigger: (pkiSync: PkiSyncData) => void;
};
const getSyncStatusBadge = (status?: string) => {
switch (status) {
case "SUCCESS":
return <Badge variant="success">Success</Badge>;
case "FAILED":
return <Badge variant="danger">Failed</Badge>;
case "RUNNING":
return <Badge variant="primary">Running</Badge>;
case "PENDING":
default:
return <Badge variant="primary">Pending</Badge>;
}
};
export const PkiSyncTable = ({ pkiSyncs, onEdit, onDelete, onTrigger }: Props) => {
if (!pkiSyncs.length) {
return (
<div className="flex h-96 items-center justify-center">
<EmptyState title="No PKI syncs created" icon={faPlug}>
Start by creating a PKI sync to synchronize certificates with external services
</EmptyState>
</div>
);
}
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Destination</Th>
<Th>Connection</Th>
<Th>Auto Sync</Th>
<Th>Status</Th>
<Th>Last Sync</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{pkiSyncs.map((pkiSync) => (
<Tr
key={pkiSync.id}
className="cursor-pointer hover:bg-mineshaft-700"
onClick={() => onEdit(pkiSync)}
>
<Td>{pkiSync.name}</Td>
<Td>
<div className="flex items-center space-x-2">
<Badge variant="primary">{pkiSync.destination}</Badge>
</div>
</Td>
<Td>{pkiSync.appConnectionName || "Unknown"}</Td>
<Td>
<Badge variant={pkiSync.isAutoSyncEnabled ? "success" : "danger"}>
{pkiSync.isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge>
</Td>
<Td>{getSyncStatusBadge(pkiSync.syncStatus ?? undefined)}</Td>
<Td>
{pkiSync.lastSyncedAt
? new Date(pkiSync.lastSyncedAt).toLocaleDateString()
: "Never"}
</Td>
<Td>
<div className="flex items-center space-x-2">
<IconButton
size="sm"
variant="plain"
ariaLabel="Trigger sync"
onClick={(e) => {
e.stopPropagation();
onTrigger(pkiSync);
}}
>
<FontAwesomeIcon icon={faRefresh} />
</IconButton>
<IconButton
size="sm"
variant="plain"
ariaLabel="Delete"
onClick={(e) => {
e.stopPropagation();
onDelete(pkiSync);
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
);
};
@@ -0,0 +1,37 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input } from "@app/components/v2";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
import { TPkiSyncForm } from "./schemas";
export const AzureKeyVaultPkiSyncFields = () => {
const { control, setValue } = useFormContext<
TPkiSyncForm & { destination: PkiSync.AzureKeyVault }
>();
return (
<>
<PkiSyncConnectionField
onChange={() => {
setValue("destinationConfig.vaultBaseUrl", "");
}}
/>
<Controller
name="destinationConfig.vaultBaseUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vault Base URL"
tooltipText="Enter your Azure Key Vault URL. This is the base URL for your Azure Key Vault, e.g. https://example.vault.azure.net."
>
<Input {...field} placeholder="https://example.vault.azure.net" />
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1,243 @@
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Switch } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas";
type Props = {
onComplete: (pkiSync: TPkiSync) => void;
destination: PkiSync;
onCancel: () => void;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [
{ name: "Source", key: "source", fields: ["subscriberId"] },
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
{ name: "Details", key: "details", fields: ["name", "description"] },
{ name: "Review", key: "review", fields: [] }
];
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) => {
const createPkiSync = useCreatePkiSync();
const { currentWorkspace } = useWorkspace();
const { name: destinationName } = PKI_SYNC_MAP[destination];
const [showConfirmation, setShowConfirmation] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const { syncOption } = usePkiSyncOption(destination);
const formMethods = useForm<TPkiSyncForm>({
resolver: zodResolver(PkiSyncFormSchema),
defaultValues: {
destination,
isAutoSyncEnabled: true,
syncOptions: {
canImportCertificates: syncOption?.canImportCertificates ?? true,
canRemoveCertificates: syncOption?.canRemoveCertificates ?? true
}
} as Partial<TPkiSyncForm>,
reValidateMode: "onChange"
});
const onSubmit = async ({ connection, ...formData }: TPkiSyncForm) => {
try {
const pkiSync = await createPkiSync.mutateAsync({
...formData,
connectionId: connection.id,
projectId: currentWorkspace.id
});
createNotification({
text: `Successfully added ${destinationName} Certificate Sync`,
type: "success"
});
onComplete(pkiSync);
} catch (err: any) {
console.error(err);
setShowConfirmation(false);
createNotification({
title: `Failed to add ${destinationName} Certificate Sync`,
text: err.message,
type: "error"
});
}
};
const handlePrev = () => {
if (selectedTabIndex === 0) {
onCancel();
return;
}
setSelectedTabIndex((prev) => prev - 1);
};
const { handleSubmit, trigger, control } = formMethods;
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
const handleNext = async () => {
if (isFinalStep) {
setShowConfirmation(true);
return;
}
const isValid = await isStepValid(selectedTabIndex);
if (!isValid) return;
setSelectedTabIndex((prev) => prev + 1);
};
const isTabEnabled = async (index: number) => {
let isEnabled = true;
for (let i = index - 1; i >= 0; i -= 1) {
// eslint-disable-next-line no-await-in-loop
isEnabled = isEnabled && (await isStepValid(i));
}
return isEnabled;
};
if (showConfirmation)
return (
<>
<div className="flex flex-col rounded-sm border border-l-[2px] border-mineshaft-600 border-l-primary bg-mineshaft-700/80 px-4 py-3">
<div className="mb-1 flex items-center text-sm">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
Certificate Sync Behavior
</div>
<p className="mt-1 text-sm text-bunker-200">
Certificate Syncs are the source of truth for connected third-party services. Any
certificate, including associated data, not present or imported in Infisical before
syncing will be overwritten, and changes made directly in the connected service outside
of infisical may also be overwritten by future syncs.
</p>
</div>
<div className="mt-4 flex gap-4">
<Button
isDisabled={createPkiSync.isPending}
isLoading={createPkiSync.isPending}
onClick={handleSubmit(onSubmit)}
colorSchema="secondary"
>
I Understand
</Button>
<Button
isDisabled={createPkiSync.isPending}
variant="plain"
onClick={() => setShowConfirmation(false)}
colorSchema="secondary"
>
Cancel
</Button>
</div>
</>
);
return (
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
<FormProvider {...formMethods}>
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
{FORM_TABS.map((tab, index) => (
<Tab
onClick={async (e) => {
e.preventDefault();
const isEnabled = await isTabEnabled(index);
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
}}
className={({ selected }) =>
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
key={tab.key}
>
{index + 1}. {tab.name}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<PkiSyncSourceFields />
</Tab.Panel>
<Tab.Panel>
<PkiSyncDestinationFields />
</Tab.Panel>
<Tab.Panel>
<PkiSyncOptionsFields />
<Controller
control={control}
name="isAutoSyncEnabled"
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<FormControl
helperText={
value
? "Certificates will automatically be synced when changes occur in the source subscriber."
: "Certificates will not automatically be synced when changes occur in the source subscriber. You can still trigger syncs manually."
}
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="auto-sync-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p className="w-[8.4rem]">Auto-Sync {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
);
}}
/>
</Tab.Panel>
<Tab.Panel>
<PkiSyncDetailsFields />
</Tab.Panel>
<Tab.Panel>
<PkiSyncReviewFields />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</FormProvider>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button onClick={handleNext} colorSchema="secondary">
{isFinalStep ? "Create Sync" : "Next"}
</Button>
{selectedTabIndex > 0 && (
<Button onClick={handlePrev} colorSchema="secondary">
Back
</Button>
)}
</div>
</form>
);
};
@@ -0,0 +1,107 @@
import { ReactNode } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { PkiSyncEditFields } from "@app/components/pki-syncs/types";
import { Button, ModalClose } from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync, useUpdatePkiSync } from "@app/hooks/api/pkiSyncs";
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
import { TPkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas";
type Props = {
onComplete: (pkiSync: TPkiSync) => void;
pkiSync: TPkiSync;
fields: PkiSyncEditFields;
};
export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
const updatePkiSync = useUpdatePkiSync();
const { name: destinationName } = PKI_SYNC_MAP[pkiSync.destination];
const formMethods = useForm<TPkiSyncForm>({
resolver: zodResolver(UpdatePkiSyncFormSchema),
defaultValues: {
...pkiSync,
description: pkiSync.description ?? "",
connection: {
id: pkiSync.connectionId,
name: pkiSync.appConnectionName
}
} as Partial<TPkiSyncForm>,
reValidateMode: "onChange"
});
const onSubmit = async ({ connection, ...formData }: TPkiSyncForm) => {
try {
const updatedPkiSync = await updatePkiSync.mutateAsync({
syncId: pkiSync.id,
...formData,
connectionId: connection.id,
projectId: pkiSync.projectId
});
createNotification({
text: `Successfully updated ${destinationName} PKI Sync`,
type: "success"
});
onComplete(updatedPkiSync);
} catch (err: any) {
console.error(err);
createNotification({
title: `Failed to update ${destinationName} PKI Sync`,
text: err.message,
type: "error"
});
}
};
let Component: ReactNode;
switch (fields) {
case PkiSyncEditFields.Destination:
Component = <PkiSyncDestinationFields />;
break;
case PkiSyncEditFields.Options:
Component = <PkiSyncOptionsFields />;
break;
case PkiSyncEditFields.Source:
Component = <PkiSyncSourceFields />;
break;
case PkiSyncEditFields.Details:
default:
Component = <PkiSyncDetailsFields />;
break;
}
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = formMethods;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...formMethods}>{Component}</FormProvider>
<div className="flex w-full justify-between gap-4 pt-4">
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
<Button
isLoading={isSubmitting}
isDisabled={!isDirty || isSubmitting}
type="submit"
colorSchema="secondary"
>
Update PKI Sync
</Button>
</div>
</form>
);
};
@@ -0,0 +1,86 @@
import { Controller, useFormContext } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { TPkiSyncForm } from "./schemas";
type Props = {
onChange?: VoidFunction;
};
export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TPkiSyncForm>();
const destination = watch("destination");
const app = PKI_SYNC_CONNECTION_MAP[destination];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
);
const appName = APP_CONNECTION_MAP[PKI_SYNC_CONNECTION_MAP[destination]].name;
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Specify the {appName} Connection to use to connect to {connectionName} and configure
destination parameters.
</p>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="App Connections can be created from the Organization Settings page."
isError={Boolean(error)}
errorText={error?.message}
label={`${connectionName} Connection`}
>
<FilterableSelect
value={value}
onChange={(newValue) => {
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{availableConnections?.length === 0 && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
)}
</p>
)}
</>
);
};
@@ -0,0 +1,19 @@
import { useFormContext } from "react-hook-form";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields";
import { TPkiSyncForm } from "./schemas";
export const PkiSyncDestinationFields = () => {
const { watch } = useFormContext<TPkiSyncForm>();
const destination = watch("destination");
switch (destination) {
case PkiSync.AzureKeyVault:
return <AzureKeyVaultPkiSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
};
@@ -0,0 +1,51 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input, TextArea } from "@app/components/v2";
import { TPkiSyncForm } from "./schemas";
export const PkiSyncDetailsFields = () => {
const { control } = useFormContext<TPkiSyncForm>();
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Provide a name and description for this Certificate Sync.
</p>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
helperText="Must be slug-friendly"
isError={Boolean(error)}
errorText={error?.message}
label="Name"
>
<Input value={value} onChange={onChange} placeholder="my-certificate-sync" />
</FormControl>
)}
control={control}
name="name"
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Description"
>
<TextArea
value={value}
onChange={onChange}
placeholder="Describe the purpose of this sync..."
className="!resize-none"
rows={4}
/>
</FormControl>
)}
control={control}
name="description"
/>
</>
);
};
@@ -0,0 +1,89 @@
import { Controller, useFormContext } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Switch, Tooltip } from "@app/components/v2";
import { TPkiSyncForm } from "../schemas";
export const PkiSyncOptionsFields = () => {
const { control } = useFormContext<TPkiSyncForm>();
return (
<>
<p className="mb-4 text-sm text-bunker-300">Configure how certificates should be synced.</p>
<Controller
control={control}
name="syncOptions.canImportCertificates"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="can-import-certificates"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>
Auto Import Certificates{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
When enabled, Infisical will automatically import certificates from the PKI
subscriber to the destination during sync operations.
</p>
<p className="mt-4">
This allows you to automatically populate your destination with certificates
issued by your Certificate Authority.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
<Controller
control={control}
name="syncOptions.canRemoveCertificates"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="can-remove-certificates"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>
Disable Certificate Removal{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
When enabled, Infisical will <span className="font-semibold">not</span>{" "}
remove certificates from the destination during a sync.
</p>
<p className="mt-4">
Enable this option if you intend to manage some certificates manually
outside of Infisical.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1 @@
export { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
@@ -0,0 +1,100 @@
import { useFormContext } from "react-hook-form";
import { Badge, GenericFieldLabel } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useListWorkspacePkiSubscribers } from "@app/hooks/api/workspace";
import { TPkiSyncForm } from "./schemas";
export const PkiSyncReviewFields = () => {
const { watch } = useFormContext<TPkiSyncForm>();
const { currentWorkspace } = useWorkspace();
const { data: pkiSubscribers = [] } = useListWorkspacePkiSubscribers(currentWorkspace?.id || "");
const getSubscriberName = (subscriberId?: string) => {
const subscriber = pkiSubscribers.find((sub) => sub.id === subscriberId);
return subscriber?.name || "Unknown";
};
const {
name,
description,
connection,
subscriberId,
syncOptions,
destination,
destinationConfig,
isAutoSyncEnabled
} = watch();
const destinationName = PKI_SYNC_MAP[destination].name;
return (
<div className="mb-4 flex flex-col gap-6">
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Source</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="PKI Subscriber">
{getSubscriberName(subscriberId)}
</GenericFieldLabel>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Destination</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Connection">{connection?.name}</GenericFieldLabel>
<GenericFieldLabel label="Service">{destinationName}</GenericFieldLabel>
{destinationConfig && "vaultBaseUrl" in destinationConfig && (
<GenericFieldLabel label="Vault URL">
{destinationConfig.vaultBaseUrl}
</GenericFieldLabel>
)}
</div>
</div>
<div className="flex flex-col gap-3">
<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">
<GenericFieldLabel label="Auto-Sync">
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
<GenericFieldLabel label="Upload Certificates">
<Badge variant="success">Always Enabled</Badge>
</GenericFieldLabel>
{syncOptions?.canImportCertificates !== undefined && (
<GenericFieldLabel label="Import Certificates">
<Badge variant={syncOptions.canImportCertificates ? "success" : "danger"}>
{syncOptions.canImportCertificates ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
)}
{syncOptions?.canRemoveCertificates !== undefined && (
<GenericFieldLabel label="Remove Certificates">
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>
{syncOptions.canRemoveCertificates ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
)}
</div>
</div>
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Details</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
</div>
</div>
</div>
);
};
@@ -0,0 +1,53 @@
import { Controller, useFormContext } from "react-hook-form";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useListWorkspacePkiSubscribers } from "@app/hooks/api/workspace";
import { TPkiSyncForm } from "./schemas";
export const PkiSyncSourceFields = () => {
const { control } = useFormContext<TPkiSyncForm>();
const { currentWorkspace } = useWorkspace();
const { data: pkiSubscribers = [], isLoading } = useListWorkspacePkiSubscribers(
currentWorkspace?.id || ""
);
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Specify the PKI subscriber where you would like to sync certificates from.
</p>
<Controller
control={control}
name="subscriberId"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl label="PKI Subscriber" isError={Boolean(error)} errorText={error?.message}>
<FilterableSelect
value={pkiSubscribers.find((sub) => sub.id === value)}
onChange={(selectedSubscriber) => {
if (Array.isArray(selectedSubscriber)) return;
if (
selectedSubscriber &&
!Array.isArray(selectedSubscriber) &&
"id" in selectedSubscriber
) {
onChange(selectedSubscriber.id);
} else {
onChange(undefined);
}
}}
options={pkiSubscribers}
isLoading={isLoading}
placeholder="Select PKI subscriber..."
getOptionLabel={(option) => option?.name || ""}
getOptionValue={(option) => option?.id || ""}
/>
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1,8 @@
export { CreatePkiSyncForm } from "./CreatePkiSyncForm";
export { EditPkiSyncForm } from "./EditPkiSyncForm";
export { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
export { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
export { PkiSyncOptionsFields } from "./PkiSyncOptionsFields/PkiSyncOptionsFields";
export { PkiSyncReviewFields } from "./PkiSyncReviewFields";
export { PkiSyncSourceFields } from "./PkiSyncSourceFields";
export { PkiSyncFormSchema, type TPkiSyncForm } from "./schemas";
@@ -0,0 +1,35 @@
import { z } from "zod";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
export const PkiSyncFormSchema = z.object({
name: z.string().trim().min(1, "Name is required"),
description: z.string().optional(),
destination: z.nativeEnum(PkiSync),
isAutoSyncEnabled: z.boolean().default(true),
subscriberId: z.string().min(1, "PKI Subscriber is required"),
connection: z.object({
id: z.string(),
name: z.string()
}),
destinationConfig: z.object({
vaultBaseUrl: z.string().url("Valid URL is required")
}),
syncOptions: z.object({
canImportCertificates: z.boolean().default(true),
canRemoveCertificates: z.boolean().default(true)
})
});
export type TPkiSyncForm = z.infer<typeof PkiSyncFormSchema>;
export const UpdatePkiSyncFormSchema = PkiSyncFormSchema.partial().merge(
z.object({
name: z.string().trim().min(1, "Name is required"),
destination: z.nativeEnum(PkiSync),
connection: z.object({
id: z.string(),
name: z.string()
})
})
);
@@ -0,0 +1,9 @@
export { CreatePkiSyncModal } from "./CreatePkiSyncModal";
export { DeletePkiSyncModal } from "./DeletePkiSyncModal";
export { EditPkiSyncModal } from "./EditPkiSyncModal";
export { PkiSyncImportCertificatesModal } from "./PkiSyncImportCertificatesModal";
export { PkiSyncImportStatusBadge } from "./PkiSyncImportStatusBadge";
export { PkiSyncRemoveCertificatesModal } from "./PkiSyncRemoveCertificatesModal";
export { PkiSyncRemoveStatusBadge } from "./PkiSyncRemoveStatusBadge";
export { PkiSyncStatusBadge } from "./PkiSyncStatusBadge";
export { PkiSyncTable } from "./PkiSyncTable";
@@ -0,0 +1,6 @@
export enum PkiSyncEditFields {
Details = "details",
Options = "options",
Source = "source",
Destination = "destination"
}