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"
}
+8
View File
@@ -320,6 +320,14 @@ export const ROUTE_PATHS = Object.freeze({
PkiSubscriberDetailsByIDPage: setRoute(
"/projects/cert-management/$projectId/subscribers/$subscriberName",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName"
),
IntegrationsListPage: setRoute(
"/projects/cert-management/$projectId/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/"
),
PkiSyncDetailsByIDPage: setRoute(
"/projects/cert-management/$projectId/integrations/$syncId",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId"
)
},
Ssh: {
@@ -62,6 +62,16 @@ export enum ProjectPermissionSecretSyncActions {
RemoveSecrets = "remove-secrets"
}
export enum ProjectPermissionPkiSyncActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
SyncCertificates = "sync-certificates",
ImportCertificates = "import-certificates",
RemoveCertificates = "remove-certificates"
}
export enum ProjectPermissionIdentityActions {
Read = "read",
Create = "create",
@@ -175,6 +185,7 @@ export type IdentityManagementSubjectFields = {
export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretSyncs
| ProjectPermissionSub.PkiSyncs
| ProjectPermissionSub.Secrets
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.Identity
@@ -258,6 +269,7 @@ export enum ProjectPermissionSub {
Kms = "kms",
Cmek = "cmek",
SecretSyncs = "secret-syncs",
PkiSyncs = "pki-syncs",
Kmip = "kmip",
Commits = "commits",
SecretScanningDataSources = "secret-scanning-data-sources",
@@ -302,6 +314,10 @@ export type SecretSyncSubjectFields = {
secretPath: string;
};
export type PkiSyncSubjectFields = {
subscriberId: string;
};
export type SecretRotationSubjectFields = {
environment: string;
secretPath: string;
@@ -349,6 +365,13 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
)
]
| [
ProjectPermissionPkiSyncActions,
(
| ProjectPermissionSub.PkiSyncs
| (ForcedSubject<ProjectPermissionSub.PkiSyncs> & PkiSyncSubjectFields)
)
]
| [
ProjectPermissionActions,
(
+19
View File
@@ -0,0 +1,19 @@
import { AppConnection } from "@app/hooks/api/appConnections";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
export const PKI_SYNC_MAP: Record<
PkiSync,
{
name: string;
image: string;
}
> = {
[PkiSync.AzureKeyVault]: {
name: "Azure Key Vault",
image: "Microsoft Azure.png"
}
};
export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault
};
@@ -1,3 +1,4 @@
export * from "./enums";
export * from "./mutations";
export * from "./queries";
export * from "./types";
@@ -140,6 +140,14 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.SECRET_SYNC_SYNC_SECRETS]: "Secret Sync synced secrets",
[EventType.SECRET_SYNC_IMPORT_SECRETS]: "Secret Sync imported secrets",
[EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets",
[EventType.GET_PKI_SYNCS]: "List PKI syncs",
[EventType.GET_PKI_SYNC]: "Get PKI sync",
[EventType.CREATE_PKI_SYNC]: "Create PKI sync",
[EventType.UPDATE_PKI_SYNC]: "Update PKI sync",
[EventType.DELETE_PKI_SYNC]: "Delete PKI sync",
[EventType.PKI_SYNC_SYNC_CERTIFICATES]: "PKI Sync synced certificates",
[EventType.PKI_SYNC_IMPORT_CERTIFICATES]: "PKI Sync imported certificates",
[EventType.PKI_SYNC_REMOVE_CERTIFICATES]: "PKI Sync removed certificates",
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER]:
"OIDC group membership mapping assigned user to groups",
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
@@ -148,6 +148,14 @@ export enum EventType {
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
GET_PKI_SYNCS = "get-pki-syncs",
GET_PKI_SYNC = "get-pki-sync",
CREATE_PKI_SYNC = "create-pki-sync",
UPDATE_PKI_SYNC = "update-pki-sync",
DELETE_PKI_SYNC = "delete-pki-sync",
PKI_SYNC_SYNC_CERTIFICATES = "pki-sync-sync-certificates",
PKI_SYNC_IMPORT_CERTIFICATES = "pki-sync-import-certificates",
PKI_SYNC_REMOVE_CERTIFICATES = "pki-sync-remove-certificates",
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
CREATE_KMIP_CLIENT = "create-kmip-client",
+10
View File
@@ -0,0 +1,10 @@
export enum PkiSync {
AzureKeyVault = "azure-key-vault"
}
export enum PkiSyncStatus {
Pending = "pending",
Running = "running",
Succeeded = "succeeded",
Failed = "failed"
}
+7
View File
@@ -0,0 +1,7 @@
export * from "./enums";
export * from "./mutations";
export * from "./queries";
export * from "./types";
// Legacy alias for backward compatibility
export type PkiSyncData = import("./types").TPkiSync;
@@ -0,0 +1,125 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { pkiSyncKeys } from "@app/hooks/api/pkiSyncs/queries";
import {
TCreatePkiSyncDTO,
TDeletePkiSyncDTO,
TPkiSyncResponse,
TTriggerPkiSyncImportCertificatesDTO,
TTriggerPkiSyncRemoveCertificatesDTO,
TTriggerPkiSyncSyncCertificatesDTO,
TUpdatePkiSyncDTO
} from "@app/hooks/api/pkiSyncs/types";
export const useCreatePkiSync = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (params: TCreatePkiSyncDTO) => {
const { data } = await apiRequest.post<TPkiSyncResponse>("/api/v1/pki-syncs", params);
return data.pkiSync;
},
onSuccess: (_, { projectId }) =>
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) })
});
};
export const useUpdatePkiSync = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId, ...params }: TUpdatePkiSyncDTO) => {
const { data } = await apiRequest.patch<TPkiSyncResponse>(
`/api/v1/pki-syncs/${syncId}`,
params,
{ params: { projectId } }
);
return data.pkiSync;
},
onSuccess: (_, { syncId, projectId }) => {
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId) });
}
});
};
export const useDeletePkiSync = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId }: TDeletePkiSyncDTO) => {
const { data } = await apiRequest.delete(`/api/v1/pki-syncs/${syncId}`, {
params: { projectId }
});
return data;
},
onSuccess: (_, { syncId, projectId }) => {
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId) });
}
});
};
export const useTriggerPkiSyncSyncCertificates = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId }: TTriggerPkiSyncSyncCertificatesDTO) => {
const { data } = await apiRequest.post(
`/api/v1/pki-syncs/${syncId}/sync`,
{},
{
params: { projectId }
}
);
return data;
},
onSuccess: (_, { syncId, projectId }) => {
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId) });
}
});
};
export const useTriggerPkiSyncImportCertificates = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId }: TTriggerPkiSyncImportCertificatesDTO) => {
const { data } = await apiRequest.post(
`/api/v1/pki-syncs/${syncId}/import`,
{},
{
params: { projectId }
}
);
return data;
},
onSuccess: (_, { syncId, projectId }) => {
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId) });
}
});
};
export const useTriggerPkiSyncRemoveCertificates = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId }: TTriggerPkiSyncRemoveCertificatesDTO) => {
const { data } = await apiRequest.post(
`/api/v1/pki-syncs/${syncId}/remove`,
{},
{
params: { projectId }
}
);
return data;
},
onSuccess: (_, { syncId, projectId }) => {
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId) });
}
});
};
@@ -0,0 +1,88 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { PkiSync, TPkiSyncOption } from "@app/hooks/api/pkiSyncs";
import {
TListPkiSyncOptions,
TListPkiSyncs,
TPkiSync,
TPkiSyncResponse
} from "@app/hooks/api/pkiSyncs/types";
export const pkiSyncKeys = {
all: ["pki-sync"] as const,
options: () => [...pkiSyncKeys.all, "options"] as const,
list: (projectId: string) => [...pkiSyncKeys.all, "list", projectId] as const,
byId: (syncId: string) => [...pkiSyncKeys.all, "by-id", syncId] as const
};
export const usePkiSyncOptions = (
options?: Omit<
UseQueryOptions<
TPkiSyncOption[],
unknown,
TPkiSyncOption[],
ReturnType<typeof pkiSyncKeys.options>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: pkiSyncKeys.options(),
queryFn: async () => {
const { data } = await apiRequest.get<TListPkiSyncOptions>("/api/v1/pki-syncs/options");
return data.pkiSyncOptions;
},
...options
});
};
export const usePkiSyncOption = (destination: PkiSync) => {
const { data: syncOptions, isPending } = usePkiSyncOptions();
const syncOption = syncOptions?.find((option) => option.destination === destination);
return { syncOption, isPending };
};
export const fetchPkiSyncsByProjectId = async (projectId: string) => {
const { data } = await apiRequest.get<TListPkiSyncs>("/api/v1/pki-syncs", {
params: { projectId }
});
return data.pkiSyncs;
};
export const useListPkiSyncs = (
projectId: string,
options?: Omit<
UseQueryOptions<TPkiSync[], unknown, TPkiSync[], ReturnType<typeof pkiSyncKeys.list>>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: pkiSyncKeys.list(projectId),
queryFn: () => fetchPkiSyncsByProjectId(projectId),
...options
});
};
export const useGetPkiSync = (
{ syncId, projectId }: { syncId: string; projectId: string },
options?: Omit<
UseQueryOptions<TPkiSync, unknown, TPkiSync, ReturnType<typeof pkiSyncKeys.byId>>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: pkiSyncKeys.byId(syncId),
queryFn: async () => {
const { data } = await apiRequest.get<TPkiSyncResponse>(`/api/v1/pki-syncs/${syncId}`, {
params: { projectId }
});
return data.pkiSync;
},
...options
});
};
@@ -0,0 +1,16 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { PkiSync } from "../enums";
import { TRootPkiSync } from "./common";
export type TAzureKeyVaultPkiSync = TRootPkiSync & {
destination: PkiSync.AzureKeyVault;
destinationConfig: {
vaultBaseUrl: string;
};
connection: {
app: AppConnection.AzureKeyVault;
name: string;
id: string;
};
};
@@ -0,0 +1,44 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { PkiSyncStatus } from "../enums";
export type RootPkiSyncOptions = {
canImportCertificates: boolean;
canRemoveCertificates: boolean;
};
export type TRootPkiSync = {
id: string;
name: string;
description?: string | null;
connectionId: string;
createdAt: string;
updatedAt: string;
isAutoSyncEnabled: boolean;
projectId: string;
subscriberId?: string | null;
syncStatus: PkiSyncStatus | null;
lastSyncJobId: string | null;
lastSyncedAt: string | null;
lastSyncMessage: string | null;
importStatus: PkiSyncStatus | null;
lastImportJobId: string | null;
lastImportedAt: string | null;
lastImportMessage: string | null;
removeStatus: PkiSyncStatus | null;
lastRemoveJobId: string | null;
lastRemovedAt: string | null;
lastRemoveMessage: string | null;
syncOptions: RootPkiSyncOptions;
connection: {
app: AppConnection;
id: string;
name: string;
};
subscriber?: {
id: string;
name: string;
} | null;
appConnectionName?: string;
appConnectionApp?: string;
};
@@ -0,0 +1,57 @@
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { DiscriminativePick } from "@app/types";
import { TAzureKeyVaultPkiSync } from "./azure-key-vault-sync";
export type TPkiSyncOption = {
name: string;
destination: PkiSync;
canImportCertificates: boolean;
canRemoveCertificates: boolean;
enterprise?: boolean;
};
export type TPkiSync = TAzureKeyVaultPkiSync;
export type TListPkiSyncs = { pkiSyncs: TPkiSync[] };
export type TListPkiSyncOptions = { pkiSyncOptions: TPkiSyncOption[] };
export type TPkiSyncResponse = { pkiSync: TPkiSync };
export type TCreatePkiSyncDTO = DiscriminativePick<
TPkiSync,
| "name"
| "destinationConfig"
| "description"
| "connectionId"
| "syncOptions"
| "destination"
| "isAutoSyncEnabled"
> & { subscriberId?: string; projectId: string };
export type TUpdatePkiSyncDTO = Partial<Omit<TCreatePkiSyncDTO, "destination" | "projectId">> & {
syncId: string;
projectId: string;
};
export type TDeletePkiSyncDTO = {
syncId: string;
projectId: string;
};
export type TTriggerPkiSyncSyncCertificatesDTO = {
syncId: string;
projectId: string;
};
export type TTriggerPkiSyncImportCertificatesDTO = {
syncId: string;
projectId: string;
};
export type TTriggerPkiSyncRemoveCertificatesDTO = {
syncId: string;
projectId: string;
};
export * from "./common";
@@ -6,6 +6,7 @@ import {
faCog,
faFileLines,
faHome,
faLink,
faMobile,
faSitemap,
faStamp,
@@ -130,6 +131,23 @@ export const PkiManagerLayout = () => {
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/integrations"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faLink} />
</div>
Integrations
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
@@ -0,0 +1,69 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import { PkiSyncsTab } from "./components";
export const IntegrationsListPage = () => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
const { selectedTab } = useSearch({
from: ROUTE_PATHS.CertManager.IntegrationsListPage.id
});
const currentTab = selectedTab || IntegrationsListPageTabs.PkiSyncs;
const updateSelectedTab = (tab: string) => {
navigate({
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
search: {
selectedTab: tab as IntegrationsListPageTabs
},
params: {
projectId: currentWorkspace.id
}
});
};
return (
<>
<Helmet>
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="Manage your certificates in seconds" />
<meta name="og:description" content="Sync and manage PKI certificates across services" />
</Helmet>
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
<div className="mb-8">
<PageHeader
title="Integrations"
description="Manage integrations with third-party certificate services."
/>
<Tabs value={currentTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={IntegrationsListPageTabs.PkiSyncs}>Certificate Syncs</Tab>
</TabList>
<TabPanel value={IntegrationsListPageTabs.PkiSyncs}>
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionPkiSyncActions.Read}
a={ProjectPermissionSub.PkiSyncs}
>
<PkiSyncsTab />
</ProjectPermissionCan>
</TabPanel>
</Tabs>
</div>
</div>
</>
);
};
@@ -0,0 +1,14 @@
import { PkiSyncData } from "@app/hooks/api/pkiSyncs";
import { getPkiSyncDestinationColValues } from "../helpers";
import { PkiSyncTableCell } from "../PkiSyncTableCell";
type Props = {
pkiSync: PkiSyncData;
};
export const PkiSyncDestinationCol = ({ pkiSync }: Props) => {
const { primaryText, secondaryText } = getPkiSyncDestinationColValues(pkiSync);
return <PkiSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -0,0 +1 @@
export { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
@@ -0,0 +1,398 @@
import { useCallback, useMemo } from "react";
import { subject } from "@casl/ability";
import {
faBan,
faCalendarCheck,
faCheck,
faCopy,
faDownload,
faEllipsisV,
faEraser,
faInfoCircle,
faRotate,
faToggleOff,
faToggleOn,
faTrash,
faTriangleExclamation,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
PkiSyncImportStatusBadge,
PkiSyncRemoveStatusBadge,
PkiSyncStatusBadge
} from "@app/components/pki-syncs";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useToggle } from "@app/hooks";
import { PkiSyncData, PkiSyncStatus, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
import { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
import { PkiSyncTableCell } from "./PkiSyncTableCell";
type Props = {
pkiSync: PkiSyncData;
onDelete: (pkiSync: PkiSyncData) => void;
onTriggerSyncCertificates: (pkiSync: PkiSyncData) => void;
onTriggerImportCertificates: (pkiSync: PkiSyncData) => void;
onTriggerRemoveCertificates: (pkiSync: PkiSyncData) => void;
onToggleEnable: (pkiSync: PkiSyncData) => void;
};
export const PkiSyncRow = ({
pkiSync,
onDelete,
onTriggerSyncCertificates,
onTriggerImportCertificates,
onTriggerRemoveCertificates,
onToggleEnable
}: Props) => {
const navigate = useNavigate();
const {
id,
subscriberId,
lastSyncMessage,
destination,
lastSyncedAt,
name,
description,
syncStatus,
isAutoSyncEnabled,
projectId
} = pkiSync;
const { syncOption } = usePkiSyncOption(destination);
const destinationName = PKI_SYNC_MAP[destination].name;
const [isIdCopied, setIsIdCopied] = useToggle(false);
const handleCopyId = useCallback(() => {
setIsIdCopied.on();
navigator.clipboard.writeText(id);
createNotification({
text: "PKI Sync ID copied to clipboard",
type: "info"
});
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
}, [isIdCopied]);
const failureMessage = useMemo(() => {
if (syncStatus === PkiSyncStatus.Failed) {
if (lastSyncMessage)
try {
return JSON.stringify(JSON.parse(lastSyncMessage), null, 2);
} catch {
return lastSyncMessage;
}
return "An Unknown Error Occurred.";
}
return null;
}, [syncStatus, lastSyncMessage]);
const destinationDetails = PKI_SYNC_MAP[destination];
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || ""
});
return (
<Tr
onClick={() => {
console.log("PKI Sync navigation:", { syncId: id, projectId });
navigate({
to: ROUTE_PATHS.CertManager.PkiSyncDetailsByIDPage.path,
params: {
syncId: id,
projectId
}
});
}}
className={twMerge(
"group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700",
syncStatus === PkiSyncStatus.Failed && "bg-red/5 hover:bg-red/10"
)}
key={`sync-${id}`}
>
<Td>
<img
alt={`${destinationDetails.name} sync`}
src={`/images/integrations/${destinationDetails.image}`}
className="min-w-[1.75rem]"
/>
</Td>
<Td className="!min-w-[8rem] max-w-0">
<div>
<div className="flex w-full items-center">
<p className="truncate">{name}</p>
{description && (
<Tooltip content={description}>
<FontAwesomeIcon
icon={faInfoCircle}
size="xs"
className="ml-1 text-mineshaft-400"
/>
</Tooltip>
)}
</div>
<p className="truncate text-xs leading-4 text-bunker-300">{destinationDetails.name}</p>
</div>
</Td>
{subscriberId ? (
<PkiSyncTableCell primaryText={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
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
variant="primary"
>
<FontAwesomeIcon icon={faTriangleExclamation} />
<span>Source Deleted</span>
</Badge>
</div>
</Tooltip>
</Td>
)}
<PkiSyncDestinationCol pkiSync={pkiSync} />
<Td>
<div className="flex items-center gap-1">
{syncStatus && (
<Tooltip
position="left"
className="max-w-sm"
content={
[PkiSyncStatus.Succeeded, PkiSyncStatus.Failed].includes(syncStatus) ? (
<div className="flex flex-col gap-2 whitespace-normal py-1">
{lastSyncedAt && (
<div>
<div
className={`mb-2 flex self-start ${syncStatus === PkiSyncStatus.Failed ? "text-yellow" : "text-green"}`}
>
<FontAwesomeIcon
icon={faCalendarCheck}
className="ml-1 pr-1.5 pt-0.5 text-sm"
/>
<div className="text-xs">Last Synced</div>
</div>
<div className="rounded bg-mineshaft-600 p-2 text-xs">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
</div>
</div>
)}
{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">Failure Reason</div>
</div>
<div className="break-words rounded bg-mineshaft-600 p-2 text-xs">
{failureMessage}
</div>
</div>
)}
</div>
) : undefined
}
>
<div>
<PkiSyncStatusBadge status={syncStatus} />
</div>
</Tooltip>
)}
{!isAutoSyncEnabled && (
<Tooltip
className="text-xs"
content="Auto-Sync is disabled. Changes to the PKI subscriber will not be automatically synced to the destination."
>
<div>
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
{!syncStatus && "Auto-Sync Disabled"}
</Badge>
</div>
</Tooltip>
)}
<PkiSyncImportStatusBadge mini pkiSync={pkiSync} />
<PkiSyncRemoveStatusBadge mini pkiSync={pkiSync} />
</div>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={(e) => {
e.stopPropagation();
handleCopyId();
}}
>
Copy Sync ID
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.SyncCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faRotate} />}
onClick={(e) => {
e.stopPropagation();
onTriggerSyncCertificates(pkiSync);
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Manually trigger a sync for this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span> Trigger Sync</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{syncOption?.canImportCertificates && (
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.ImportCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />}
onClick={(e) => {
e.stopPropagation();
onTriggerImportCertificates(pkiSync);
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Import certificates from this ${destinationName} destination into Infisical.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.RemoveCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEraser} />}
onClick={(e) => {
e.stopPropagation();
onTriggerRemoveCertificates(pkiSync);
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Remove certificates synced by Infisical from this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Remove Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={isAutoSyncEnabled ? faToggleOff : faToggleOn} />}
onClick={(e) => {
e.stopPropagation();
onToggleEnable(pkiSync);
}}
>
{isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Delete}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={(e) => {
e.stopPropagation();
onDelete(pkiSync);
}}
>
Delete Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
};
@@ -0,0 +1,71 @@
import { ReactNode } from "react";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Td, Tooltip } from "@app/components/v2";
export type PkiSyncTableCellProps = {
primaryText: string;
secondaryText?: string;
infoBadge?: "primary" | "secondary";
additionalTooltipContent?: ReactNode;
primaryClassName?: string;
secondaryClassName?: string;
};
export const PkiSyncTableCell = ({
primaryText,
secondaryText,
infoBadge,
additionalTooltipContent,
primaryClassName,
secondaryClassName
}: PkiSyncTableCellProps) => {
return (
<Td className="!min-w-[8rem] max-w-0">
<Tooltip
side="left"
className="max-w-2xl break-words"
content={
<>
<p className="text-sm">{primaryText}</p>
{secondaryText && (
<p className={twMerge("text-xs leading-3 text-bunker-300", secondaryClassName)}>
{secondaryText}
</p>
)}
{additionalTooltipContent}
</>
}
>
<div>
<p className={twMerge("truncate text-sm", primaryClassName)}>
{primaryText}
{infoBadge === "primary" && (
<FontAwesomeIcon
size="xs"
icon={faInfoCircle}
className="ml-1 inline-block text-bunker-300"
/>
)}
</p>
{secondaryText && (
<p
className={twMerge("truncate text-xs leading-4 text-bunker-300", secondaryClassName)}
>
{secondaryText}
{infoBadge === "secondary" && (
<FontAwesomeIcon
size="xs"
icon={faInfoCircle}
className="ml-1 inline-block text-bunker-300"
/>
)}
</p>
)}
</div>
</Tooltip>
</Td>
);
};
@@ -0,0 +1,473 @@
import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faCheck,
faCheckCircle,
faFilter,
faMagnifyingGlass,
faRotate,
faSearch,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import {
DeletePkiSyncModal,
PkiSyncImportCertificatesModal,
PkiSyncRemoveCertificatesModal
} from "@app/components/pki-syncs";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import {
PkiSync,
PkiSyncData,
PkiSyncStatus,
useTriggerPkiSyncSyncCertificates,
useUpdatePkiSync
} from "@app/hooks/api/pkiSyncs";
import { getPkiSyncDestinationColValues } from "./helpers";
import { PkiSyncRow } from "./PkiSyncRow";
enum PkiSyncsOrderBy {
Destination = "destination",
Source = "source",
Name = "name",
Status = "status"
}
type PkiSyncFilters = {
destinations: PkiSync[];
status: PkiSyncStatus[];
};
const getSyncStatusOrderValue = (syncStatus: PkiSyncStatus | null) => {
switch (syncStatus) {
case PkiSyncStatus.Failed:
return 0;
case PkiSyncStatus.Pending:
case PkiSyncStatus.Running:
return 1;
case PkiSyncStatus.Succeeded:
return 2;
default:
return 3;
}
};
type Props = {
pkiSyncs: PkiSyncData[];
};
const STATUS_ICON_MAP = {
[PkiSyncStatus.Succeeded]: { icon: faCheck, className: "text-green", name: "Synced" },
[PkiSyncStatus.Failed]: { icon: faWarning, className: "text-red", name: "Not Synced" },
[PkiSyncStatus.Pending]: { icon: faRotate, className: "text-yellow", name: "Syncing" },
[PkiSyncStatus.Running]: { icon: faRotate, className: "text-yellow", name: "Syncing" }
};
export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deleteSync",
"importCertificates",
"removeCertificates"
] as const);
const triggerSync = useTriggerPkiSyncSyncCertificates();
const updateSync = useUpdatePkiSync();
const [filters, setFilters] = useState<PkiSyncFilters>({
destinations: [],
status: []
});
const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
toggleOrderDirection,
orderBy,
setOrderDirection,
setOrderBy
} = usePagination<PkiSyncsOrderBy>(PkiSyncsOrderBy.Name, {
initPerPage: getUserTablePreference("pkiSyncTable", PreferenceKey.PerPage, 20)
});
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("pkiSyncTable", PreferenceKey.PerPage, newPerPage);
};
const filteredPkiSyncs = useMemo(
() =>
pkiSyncs
.filter((pkiSync) => {
const { destination, name, syncStatus } = pkiSync;
if (filters.destinations.length && !filters.destinations.includes(destination))
return false;
if (filters.status.length && (!syncStatus || !filters.status.includes(syncStatus))) {
return false;
}
const searchValue = search.trim().toLowerCase();
const destinationValues = getPkiSyncDestinationColValues(pkiSync);
return (
PKI_SYNC_MAP[destination].name.toLowerCase().includes(searchValue) ||
name.toLowerCase().includes(searchValue) ||
(pkiSync.appConnectionName &&
pkiSync.appConnectionName.toLowerCase().includes(searchValue)) ||
destinationValues.primaryText.toLowerCase().includes(searchValue) ||
destinationValues.secondaryText?.toLowerCase().includes(searchValue)
);
})
.sort((a, b) => {
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()
.localeCompare(getPkiSyncDestinationColValues(syncTwo).primaryText.toLowerCase());
case PkiSyncsOrderBy.Status:
if (!syncOne.syncStatus && syncTwo.syncStatus) return 1;
if (syncOne.syncStatus && !syncTwo.syncStatus) return -1;
if (!syncOne.syncStatus && !syncTwo.syncStatus) return 0;
return (
getSyncStatusOrderValue(syncOne.syncStatus) -
getSyncStatusOrderValue(syncTwo.syncStatus)
);
case PkiSyncsOrderBy.Name:
default:
return syncOne.name.toLowerCase().localeCompare(syncTwo.name.toLowerCase());
}
}),
[pkiSyncs, orderDirection, search, orderBy, filters]
);
useResetPageHelper({
totalCount: filteredPkiSyncs.length,
offset,
setPage
});
const handleSort = (column: PkiSyncsOrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
const getClassName = (col: PkiSyncsOrderBy) =>
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
const getColSortIcon = (col: PkiSyncsOrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
const isTableFiltered = Boolean(filters.destinations.length);
const handleDelete = (pkiSync: PkiSyncData) => handlePopUpOpen("deleteSync", pkiSync);
const handleTriggerImportCertificates = (pkiSync: PkiSyncData) =>
handlePopUpOpen("importCertificates", pkiSync);
const handleTriggerRemoveCertificates = (pkiSync: PkiSyncData) =>
handlePopUpOpen("removeCertificates", pkiSync);
const handleToggleEnableSync = async (pkiSync: PkiSyncData) => {
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const isAutoSyncEnabled = !pkiSync.isAutoSyncEnabled;
try {
await updateSync.mutateAsync({
syncId: pkiSync.id,
projectId: pkiSync.projectId,
isAutoSyncEnabled
});
createNotification({
text: `Successfully ${isAutoSyncEnabled ? "enabled" : "disabled"} auto-sync for ${destinationName} Sync`,
type: "success"
});
} catch {
createNotification({
text: `Failed to ${isAutoSyncEnabled ? "enable" : "disable"} auto-sync for ${destinationName} Sync`,
type: "error"
});
}
};
const handleTriggerSync = async (pkiSync: PkiSyncData) => {
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
try {
await triggerSync.mutateAsync({
syncId: pkiSync.id,
projectId: pkiSync.projectId
});
createNotification({
text: `Successfully triggered ${destinationName} Sync`,
type: "success"
});
} catch {
createNotification({
text: `Failed to trigger ${destinationName} Sync`,
type: "error"
});
}
};
return (
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search PKI syncs..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter PKI syncs"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuLabel>Status</DropdownMenuLabel>
{[PkiSyncStatus.Running, PkiSyncStatus.Succeeded, PkiSyncStatus.Failed].map(
(status) => (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
status: prev.status.includes(status)
? prev.status.filter((s) => s !== status)
: [...prev.status, status]
}));
}}
key={status}
icon={
filters.status.includes(status) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<FontAwesomeIcon
icon={STATUS_ICON_MAP[status].icon}
className={STATUS_ICON_MAP[status].className}
/>
<span className="capitalize">{STATUS_ICON_MAP[status].name}</span>
</div>
</DropdownMenuItem>
)
)}
<DropdownMenuLabel>Service</DropdownMenuLabel>
{pkiSyncs.length ? (
[...new Set(pkiSyncs.map(({ destination }) => destination))].map((destination) => {
const { name, image } = PKI_SYNC_MAP[destination];
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
destinations: prev.destinations.includes(destination)
? prev.destinations.filter((a) => a !== destination)
: [...prev.destinations, destination]
}));
}}
key={destination}
icon={
filters.destinations.includes(destination) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<img
alt={`${name} integration`}
src={`/images/integrations/${image}`}
className="h-4 w-4"
/>
<span>{name}</span>
</div>
</DropdownMenuItem>
);
})
) : (
<DropdownMenuItem isDisabled>No PKI Syncs Configured</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-2" />
<Th className="w-1/4">
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={getClassName(PkiSyncsOrderBy.Name)}
ariaLabel="sort"
onClick={() => handleSort(PkiSyncsOrderBy.Name)}
>
<FontAwesomeIcon icon={getColSortIcon(PkiSyncsOrderBy.Name)} />
</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">
<div className="flex items-center">
Destination
<IconButton
variant="plain"
className={getClassName(PkiSyncsOrderBy.Destination)}
ariaLabel="sort"
onClick={() => handleSort(PkiSyncsOrderBy.Destination)}
>
<FontAwesomeIcon icon={getColSortIcon(PkiSyncsOrderBy.Destination)} />
</IconButton>
</div>
</Th>
<Th className="min-w-[10.5rem]">
<div className="flex items-center">
Status
<IconButton
variant="plain"
className={getClassName(PkiSyncsOrderBy.Status)}
ariaLabel="sort"
onClick={() => handleSort(PkiSyncsOrderBy.Status)}
>
<FontAwesomeIcon icon={getColSortIcon(PkiSyncsOrderBy.Status)} />
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{filteredPkiSyncs.slice(offset, perPage * page).map((pkiSync) => (
<PkiSyncRow
key={pkiSync.id}
pkiSync={pkiSync}
onDelete={handleDelete}
onTriggerSyncCertificates={handleTriggerSync}
onTriggerImportCertificates={handleTriggerImportCertificates}
onTriggerRemoveCertificates={handleTriggerRemoveCertificates}
onToggleEnable={handleToggleEnableSync}
/>
))}
</TBody>
</Table>
{Boolean(filteredPkiSyncs.length) && (
<Pagination
count={filteredPkiSyncs.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{!filteredPkiSyncs?.length && (
<EmptyState
title={
pkiSyncs.length ? "No syncs match search..." : "This project has no syncs configured"
}
icon={pkiSyncs.length ? faSearch : faRotate}
/>
)}
</TableContainer>
<DeletePkiSyncModal
onOpenChange={(isOpen) => handlePopUpToggle("deleteSync", isOpen)}
isOpen={popUp.deleteSync.isOpen}
pkiSync={popUp.deleteSync.data}
/>
<PkiSyncImportCertificatesModal
onOpenChange={(isOpen) => handlePopUpToggle("importCertificates", isOpen)}
isOpen={popUp.importCertificates.isOpen}
pkiSync={popUp.importCertificates.data}
/>
<PkiSyncRemoveCertificatesModal
onOpenChange={(isOpen) => handlePopUpToggle("removeCertificates", isOpen)}
isOpen={popUp.removeCertificates.isOpen}
pkiSync={popUp.removeCertificates.data}
/>
</div>
);
};
@@ -0,0 +1,13 @@
import { PkiSyncData } from "@app/hooks/api/pkiSyncs";
export const getPkiSyncDestinationColValues = (pkiSync: PkiSyncData) => {
const { destination } = pkiSync;
switch (destination) {
default:
return {
primaryText: pkiSync.name,
secondaryText: pkiSync.appConnectionName || "Unknown Connection"
};
}
};
@@ -0,0 +1 @@
export { PkiSyncsTable } from "./PkiSyncsTable";
@@ -0,0 +1,110 @@
import { useCallback, useEffect, useMemo } from "react";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { CreatePkiSyncModal } from "@app/components/pki-syncs";
import { Button, Spinner } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks";
import { useListPkiSyncs } from "@app/hooks/api/pkiSyncs";
import { PkiSyncsTable } from "./PkiSyncTable";
export const PkiSyncsTab = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const);
const { addSync, ...search } = useSearch({
from: ROUTE_PATHS.CertManager.IntegrationsListPage.id
});
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const memoizedSearch = useMemo(() => search, [search]);
const navigateToBase = useCallback(() => {
navigate({
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
params: {
projectId: currentWorkspace.id
},
search: memoizedSearch
});
}, [navigate, currentWorkspace.id, memoizedSearch]);
useEffect(() => {
if (!addSync) return;
handlePopUpOpen("addSync", addSync);
navigateToBase();
}, [addSync, handlePopUpOpen, navigateToBase]);
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
currentWorkspace.id
);
if (isPkiSyncsPending)
return (
<div className="flex h-[60vh] flex-col items-center justify-center gap-2">
<Spinner />
</div>
);
return (
<>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Certificate Syncs</p>
<a
href="https://infisical.com/docs/integrations/pki-syncs/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Use App Connections to sync certificates to third-party services.
</p>
</div>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Create}
a={ProjectPermissionSub.PkiSyncs}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addSync")}
isDisabled={!isAllowed}
>
Add Sync
</Button>
)}
</ProjectPermissionCan>
</div>
<PkiSyncsTable pkiSyncs={pkiSyncs} />
</div>
<CreatePkiSyncModal
selectSync={popUp.addSync.data}
isOpen={popUp.addSync.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addSync", isOpen)}
/>
</>
);
};
@@ -0,0 +1 @@
export { PkiSyncsTab } from "./PkiSyncsTab";
@@ -0,0 +1 @@
export { PkiSyncsTab } from "./PkiSyncsTab";
@@ -0,0 +1 @@
export { IntegrationsListPage } from "./IntegrationsListPage";
@@ -0,0 +1,30 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import { IntegrationsListPage } from "./IntegrationsListPage";
const IntegrationsListPageQuerySchema = z.object({
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(),
addSync: z.nativeEnum(PkiSync).optional()
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/"
)({
component: IntegrationsListPage,
validateSearch: zodValidator(IntegrationsListPageQuerySchema),
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Integrations"
}
]
};
}
});
@@ -0,0 +1,146 @@
import { Helmet } from "react-helmet";
import { faBan, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { EditPkiSyncModal } from "@app/components/pki-syncs";
import { PkiSyncEditFields } from "@app/components/pki-syncs/types";
import { Button, ContentLoader, EmptyState } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { usePopUp } from "@app/hooks";
import { useGetPkiSync } from "@app/hooks/api/pkiSyncs";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import {
PkiSyncActionTriggers,
PkiSyncAuditLogsSection,
PkiSyncDestinationSection,
PkiSyncDetailsSection,
PkiSyncOptionsSection,
PkiSyncSourceSection
} from "./components";
const PageContent = () => {
const navigate = useNavigate();
const { syncId, projectId } = useParams({
from: ROUTE_PATHS.CertManager.PkiSyncDetailsByIDPage.id
});
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editSync"] as const);
const { data: pkiSync, isPending } = useGetPkiSync(
{ syncId, projectId },
{
refetchInterval: 30000
}
);
if (isPending) {
return (
<div className="flex h-full w-full items-center justify-center">
<ContentLoader />
</div>
);
}
if (!pkiSync) {
return (
<div className="flex h-full w-full items-center justify-center px-20">
<EmptyState
className="max-w-2xl rounded-md text-center"
icon={faBan}
title={`Could not find PKI Sync with ID ${syncId}`}
/>
</div>
);
}
const destinationDetails = PKI_SYNC_MAP[pkiSync.destination];
const handleEditDetails = () => handlePopUpOpen("editSync", PkiSyncEditFields.Details);
const handleEditSource = () => handlePopUpOpen("editSync", PkiSyncEditFields.Source);
const handleEditOptions = () => handlePopUpOpen("editSync", PkiSyncEditFields.Options);
const handleEditDestination = () => handlePopUpOpen("editSync", PkiSyncEditFields.Destination);
return (
<>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
params: {
projectId
},
search: {
selectedTab: IntegrationsListPageTabs.PkiSyncs
}
});
}}
>
PKI Syncs
</Button>
<div className="mb-6 flex w-full items-center gap-3">
<img
alt={`${destinationDetails.name} sync`}
src={`/images/integrations/${destinationDetails.image}`}
className="ml-1 mt-3 w-16"
/>
<div>
<p className="text-3xl font-semibold text-white">{pkiSync.name}</p>
<p className="leading-3 text-bunker-300">{destinationDetails.name} PKI Sync</p>
</div>
<PkiSyncActionTriggers pkiSync={pkiSync} />
</div>
<div className="flex justify-center">
<div className="mr-4 flex w-72 flex-col gap-4">
<PkiSyncDetailsSection pkiSync={pkiSync} onEditDetails={handleEditDetails} />
<PkiSyncSourceSection pkiSync={pkiSync} onEditSource={handleEditSource} />
<PkiSyncOptionsSection pkiSync={pkiSync} onEditOptions={handleEditOptions} />
</div>
<div className="flex flex-1 flex-col gap-4">
<PkiSyncDestinationSection
pkiSync={pkiSync}
onEditDestination={handleEditDestination}
/>
<PkiSyncAuditLogsSection pkiSync={pkiSync} />
</div>
</div>
</div>
</div>
<EditPkiSyncModal
isOpen={popUp.editSync.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editSync", isOpen)}
fields={popUp.editSync.data}
pkiSync={pkiSync}
/>
</>
);
};
export const PkiSyncDetailsByIDPage = () => {
return (
<>
<Helmet>
<title>PKI Sync | Infisical</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<ProjectPermissionCan
renderGuardBanner
passThrough={false}
I={ProjectPermissionPkiSyncActions.Read}
a={ProjectPermissionSub.PkiSyncs}
>
<PageContent />
</ProjectPermissionCan>
</>
);
};
@@ -0,0 +1,318 @@
import { useCallback } from "react";
import { subject } from "@casl/ability";
import {
faBan,
faCheck,
faCopy,
faDownload,
faEllipsisV,
faEraser,
faInfoCircle,
faRotate,
faToggleOff,
faToggleOn,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DeletePkiSyncModal,
PkiSyncImportCertificatesModal,
PkiSyncImportStatusBadge,
PkiSyncRemoveCertificatesModal,
PkiSyncRemoveStatusBadge
} from "@app/components/pki-syncs";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { usePopUp, useToggle } from "@app/hooks";
import {
TPkiSync,
usePkiSyncOption,
useTriggerPkiSyncSyncCertificates,
useUpdatePkiSync
} from "@app/hooks/api/pkiSyncs";
import { IntegrationsListPageTabs } from "@app/types/integrations";
type Props = {
pkiSync: TPkiSync;
};
export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
const { destination, subscriberId, projectId, id } = pkiSync;
const navigate = useNavigate();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"importCertificates",
"removeCertificates",
"deleteSync"
] as const);
const [isIdCopied, setIsIdCopied] = useToggle(false);
// API mutations
const triggerSyncMutation = useTriggerPkiSyncSyncCertificates();
const updatePkiSyncMutation = useUpdatePkiSync();
const destinationName = PKI_SYNC_MAP[destination].name;
const { syncOption } = usePkiSyncOption(destination);
const handleCopyId = useCallback(() => {
setIsIdCopied.on();
navigator.clipboard.writeText(pkiSync.id);
createNotification({
text: "PKI Sync ID copied to clipboard",
type: "info"
});
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
return () => clearTimeout(timer);
}, [pkiSync.id, setIsIdCopied]);
const handleTriggerSync = useCallback(async () => {
try {
await triggerSyncMutation.mutateAsync({
syncId: id,
projectId
});
createNotification({
text: "PKI sync job queued successfully",
type: "success"
});
} catch (error) {
console.error("Failed to trigger sync:", error);
createNotification({
text: "Failed to trigger PKI sync",
type: "error"
});
}
}, [triggerSyncMutation, id, projectId]);
const handleToggleAutoSync = useCallback(async () => {
try {
await updatePkiSyncMutation.mutateAsync({
syncId: id,
projectId,
isAutoSyncEnabled: !pkiSync.isAutoSyncEnabled
});
createNotification({
text: `Auto-sync ${pkiSync.isAutoSyncEnabled ? "disabled" : "enabled"} successfully`,
type: "success"
});
} catch (error) {
console.error("Failed to toggle auto-sync:", error);
createNotification({
text: "Failed to toggle auto-sync",
type: "error"
});
}
}, [updatePkiSyncMutation, id, projectId, pkiSync.isAutoSyncEnabled]);
const permissionSubject = subscriberId
? subject(ProjectPermissionSub.PkiSyncs, { subscriberId })
: ProjectPermissionSub.PkiSyncs;
return (
<>
<div className="ml-auto mt-4 flex flex-wrap items-center justify-end gap-2">
<PkiSyncImportStatusBadge pkiSync={pkiSync} />
<PkiSyncRemoveStatusBadge pkiSync={pkiSync} />
{pkiSync.isAutoSyncEnabled ? (
<Badge
variant="success"
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faRotate} />
<span>Auto-Sync Enabled</span>
</Badge>
) : (
<Tooltip
className="text-xs"
content="Auto-Sync is disabled. Changes to the PKI subscriber will not be automatically synced to the destination."
>
<div>
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
<span>Auto-Sync Disabled</span>
</Badge>
</div>
</Tooltip>
)}
<div>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.SyncCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faRotate} />}
className="h-9 rounded-r-none bg-mineshaft-500"
isDisabled={!isAllowed || triggerSyncMutation.isPending}
isLoading={triggerSyncMutation.isPending}
onClick={handleTriggerSync}
>
Trigger Sync
</Button>
)}
</ProjectPermissionCan>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add-folder-or-import"
variant="outline_bg"
className="h-9 w-10 rounded-l-none border-l-2 border-mineshaft border-l-mineshaft-700 bg-mineshaft-500"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={(e) => {
e.stopPropagation();
handleCopyId();
}}
>
Copy Sync ID
</DropdownMenuItem>
{syncOption?.canImportCertificates && (
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.ImportCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />}
onClick={() => handlePopUpOpen("importCertificates")}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Import certificates from this ${destinationName} destination into Infisical.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
{syncOption?.canRemoveCertificates && (
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.RemoveCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEraser} />}
onClick={() => handlePopUpOpen("removeCertificates")}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Remove certificates synced by Infisical from this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Remove Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed || updatePkiSyncMutation.isPending}
icon={
<FontAwesomeIcon
icon={pkiSync.isAutoSyncEnabled ? faToggleOff : faToggleOn}
/>
}
onClick={handleToggleAutoSync}
>
{pkiSync.isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Delete}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => handlePopUpOpen("deleteSync")}
>
Delete Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<PkiSyncImportCertificatesModal
onOpenChange={(isOpen) => handlePopUpToggle("importCertificates", isOpen)}
isOpen={popUp.importCertificates.isOpen}
pkiSync={pkiSync}
/>
<PkiSyncRemoveCertificatesModal
onOpenChange={(isOpen) => handlePopUpToggle("removeCertificates", isOpen)}
isOpen={popUp.removeCertificates.isOpen}
pkiSync={pkiSync}
/>
<DeletePkiSyncModal
onOpenChange={(isOpen) => handlePopUpToggle("deleteSync", isOpen)}
isOpen={popUp.deleteSync.isOpen}
pkiSync={pkiSync}
onComplete={() =>
navigate({
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
params: {
projectId
},
search: {
selectedTab: IntegrationsListPageTabs.PkiSyncs
}
})
}
/>
</>
);
};
@@ -0,0 +1,85 @@
import { faFingerprint } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { useSubscription, useWorkspace } from "@app/context";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components/LogsSection";
const PKI_SYNC_EVENTS = [
EventType.PKI_SYNC_SYNC_CERTIFICATES,
EventType.PKI_SYNC_IMPORT_CERTIFICATES,
EventType.PKI_SYNC_REMOVE_CERTIFICATES
];
type Props = {
pkiSync: TPkiSync;
};
export const PkiSyncAuditLogsSection = ({ pkiSync }: Props) => {
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const auditLogsRetentionDays = subscription?.auditLogsRetentionDays ?? 30;
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-semibold 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
</p>
)}
</div>
{subscription.auditLogs ? (
<LogsSection
refetchInterval={4000}
showFilters={false}
project={currentWorkspace}
presets={{
eventMetadata: { syncId: pkiSync.id },
startDate: new Date(
new Date().setDate(new Date().getDate() - Math.min(auditLogsRetentionDays, 60))
),
eventType: PKI_SYNC_EVENTS
}}
/>
) : (
<div className="flex h-full items-center justify-center rounded-lg bg-mineshaft-800 text-sm text-mineshaft-200">
<div className="flex flex-col items-center gap-4 py-20">
<FontAwesomeIcon size="2x" icon={faFingerprint} />
<p>
Please{" "}
{subscription && subscription.slug !== null ? (
<Link to="/organization/billing" target="_blank" rel="noopener noreferrer">
<a
className="cursor-pointer underline transition-all hover:text-white"
target="_blank"
>
upgrade your subscription
</a>
</Link>
) : (
<a
href="https://infisical.com/scheduledemo"
target="_blank"
rel="noopener noreferrer"
>
<a
className="cursor-pointer underline transition-all hover:text-white"
target="_blank"
>
upgrade your subscription
</a>
</a>
)}{" "}
to view sync logs.
</p>
</div>
</div>
)}
</div>
);
};
@@ -0,0 +1,73 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { ReactNode } from "react";
import { subject } from "@casl/ability";
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
import { 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>
);
type Props = {
pkiSync: TPkiSync;
onEditDestination: VoidFunction;
};
export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props) => {
const { destination, subscriberId } = pkiSync;
const destinationDetails = PKI_SYNC_MAP[destination];
let DestinationComponents: ReactNode;
switch (destination) {
case PkiSync.AzureKeyVault:
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
break;
default:
// For future destinations, return null (no additional fields to show)
DestinationComponents = null;
}
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || ""
});
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-semibold text-mineshaft-100">Destination Configuration</h3>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit destination"
onClick={onEditDestination}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div className="flex w-full flex-wrap gap-8">
<GenericFieldLabel label={`${destinationDetails.name} Connection`}>
{pkiSync.appConnectionName || "Default Connection"}
</GenericFieldLabel>
{DestinationComponents}
</div>
</div>
);
};
@@ -0,0 +1,21 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
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>
);
type Props = {
pkiSync: TAzureKeyVaultPkiSync;
};
export const AzureKeyVaultPkiSyncDestinationSection = ({ pkiSync }: Props) => {
return (
<GenericFieldLabel label="Key Vault URL">
{pkiSync.destinationConfig.vaultBaseUrl}
</GenericFieldLabel>
);
};
@@ -0,0 +1 @@
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
@@ -0,0 +1,101 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { useMemo } from "react";
import { subject } from "@casl/ability";
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PkiSyncStatusBadge } from "@app/components/pki-syncs";
import { IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({
label,
children,
labelClassName
}: {
label: string;
children: React.ReactNode;
labelClassName?: string;
}) => (
<div>
<label className={`text-sm text-bunker-300 ${labelClassName || ""}`}>{label}</label>
<div className="mt-1">{children}</div>
</div>
);
type Props = {
pkiSync: TPkiSync;
onEditDetails: VoidFunction;
};
export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, subscriberId, subscriber } =
pkiSync;
const failureMessage = useMemo(() => {
if (syncStatus === PkiSyncStatus.Failed) {
if (lastSyncMessage)
try {
return JSON.stringify(JSON.parse(lastSyncMessage), null, 2);
} catch {
return lastSyncMessage;
}
return "An Unknown Error Occurred.";
}
return null;
}, [syncStatus, lastSyncMessage]);
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriber?.id || subscriberId || ""
});
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-semibold text-mineshaft-100">Details</h3>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync details"
onClick={onEditDetails}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</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"}
</GenericFieldLabel>
{syncStatus && (
<GenericFieldLabel label="Status">
<PkiSyncStatusBadge status={syncStatus} />
</GenericFieldLabel>
)}
{lastSyncedAt && (
<GenericFieldLabel label="Last Synced">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
</GenericFieldLabel>
)}
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
<p className="break-words rounded bg-mineshaft-600 p-2 text-xs">{failureMessage}</p>
</GenericFieldLabel>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1,62 @@
import { subject } from "@casl/ability";
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 { Badge, IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: TPkiSync;
onEditOptions: VoidFunction;
};
export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
const {
syncOptions: { canImportCertificates, canRemoveCertificates }
} = pkiSync;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: pkiSync.subscriberId || ""
});
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-semibold text-mineshaft-100">Sync Options</h3>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync options"
onClick={onEditOptions}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div>
<div className="space-y-3">
<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>
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection";
@@ -0,0 +1,75 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { subject } from "@casl/ability";
import { faEdit, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Badge, IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
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>
);
type Props = {
pkiSync: TPkiSync;
onEditSource: VoidFunction;
};
export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
const { subscriberId } = pkiSync;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || ""
});
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-semibold text-mineshaft-100">Source</h3>
<div>
{!subscriberId && (
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">
<div className="mr-1 inline-block w-min">
<Badge
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
variant="primary"
>
<FontAwesomeIcon icon={faTriangleExclamation} />
<span>Source Deleted</span>
</Badge>
</div>
</Tooltip>
)}
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync source"
onClick={onEditSource}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
</div>
<div>
<div className="space-y-3">
<GenericFieldLabel label="PKI Subscriber">
{subscriberId || "Deleted"}
</GenericFieldLabel>
</div>
</div>
</div>
</div>
);
};
@@ -0,0 +1,6 @@
export { PkiSyncActionTriggers } from "./PkiSyncActionTriggers";
export { PkiSyncAuditLogsSection } from "./PkiSyncAuditLogsSection";
export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection";
export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection";
export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection";
export { PkiSyncSourceSection } from "./PkiSyncSourceSection";
@@ -0,0 +1 @@
export { PkiSyncDetailsByIDPage } from "./PkiSyncDetailsByIDPage";
@@ -0,0 +1,31 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IntegrationsListPageTabs } from "@app/types/integrations";
import { PkiSyncDetailsByIDPage } from "./index";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId"
)({
component: PkiSyncDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Integrations",
link: linkOptions({
to: "/projects/cert-management/$projectId/integrations",
params,
search: {
selectedTab: IntegrationsListPageTabs.PkiSyncs
}
})
},
{
label: "PKI Sync"
}
]
};
}
});
@@ -41,12 +41,12 @@ export const ConditionsFields = ({
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${subject}.${position}.conditions`
name: `permissions.${subject}.${position}.conditions` as const
});
const conditionErrorMessage =
errors?.permissions?.[subject]?.[position]?.conditions?.message ||
errors?.permissions?.[subject]?.[position]?.conditions?.root?.message;
(errors?.permissions as any)?.[subject]?.[position]?.conditions?.message ||
(errors?.permissions as any)?.[subject]?.[position]?.conditions?.root?.message;
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
@@ -91,7 +91,7 @@ export const ConditionsFields = ({
{Boolean(items.fields.length) &&
items.fields.map((el, index) => {
const condition =
(watch(`permissions.${subject}.${position}.conditions.${index}`) as {
(watch(`permissions.${subject}.${position}.conditions.${index}` as const) as {
lhs: string;
rhs: string;
operator: string;
@@ -104,7 +104,7 @@ export const ConditionsFields = ({
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${subject}.${position}.conditions.${index}.lhs`}
name={`permissions.${subject}.${position}.conditions.${index}.lhs` as const}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
@@ -116,7 +116,7 @@ export const ConditionsFields = ({
{...field}
onValueChange={(e) => {
setValue(
`permissions.${subject}.${position}.conditions.${index}.operator`,
`permissions.${subject}.${position}.conditions.${index}.operator` as const,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
@@ -137,7 +137,9 @@ export const ConditionsFields = ({
<div className="flex w-44 items-center space-x-2">
<Controller
control={control}
name={`permissions.${subject}.${position}.conditions.${index}.operator`}
name={
`permissions.${subject}.${position}.conditions.${index}.operator` as const
}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
@@ -171,7 +173,7 @@ export const ConditionsFields = ({
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${subject}.${position}.conditions.${index}.rhs`}
name={`permissions.${subject}.${position}.conditions.${index}.rhs` as const}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
@@ -8,7 +8,8 @@ type Props = {
type:
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation;
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.PkiSyncs;
};
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {
@@ -20,6 +20,7 @@ import {
ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiSyncActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretEventActions,
@@ -100,6 +101,16 @@ const SecretSyncPolicyActionSchema = z.object({
[ProjectPermissionSecretSyncActions.RemoveSecrets]: z.boolean().optional()
});
const PkiSyncPolicyActionSchema = z.object({
[ProjectPermissionPkiSyncActions.Read]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.Create]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.Edit]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.Delete]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.SyncCertificates]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.ImportCertificates]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.RemoveCertificates]: z.boolean().optional()
});
const CommitPolicyActionSchema = z.object({
[ProjectPermissionCommitsActions.Read]: z.boolean().optional(),
[ProjectPermissionCommitsActions.PerformRollback]: z.boolean().optional()
@@ -311,6 +322,12 @@ export const projectRoleFormSchema = z.object({
})
.array()
.default([]),
[ProjectPermissionSub.PkiSyncs]: PkiSyncPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]),
@@ -393,6 +410,7 @@ type TConditionalFields =
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.Identity
| ProjectPermissionSub.SecretSyncs
| ProjectPermissionSub.PkiSyncs
| ProjectPermissionSub.SecretEvents;
export const isConditionalSubjects = (
@@ -408,6 +426,7 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.PkiSubscribers ||
subject === ProjectPermissionSub.CertificateTemplates ||
subject === ProjectPermissionSub.SecretSyncs ||
subject === ProjectPermissionSub.PkiSyncs ||
subject === ProjectPermissionSub.SecretEvents;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
@@ -515,6 +534,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshHostGroups,
ProjectPermissionSub.SecretSyncs,
ProjectPermissionSub.PkiSyncs,
ProjectPermissionSub.SecretEvents
].includes(subject)
) {
@@ -576,6 +596,38 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return;
}
if (subject === ProjectPermissionSub.PkiSyncs) {
const canRead = action.includes(ProjectPermissionPkiSyncActions.Read);
const canEdit = action.includes(ProjectPermissionPkiSyncActions.Edit);
const canDelete = action.includes(ProjectPermissionPkiSyncActions.Delete);
const canCreate = action.includes(ProjectPermissionPkiSyncActions.Create);
const canSyncCertificates = action.includes(
ProjectPermissionPkiSyncActions.SyncCertificates
);
const canImportCertificates = action.includes(
ProjectPermissionPkiSyncActions.ImportCertificates
);
const canRemoveCertificates = action.includes(
ProjectPermissionPkiSyncActions.RemoveCertificates
);
if (!formVal[subject]) formVal[subject] = [{ conditions: [], inverted: false }];
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
[ProjectPermissionPkiSyncActions.Read]: canRead,
[ProjectPermissionPkiSyncActions.Create]: canCreate,
[ProjectPermissionPkiSyncActions.Edit]: canEdit,
[ProjectPermissionPkiSyncActions.Delete]: canDelete,
[ProjectPermissionPkiSyncActions.SyncCertificates]: canSyncCertificates,
[ProjectPermissionPkiSyncActions.ImportCertificates]: canImportCertificates,
[ProjectPermissionPkiSyncActions.RemoveCertificates]: canRemoveCertificates,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return;
}
if (subject === ProjectPermissionSub.DynamicSecrets) {
const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential);
const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential);
@@ -1493,6 +1545,24 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
}
]
},
[ProjectPermissionSub.PkiSyncs]: {
title: "Certificate Syncs",
actions: [
{ label: "Read", value: ProjectPermissionPkiSyncActions.Read },
{ label: "Create", value: ProjectPermissionPkiSyncActions.Create },
{ label: "Modify", value: ProjectPermissionPkiSyncActions.Edit },
{ label: "Remove", value: ProjectPermissionPkiSyncActions.Delete },
{ label: "Trigger Syncs", value: ProjectPermissionPkiSyncActions.SyncCertificates },
{
label: "Import Certificates from Destination",
value: ProjectPermissionPkiSyncActions.ImportCertificates
},
{
label: "Remove Certificates from Destination",
value: ProjectPermissionPkiSyncActions.RemoveCertificates
}
]
},
[ProjectPermissionSub.Kmip]: {
title: "KMIP",
actions: [
@@ -1618,6 +1688,7 @@ const SecretsManagerPermissionSubjects = (enabled = false) => ({
[ProjectPermissionSub.SecretApproval]: enabled,
[ProjectPermissionSub.Integrations]: enabled,
[ProjectPermissionSub.SecretSyncs]: enabled,
[ProjectPermissionSub.PkiSyncs]: enabled,
[ProjectPermissionSub.Kms]: enabled,
[ProjectPermissionSub.Environments]: enabled,
[ProjectPermissionSub.Tags]: enabled,
@@ -2008,6 +2079,10 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
subject: ProjectPermissionSub.SecretSyncs,
actions: [ProjectPermissionSecretSyncActions.Read]
},
{
subject: ProjectPermissionSub.PkiSyncs,
actions: [ProjectPermissionPkiSyncActions.Read]
},
{
subject: ProjectPermissionSub.Commits,
actions: [ProjectPermissionCommitsActions.Read]
@@ -2069,6 +2144,10 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
subject: ProjectPermissionSub.SecretSyncs,
actions: Object.values(ProjectPermissionSecretSyncActions)
},
{
subject: ProjectPermissionSub.PkiSyncs,
actions: Object.values(ProjectPermissionPkiSyncActions)
},
{
subject: ProjectPermissionSub.Commits,
actions: Object.values(ProjectPermissionCommitsActions)
@@ -28,7 +28,9 @@ export const IntegrationsListPage = () => {
const updateSelectedTab = (tab: string) => {
navigate({
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
search: (prev) => ({ ...prev, selectedTab: tab as IntegrationsListPageTabs }),
search: {
selectedTab: tab as IntegrationsListPageTabs
},
params: {
projectId: currentWorkspace.id
}
+106
View File
@@ -135,10 +135,12 @@ import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/se
import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route'
import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route'
import { Route as certManagerPkiSubscriberDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSubscriberDetailsByIDPage/route'
import { Route as certManagerPkiSyncDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSyncDetailsByIDPage/route'
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
import { Route as secretScanningSecretScanningDataSourcesPageRouteImport } from './pages/secret-scanning/SecretScanningDataSourcesPage/route'
import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route'
import { Route as certManagerPkiSubscribersPageRouteImport } from './pages/cert-manager/PkiSubscribersPage/route'
import { Route as certManagerIntegrationsListPageRouteImport } from './pages/cert-manager/IntegrationsListPage/route'
import { Route as certManagerPkiTemplateListPageRouteImport } from './pages/cert-manager/PkiTemplateListPage/route'
import { Route as secretScanningSecretScanningDataSourceByIdPageRouteImport } from './pages/secret-scanning/SecretScanningDataSourceByIdPage/route'
import { Route as secretManagerIntegrationsWindmillConfigurePageRouteImport } from './pages/secret-manager/integrations/WindmillConfigurePage/route'
@@ -292,6 +294,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertMa
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers',
)()
const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations',
)()
const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates',
@@ -973,6 +979,15 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertMa
} as any,
)
const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRoute =
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsImport.update(
{
id: '/integrations',
path: '/integrations',
getParentRoute: () => certManagerLayoutRoute,
} as any,
)
const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute =
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesImport.update(
{
@@ -1314,6 +1329,14 @@ const certManagerPkiSubscriberDetailsByIDPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute,
} as any)
const certManagerPkiSyncDetailsByIDPageRouteRoute =
certManagerPkiSyncDetailsByIDPageRouteImport.update({
id: '/$syncId',
path: '/$syncId',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRoute,
} as any)
const certManagerCertAuthDetailsByIDPageRouteRoute =
certManagerCertAuthDetailsByIDPageRouteImport.update({
id: '/ca/$caName',
@@ -1345,6 +1368,14 @@ const certManagerPkiSubscribersPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute,
} as any)
const certManagerIntegrationsListPageRouteRoute =
certManagerIntegrationsListPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRoute,
} as any)
const certManagerPkiTemplateListPageRouteRoute =
certManagerPkiTemplateListPageRouteImport.update({
id: '/',
@@ -2744,6 +2775,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesImport
parentRoute: typeof certManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations'
path: '/integrations'
fullPath: '/projects/cert-management/$projectId/integrations'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsImport
parentRoute: typeof certManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers'
path: '/subscribers'
@@ -2828,6 +2866,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof certManagerPkiTemplateListPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/'
path: '/'
fullPath: '/projects/cert-management/$projectId/integrations/'
preLoaderRoute: typeof certManagerIntegrationsListPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/'
path: '/'
@@ -2856,6 +2901,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport
parentRoute: typeof certManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId'
path: '/$syncId'
fullPath: '/projects/cert-management/$projectId/integrations/$syncId'
preLoaderRoute: typeof certManagerPkiSyncDetailsByIDPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName'
path: '/$subscriberName'
@@ -3798,6 +3850,24 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertMa
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRouteChildren {
certManagerIntegrationsListPageRouteRoute: typeof certManagerIntegrationsListPageRouteRoute
certManagerPkiSyncDetailsByIDPageRouteRoute: typeof certManagerPkiSyncDetailsByIDPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRouteChildren =
{
certManagerIntegrationsListPageRouteRoute:
certManagerIntegrationsListPageRouteRoute,
certManagerPkiSyncDetailsByIDPageRouteRoute:
certManagerPkiSyncDetailsByIDPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteChildren {
certManagerPkiSubscribersPageRouteRoute: typeof certManagerPkiSubscribersPageRouteRoute
certManagerPkiSubscriberDetailsByIDPageRouteRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
@@ -3824,6 +3894,7 @@ interface certManagerLayoutRouteChildren {
projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute
projectAuditLogsPageRouteCertManagerRoute: typeof projectAuditLogsPageRouteCertManagerRoute
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute
projectGroupDetailsByIDPageRouteCertManagerRoute: typeof projectGroupDetailsByIDPageRouteCertManagerRoute
@@ -3845,6 +3916,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = {
projectAuditLogsPageRouteCertManagerRoute,
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute:
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRoute:
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute:
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren,
certManagerCertAuthDetailsByIDPageRouteRoute:
@@ -4691,6 +4764,7 @@ export interface FileRoutesByFullPath {
'/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
'/projects/cert-management/$projectId/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
'/projects/cert-management/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRouteWithChildren
'/projects/cert-management/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
@@ -4703,10 +4777,12 @@ export interface FileRoutesByFullPath {
'/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
'/projects/ssh/$projectId/audit-logs': typeof projectAuditLogsPageRouteSshRoute
'/projects/cert-management/$projectId/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute
'/projects/cert-management/$projectId/integrations/': typeof certManagerIntegrationsListPageRouteRoute
'/projects/cert-management/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
'/projects/secret-management/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
'/projects/secret-scanning/$projectId/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
'/projects/cert-management/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
'/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
'/projects/cert-management/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
'/projects/secret-management/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
'/projects/secret-management/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
@@ -4912,10 +4988,12 @@ export interface FileRoutesByTo {
'/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
'/projects/ssh/$projectId/audit-logs': typeof projectAuditLogsPageRouteSshRoute
'/projects/cert-management/$projectId/certificate-templates': typeof certManagerPkiTemplateListPageRouteRoute
'/projects/cert-management/$projectId/integrations': typeof certManagerIntegrationsListPageRouteRoute
'/projects/cert-management/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute
'/projects/secret-management/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
'/projects/secret-scanning/$projectId/data-sources': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
'/projects/cert-management/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
'/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
'/projects/cert-management/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
'/projects/secret-management/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
'/projects/secret-management/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
@@ -5130,6 +5208,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutIntegrationsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
@@ -5142,10 +5221,12 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs': typeof projectAuditLogsPageRouteSshRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/': typeof certManagerIntegrationsListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
@@ -5353,6 +5434,7 @@ export interface FileRouteTypes {
| '/projects/cert-management/$projectId/access-management'
| '/projects/cert-management/$projectId/audit-logs'
| '/projects/cert-management/$projectId/certificate-templates'
| '/projects/cert-management/$projectId/integrations'
| '/projects/cert-management/$projectId/subscribers'
| '/projects/kms/$projectId/access-management'
| '/projects/kms/$projectId/audit-logs'
@@ -5365,10 +5447,12 @@ export interface FileRouteTypes {
| '/projects/ssh/$projectId/access-management'
| '/projects/ssh/$projectId/audit-logs'
| '/projects/cert-management/$projectId/certificate-templates/'
| '/projects/cert-management/$projectId/integrations/'
| '/projects/cert-management/$projectId/subscribers/'
| '/projects/secret-management/$projectId/integrations/'
| '/projects/secret-scanning/$projectId/data-sources/'
| '/projects/cert-management/$projectId/ca/$caName'
| '/projects/cert-management/$projectId/integrations/$syncId'
| '/projects/cert-management/$projectId/subscribers/$subscriberName'
| '/projects/secret-management/$projectId/integrations/$integrationId'
| '/projects/secret-management/$projectId/integrations/select-integration-auth'
@@ -5573,10 +5657,12 @@ export interface FileRouteTypes {
| '/projects/ssh/$projectId/access-management'
| '/projects/ssh/$projectId/audit-logs'
| '/projects/cert-management/$projectId/certificate-templates'
| '/projects/cert-management/$projectId/integrations'
| '/projects/cert-management/$projectId/subscribers'
| '/projects/secret-management/$projectId/integrations'
| '/projects/secret-scanning/$projectId/data-sources'
| '/projects/cert-management/$projectId/ca/$caName'
| '/projects/cert-management/$projectId/integrations/$syncId'
| '/projects/cert-management/$projectId/subscribers/$subscriberName'
| '/projects/secret-management/$projectId/integrations/$integrationId'
| '/projects/secret-management/$projectId/integrations/select-integration-auth'
@@ -5789,6 +5875,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs'
@@ -5801,10 +5888,12 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth'
@@ -6390,6 +6479,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/groups/$groupId",
@@ -6558,6 +6648,14 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/"
]
},
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout",
"children": [
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId"
]
},
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout",
@@ -6694,6 +6792,10 @@ export const routeTree = rootRoute
"filePath": "cert-manager/PkiTemplateListPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates"
},
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/": {
"filePath": "cert-manager/IntegrationsListPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations"
},
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/": {
"filePath": "cert-manager/PkiSubscribersPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
@@ -6710,6 +6812,10 @@ export const routeTree = rootRoute
"filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId": {
"filePath": "cert-manager/PkiSyncDetailsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations"
},
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName": {
"filePath": "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
+4
View File
@@ -310,6 +310,10 @@ const certManagerRoutes = route("/projects/cert-management/$projectId", [
route("/alerting", "cert-manager/AlertingPage/route.tsx"),
route("/ca/$caName", "cert-manager/CertAuthDetailsByIDPage/route.tsx"),
route("/pki-collections/$collectionId", "cert-manager/PkiCollectionDetailsByIDPage/routes.tsx"),
route("/integrations", [
index("cert-manager/IntegrationsListPage/route.tsx"),
route("/$syncId", "cert-manager/PkiSyncDetailsByIDPage/route.tsx")
]),
route("/settings", "cert-manager/SettingsPage/route.tsx"),
route("/audit-logs", "project/AuditLogsPage/route-cert-manager.tsx"),
route("/access-management", "project/AccessControlPage/route-cert-manager.tsx"),
+2 -1
View File
@@ -2,5 +2,6 @@ export enum IntegrationsListPageTabs {
NativeIntegrations = "native-integrations",
FrameworkIntegrations = "framework-integrations",
InfrastructureIntegrations = "infrastructure-integrations",
SecretSyncs = "secret-syncs"
SecretSyncs = "secret-syncs",
PkiSyncs = "pki-syncs"
}