mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 16:28:12 +00:00
Add Certificate Syncs
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+14
@@ -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} />;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
|
||||
+398
@@ -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>
|
||||
);
|
||||
};
|
||||
+71
@@ -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>
|
||||
);
|
||||
};
|
||||
+473
@@ -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>
|
||||
);
|
||||
};
|
||||
+13
@@ -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"
|
||||
};
|
||||
}
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { PkiSyncsTable } from "./PkiSyncsTable";
|
||||
+110
@@ -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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user