mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 19:02:03 +00:00
Merge branch 'main' into feature/slack-secret-sync-error-notification
This commit is contained in:
@@ -7,16 +7,15 @@ import {
|
||||
faTrash,
|
||||
faUsers,
|
||||
faUserXmark,
|
||||
faWarning,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useSubscription, useUser } from "@app/context";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
@@ -432,7 +432,7 @@ export const ServerAdminsTable = () => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`}
|
||||
text="Your current plan does not allow removing server admins. To unlock this feature, please upgrade to Infisical Pro plan."
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeUsers.isOpen}
|
||||
@@ -451,7 +451,7 @@ export const ServerAdminsTable = () => {
|
||||
const email = user.email ?? user.username;
|
||||
return (
|
||||
<li key={user.id}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<p>
|
||||
{user.firstName || user.lastName ? (
|
||||
<>
|
||||
@@ -464,15 +464,10 @@ export const ServerAdminsTable = () => {
|
||||
</p>
|
||||
{userId === user.id && (
|
||||
<Tooltip content="Are you sure you want to remove yourself from this instance?">
|
||||
<div className="inline-block">
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} />
|
||||
<span>Deleting Yourself</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="danger">
|
||||
<AlertTriangleIcon />
|
||||
Deleting Yourself
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { RefreshCwIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useUser } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useInvalidateCache } from "@app/hooks/api";
|
||||
@@ -62,11 +62,8 @@ export const CachingPageForm = () => {
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
<span className="text-xl font-medium text-mineshaft-100">Secrets Cache</span>
|
||||
{isInvalidating && (
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
|
||||
<Badge variant="danger">
|
||||
<RefreshCwIcon className="animate-spin" />
|
||||
Invalidating Cache
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useCallback } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Badge, Button, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import { Button, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useServerConfig, useSubscription } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
@@ -54,7 +54,8 @@ export const EncryptionPageForm = () => {
|
||||
|
||||
if (!subscription.hsm) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "Hardware Security Module's (HSM's), are only available on Enterprise plans."
|
||||
isEnterpriseFeature: true,
|
||||
text: "Your current plan does not include access to Hardware Security Module (HSM). To unlock this feature, please upgrade to Infisical Enterprise plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -132,12 +133,10 @@ export const EncryptionPageForm = () => {
|
||||
|
||||
{config.fipsEnabled && (
|
||||
<Tooltip content="FIPS mode of operation is enabled for your instance. All cryptographic operations within the FIPS boundaries are validated to be FIPS compliant.">
|
||||
<div>
|
||||
<Badge className="flex items-center gap-2" variant="primary">
|
||||
FIPS Mode: Enabled
|
||||
<FontAwesomeIcon icon={faInfoCircle} />
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="info">
|
||||
FIPS Mode: Enabled
|
||||
<InfoIcon />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
@@ -145,7 +144,8 @@ export const EncryptionPageForm = () => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
text={popUp.upgradePlan?.data?.text}
|
||||
isEnterpriseFeature={popUp.upgradePlan?.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Control, Controller, useForm, useWatch } from "react-hook-form";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faChevronRight,
|
||||
faExclamationTriangle,
|
||||
faMagnifyingGlass
|
||||
@@ -14,6 +12,7 @@ import { z } from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api";
|
||||
|
||||
type TForm = Record<string, string>;
|
||||
@@ -213,22 +212,9 @@ export const EnvironmentPageForm = () => {
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-start gap-1">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Overrides</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.32rem] ml-1 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>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides" />
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Override specific environment variables. After saving, it may take up to 5 minutes for
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ServerCogIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
@@ -110,7 +111,8 @@ const IdentityPanelTable = ({
|
||||
<Td>
|
||||
{name}
|
||||
{isInstanceAdmin && (
|
||||
<Badge variant="primary" className="ml-2">
|
||||
<Badge variant="info" className="ml-2">
|
||||
<ServerCogIcon />
|
||||
Server Admin
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faBuilding,
|
||||
faCircleQuestion,
|
||||
faEllipsisV,
|
||||
faEnvelope,
|
||||
faEye,
|
||||
@@ -19,11 +18,11 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { CircleQuestionMarkIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
@@ -46,6 +45,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useUser } from "@app/context";
|
||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||
import {
|
||||
@@ -295,15 +295,17 @@ const ViewMembersModalContent = ({
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<Badge className="flex w-fit max-w-full items-center gap-x-1 bg-mineshaft-400/50 whitespace-nowrap text-bunker-200">
|
||||
<p className="truncate capitalize">{member.role.replace("-", " ")}</p>
|
||||
{Boolean(member.roleId) && (
|
||||
<Tooltip content="This member has a custom role assigned.">
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="w-3" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Badge>
|
||||
<Td>
|
||||
<div className="flex max-w-32">
|
||||
<Tooltip
|
||||
content={member.roleId ? "This member has a custom role assigned." : ""}
|
||||
>
|
||||
<Badge isTruncatable variant="neutral">
|
||||
<span className="capitalize">{member.role.replace("-", " ")}</span>
|
||||
{Boolean(member.roleId) && <CircleQuestionMarkIcon />}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-end">
|
||||
|
||||
@@ -9,16 +9,15 @@ import {
|
||||
faUsers,
|
||||
faUserShield,
|
||||
faUserXmark,
|
||||
faWarning,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AlertTriangleIcon, UserCogIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
@@ -41,6 +40,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useSubscription, useUser } from "@app/context";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
@@ -58,9 +58,6 @@ import {
|
||||
import { User } from "@app/hooks/api/users/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions";
|
||||
const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user";
|
||||
|
||||
const UserPanelTable = ({
|
||||
handlePopUpOpen,
|
||||
users,
|
||||
@@ -84,7 +81,7 @@ const UserPanelTable = ({
|
||||
data?: {
|
||||
username: string;
|
||||
id: string;
|
||||
message?: string;
|
||||
text?: string;
|
||||
}
|
||||
) => void;
|
||||
isPending: boolean;
|
||||
@@ -218,7 +215,8 @@ const UserPanelTable = ({
|
||||
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
||||
</p>
|
||||
{superAdmin && (
|
||||
<Badge variant="primary" className="ml-2 whitespace-nowrap">
|
||||
<Badge variant="info" className="ml-2">
|
||||
<UserCogIcon />
|
||||
Server Admin
|
||||
</Badge>
|
||||
)}
|
||||
@@ -259,7 +257,7 @@ const UserPanelTable = ({
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
username,
|
||||
id,
|
||||
message: addServerAdminUpgradePlanMessage
|
||||
text: "Your current plan does not allow setting additional server admins. To unlock this feature, please upgrade to Infisical Pro plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -287,7 +285,7 @@ const UserPanelTable = ({
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
username,
|
||||
id,
|
||||
message: removeServerAdminUpgradePlanMessage
|
||||
text: "Your current plan does not allow removing server admins. To unlock this feature, please upgrade to Infisical Pro plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -529,7 +527,7 @@ export const UserIdentitiesTable = () => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`}
|
||||
text={popUp.upgradePlan.data?.text}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeUsers.isOpen}
|
||||
@@ -548,7 +546,7 @@ export const UserIdentitiesTable = () => {
|
||||
const email = user.email ?? user.username;
|
||||
return (
|
||||
<li key={user.id}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<p>
|
||||
{user.firstName || user.lastName ? (
|
||||
<>
|
||||
@@ -561,15 +559,10 @@ export const UserIdentitiesTable = () => {
|
||||
</p>
|
||||
{userId === user.id && (
|
||||
<Tooltip content="Are you sure you want to remove yourself from this instance?">
|
||||
<div className="inline-block">
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} />
|
||||
<span>Deleting Yourself</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="danger">
|
||||
<AlertTriangleIcon />
|
||||
Deleting Yourself
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -7,7 +7,6 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useGetCaCerts } from "@app/hooks/api";
|
||||
|
||||
@@ -60,7 +60,7 @@ export const CaCertificatesTable = ({ caId }: Props) => {
|
||||
<div className="flex items-center">
|
||||
CA Certificate {caCert.version}
|
||||
{isLastItem && (
|
||||
<Badge variant="success" className="ml-4">
|
||||
<Badge variant="info" className="ml-4">
|
||||
Current
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
@@ -24,8 +23,7 @@ export const CaSection = () => {
|
||||
"caCert",
|
||||
"installCaCert",
|
||||
"deleteCa",
|
||||
"caStatus", // enable / disable
|
||||
"upgradePlan"
|
||||
"caStatus" // enable / disable
|
||||
] as const);
|
||||
|
||||
const onRemoveCaSubmit = async (caName: string) => {
|
||||
@@ -124,11 +122,6 @@ export const CaSection = () => {
|
||||
onUpdateCaStatus(popUp?.caStatus?.data as { caName: string; status: CaStatus })
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
|
||||
import {
|
||||
@@ -34,15 +34,12 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["installCaCert", "caCert", "ca", "deleteCa", "caStatus", "upgradePlan"]
|
||||
>,
|
||||
popUpName: keyof UsePopUpState<["installCaCert", "caCert", "ca", "deleteCa", "caStatus"]>,
|
||||
data?: {
|
||||
caId?: string;
|
||||
caName?: string;
|
||||
dn?: string;
|
||||
status?: CaStatus;
|
||||
description?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ type Props = {
|
||||
|
||||
const caTypes = [
|
||||
{ label: "ACME", value: CaType.ACME },
|
||||
{ label: "Azure AD Certificate Service", value: CaType.AZURE_AD_CS }
|
||||
{ label: "Active Directory Certificate Services (AD CS)", value: CaType.AZURE_AD_CS }
|
||||
];
|
||||
|
||||
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
|
||||
+1
-8
@@ -1,7 +1,6 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
@@ -20,8 +19,7 @@ export const ExternalCaSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"ca",
|
||||
"deleteCa",
|
||||
"caStatus", // enable / disable
|
||||
"upgradePlan"
|
||||
"caStatus" // enable / disable
|
||||
] as const);
|
||||
|
||||
const onRemoveCaSubmit = async (caName: string, type: CaType) => {
|
||||
@@ -131,11 +129,6 @@ export const ExternalCaSection = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-3
@@ -10,7 +10,6 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api";
|
||||
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
|
||||
@@ -33,12 +33,11 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus", "upgradePlan"]>,
|
||||
popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus"]>,
|
||||
data?: {
|
||||
name?: string;
|
||||
type?: CaType;
|
||||
status?: CaStatus;
|
||||
description?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { PkiCollectionSection } from "../AlertingPage/components";
|
||||
import { CertificatesSection } from "./components";
|
||||
|
||||
export const CertificatesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const canAccessPkiColl = permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.PkiCollections
|
||||
);
|
||||
const canAccessCerts = permission.can(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificates"
|
||||
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
|
||||
/>
|
||||
{/* If both are false, the section does not render. This is to prevent duplicate banners. */}
|
||||
{(canAccessCerts || canAccessPkiColl) && (
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.PkiCollections}
|
||||
>
|
||||
<PkiCollectionSection />
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionCertificateActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
<CertificatesSection />
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+3
-2
@@ -122,11 +122,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
|
||||
const { data: profilesData } = useListCertificateProfiles({
|
||||
projectId: currentProject?.id || "",
|
||||
includeMetrics: false,
|
||||
enrollmentType: "api"
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
||||
const { mutateAsync: createCertificate } = useCreateCertificateV3({
|
||||
projectId: currentProject?.id
|
||||
});
|
||||
|
||||
const formResolver = useMemo(() => {
|
||||
return zodResolver(createSchema(shouldShowSubjectSection));
|
||||
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { faPlus, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
EmptyState,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
PkiSync,
|
||||
useAddCertificatesToPkiSync,
|
||||
useListPkiSyncsWithCertificate,
|
||||
useRemoveCertificatesFromPkiSync
|
||||
} from "@app/hooks/api/pkiSyncs";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
|
||||
type Props = {
|
||||
popUp: {
|
||||
isOpen: boolean;
|
||||
data?: {
|
||||
certificateId?: string;
|
||||
commonName?: string;
|
||||
};
|
||||
};
|
||||
handlePopUpToggle: (popUpName: "managePkiSyncs", state?: boolean) => void;
|
||||
};
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [selectedSyncIds, setSelectedSyncIds] = useState<Set<string>>(new Set());
|
||||
const [initialSyncIds, setInitialSyncIds] = useState<Set<string>>(new Set());
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const { currentProject } = useProject();
|
||||
const navigate = useNavigate();
|
||||
const { certificateId, commonName } = popUp.data || {};
|
||||
|
||||
const { data: pkiSyncs = [], isPending } = useListPkiSyncsWithCertificate(
|
||||
currentProject?.id || "",
|
||||
certificateId || "",
|
||||
{
|
||||
enabled: !!currentProject?.id && !!certificateId
|
||||
}
|
||||
);
|
||||
const addCertificatesToSync = useAddCertificatesToPkiSync();
|
||||
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
|
||||
|
||||
const filteredSyncs = useMemo(() => {
|
||||
if (!searchTerm.trim()) return pkiSyncs;
|
||||
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return pkiSyncs.filter((sync) => sync.name.toLowerCase().includes(searchLower));
|
||||
}, [pkiSyncs, searchTerm]);
|
||||
|
||||
const startIndex = (currentPage - 1) * PER_PAGE;
|
||||
const endIndex = startIndex + PER_PAGE;
|
||||
const paginatedSyncs = filteredSyncs.slice(startIndex, endIndex);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchTerm]);
|
||||
|
||||
const handleClose = () => {
|
||||
handlePopUpToggle("managePkiSyncs", false);
|
||||
setSelectedSyncIds(new Set());
|
||||
setInitialSyncIds(new Set());
|
||||
setSearchTerm("");
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleNavigateToPkiSyncs = () => {
|
||||
if (!currentProject?.id) return;
|
||||
|
||||
navigate({
|
||||
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
|
||||
params: {
|
||||
projectId: currentProject.id
|
||||
},
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.PkiSyncs
|
||||
}
|
||||
});
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const getDestinationDisplayName = (destination: string) => {
|
||||
switch (destination) {
|
||||
case PkiSync.AzureKeyVault:
|
||||
return "Azure Key Vault";
|
||||
case PkiSync.AwsCertificateManager:
|
||||
return "AWS Certificate Manager";
|
||||
default:
|
||||
return destination;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!certificateId || !pkiSyncs || pkiSyncs.length === 0) return;
|
||||
|
||||
const currentSyncIds = new Set(
|
||||
pkiSyncs.filter((sync) => sync.hasCertificate).map((sync) => sync.id)
|
||||
);
|
||||
setSelectedSyncIds(currentSyncIds);
|
||||
setInitialSyncIds(new Set(currentSyncIds));
|
||||
}, [certificateId, pkiSyncs]);
|
||||
|
||||
const handleSyncToggle = (syncId: string) => {
|
||||
setSelectedSyncIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(syncId)) {
|
||||
newSet.delete(syncId);
|
||||
} else {
|
||||
newSet.add(syncId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveChanges = async () => {
|
||||
if (!certificateId) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const syncsToAdd = Array.from(selectedSyncIds).filter((id) => !initialSyncIds.has(id));
|
||||
const syncsToRemove = Array.from(initialSyncIds).filter((id) => !selectedSyncIds.has(id));
|
||||
|
||||
await Promise.all(
|
||||
syncsToAdd.map((syncId) =>
|
||||
addCertificatesToSync.mutateAsync({
|
||||
pkiSyncId: syncId,
|
||||
certificateIds: [certificateId]
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
syncsToRemove.map((syncId) =>
|
||||
removeCertificatesFromSync.mutateAsync({
|
||||
pkiSyncId: syncId,
|
||||
certificateIds: [certificateId]
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
createNotification({
|
||||
text: `PKI sync settings updated for certificate "${commonName}"`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to update PKI sync settings",
|
||||
type: "error"
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={popUp.isOpen} onOpenChange={handleClose}>
|
||||
<ModalContent
|
||||
title="Manage PKI Syncs"
|
||||
subTitle={`Select which PKI syncs "${commonName}" should be part of`}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search PKI syncs by name..."
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 max-h-96 overflow-y-auto">
|
||||
{isPending && (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<div className="text-bunker-300">Loading PKI syncs...</div>
|
||||
</div>
|
||||
)}
|
||||
{!isPending && pkiSyncs.length === 0 && (
|
||||
<EmptyState title="No PKI syncs available" icon={faPlus}>
|
||||
<div className="mt-1">
|
||||
Create a{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNavigateToPkiSyncs}
|
||||
className="cursor-pointer underline hover:text-mineshaft-300"
|
||||
>
|
||||
PKI sync
|
||||
</button>{" "}
|
||||
first to manage certificate syncing.
|
||||
</div>
|
||||
</EmptyState>
|
||||
)}
|
||||
{!isPending && pkiSyncs.length > 0 && filteredSyncs.length === 0 && searchTerm && (
|
||||
<EmptyState title="No PKI syncs found" icon={faSearch}>
|
||||
<div className="mt-1">
|
||||
No PKI syncs match your search criteria. Try a different search term.
|
||||
</div>
|
||||
</EmptyState>
|
||||
)}
|
||||
{!isPending && filteredSyncs.length > 0 && (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-12" />
|
||||
<Th className="w-1/2">Name</Th>
|
||||
<Th className="w-1/2">Destination</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{paginatedSyncs.map((sync) => (
|
||||
<Tr
|
||||
key={sync.id}
|
||||
className="cursor-pointer hover:bg-mineshaft-700"
|
||||
onClick={() => handleSyncToggle(sync.id)}
|
||||
>
|
||||
<Td>
|
||||
<Checkbox
|
||||
isChecked={selectedSyncIds.has(sync.id)}
|
||||
onCheckedChange={() => handleSyncToggle(sync.id)}
|
||||
id={`sync-${sync.id}`}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="w-1/2 max-w-0">
|
||||
<div className="truncate" title={sync.name}>
|
||||
{sync.name}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="w-1/2 max-w-0">
|
||||
<div
|
||||
className="truncate capitalize"
|
||||
title={getDestinationDisplayName(sync.destination)}
|
||||
>
|
||||
{getDestinationDisplayName(sync.destination)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{!isPending && filteredSyncs.length > PER_PAGE && (
|
||||
<div className="mt-4">
|
||||
<Pagination
|
||||
count={filteredSyncs.length}
|
||||
page={currentPage}
|
||||
perPage={PER_PAGE}
|
||||
onChangePage={setCurrentPage}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Button variant="outline_bg" onClick={handleClose} isDisabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="primary"
|
||||
onClick={handleSaveChanges}
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useUpdateRenewalConfig } from "@app/hooks/api";
|
||||
import { useGetCertificateProfileById } from "@app/hooks/api/certificateProfiles";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const DEFAULT_RENEWAL_BEFORE_DAYS = 20;
|
||||
const MIN_RENEWAL_BEFORE_DAYS = 1;
|
||||
const MAX_RENEWAL_BEFORE_DAYS = 30;
|
||||
|
||||
const createFormSchema = (ttlDays: number, notAfter: string) =>
|
||||
z.object({
|
||||
renewBeforeDays: z
|
||||
.number()
|
||||
.min(MIN_RENEWAL_BEFORE_DAYS, `Renewal days must be at least ${MIN_RENEWAL_BEFORE_DAYS}`)
|
||||
.max(MAX_RENEWAL_BEFORE_DAYS, `Renewal days cannot exceed ${MAX_RENEWAL_BEFORE_DAYS}`)
|
||||
.refine(
|
||||
(value) => value < ttlDays,
|
||||
(value) => ({
|
||||
message: `Renewal days (${value}) must be less than certificate TTL (${ttlDays} days)`
|
||||
})
|
||||
)
|
||||
.refine(
|
||||
(value) => {
|
||||
const expiryDate = new Date(notAfter);
|
||||
const renewalDate = new Date(expiryDate.getTime() - value * 24 * 60 * 60 * 1000);
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(0, 0, 0, 0);
|
||||
return renewalDate >= tomorrow;
|
||||
},
|
||||
() => ({
|
||||
message: "Renewals can only be scheduled from tomorrow onwards."
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["manageRenewal"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["manageRenewal"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
const RenewalConfigForm = ({
|
||||
control,
|
||||
errors,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
buttonText,
|
||||
onCancel
|
||||
}: {
|
||||
control: any;
|
||||
errors: { renewBeforeDays?: { message?: string } };
|
||||
onSubmit: (e?: React.BaseSyntheticEvent) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
buttonText: string;
|
||||
onCancel: () => void;
|
||||
}) => (
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormControl
|
||||
label="Auto-renew days before expiry"
|
||||
isError={Boolean(errors.renewBeforeDays)}
|
||||
errorText={errors.renewBeforeDays?.message}
|
||||
className="mb-6"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="renewBeforeDays"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={MIN_RENEWAL_BEFORE_DAYS}
|
||||
max={MAX_RENEWAL_BEFORE_DAYS}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
field.onChange(value);
|
||||
}}
|
||||
placeholder="Enter days before expiration"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button type="button" colorSchema="secondary" variant="plain" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" colorSchema="primary" isLoading={isLoading} isDisabled={isLoading}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentProject } = useProject();
|
||||
const { mutateAsync: updateRenewalConfig, isPending: isUpdatingConfig } =
|
||||
useUpdateRenewalConfig();
|
||||
|
||||
const certificateData = popUp.manageRenewal.data as {
|
||||
certificateId: string;
|
||||
commonName: string;
|
||||
profileId: string;
|
||||
renewBeforeDays?: number;
|
||||
ttlDays?: number;
|
||||
notAfter: string;
|
||||
renewalError?: string;
|
||||
renewedFromCertificateId?: string;
|
||||
renewedByCertificateId?: string;
|
||||
};
|
||||
|
||||
const { data: profileData } = useGetCertificateProfileById({
|
||||
profileId: certificateData?.profileId || ""
|
||||
});
|
||||
|
||||
const defaultRenewalDays = useMemo(() => {
|
||||
if (certificateData?.renewBeforeDays) {
|
||||
return certificateData.renewBeforeDays;
|
||||
}
|
||||
if (profileData?.apiConfig?.renewBeforeDays) {
|
||||
return profileData.apiConfig.renewBeforeDays;
|
||||
}
|
||||
return DEFAULT_RENEWAL_BEFORE_DAYS;
|
||||
}, [certificateData?.renewBeforeDays, profileData?.apiConfig?.renewBeforeDays]);
|
||||
|
||||
const isAutoRenewalEnabled = Boolean(
|
||||
certificateData?.renewBeforeDays && certificateData.renewBeforeDays > 0
|
||||
);
|
||||
|
||||
const hasRenewalError = Boolean(certificateData?.renewalError);
|
||||
|
||||
const formSchema = createFormSchema(
|
||||
certificateData?.ttlDays || 365,
|
||||
certificateData?.notAfter || ""
|
||||
);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
renewBeforeDays: defaultRenewalDays
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (popUp.manageRenewal.isOpen) {
|
||||
reset({
|
||||
renewBeforeDays: defaultRenewalDays
|
||||
});
|
||||
}
|
||||
}, [popUp.manageRenewal.isOpen, defaultRenewalDays, reset]);
|
||||
|
||||
const onUpdateRenewal = async (data: FormData) => {
|
||||
try {
|
||||
if (!currentProject?.slug) {
|
||||
createNotification({
|
||||
text: "Unable to update auto-renewal: Project not found. Please refresh the page and try again.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await updateRenewalConfig({
|
||||
certificateId: certificateData.certificateId,
|
||||
renewBeforeDays: data.renewBeforeDays,
|
||||
projectSlug: currentProject.slug
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: isAutoRenewalEnabled
|
||||
? "Auto-renewal configuration updated successfully"
|
||||
: "Auto-renewal enabled successfully",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpToggle("manageRenewal", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: isAutoRenewalEnabled
|
||||
? "Failed to update auto-renewal configuration. Please check your inputs and try again."
|
||||
: "Failed to enable auto-renewal. Please check your inputs and try again.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getModalTitle = () => {
|
||||
if (hasRenewalError) {
|
||||
return `Fix Auto-Renewal: ${certificateData?.commonName || ""}`;
|
||||
}
|
||||
if (isAutoRenewalEnabled) {
|
||||
return `Manage Auto-Renewal for ${certificateData?.commonName || ""}`;
|
||||
}
|
||||
return `Enable Auto-Renewal for ${certificateData?.commonName || ""}`;
|
||||
};
|
||||
|
||||
if (!certificateData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.manageRenewal?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("manageRenewal", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={getModalTitle()}>
|
||||
{hasRenewalError && (
|
||||
<div className="mb-6 rounded-md border border-red-600 bg-red-900/20 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-600">
|
||||
<span className="text-xs font-bold text-white">!</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-red-400">Automatic Renewal Failed</h3>
|
||||
<p className="mt-1 text-sm text-red-300">
|
||||
The last automatic renewal attempt failed: {certificateData.renewalError}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-red-300">
|
||||
You can reconfigure auto-renewal below or disable it completely.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!isAutoRenewalEnabled || hasRenewalError) && (
|
||||
<RenewalConfigForm
|
||||
control={control}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit(onUpdateRenewal)}
|
||||
isLoading={isUpdatingConfig}
|
||||
buttonText={isAutoRenewalEnabled ? "Update Configuration" : "Enable Auto-Renewal"}
|
||||
onCancel={() => handlePopUpToggle("manageRenewal", false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isAutoRenewalEnabled && !hasRenewalError && (
|
||||
<RenewalConfigForm
|
||||
control={control}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit(onUpdateRenewal)}
|
||||
isLoading={isUpdatingConfig}
|
||||
buttonText="Update Configuration"
|
||||
onCancel={() => handlePopUpToggle("manageRenewal", false)}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useUpdateRenewalConfig } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const createFormSchema = (ttlDays: number) =>
|
||||
z.object({
|
||||
renewBeforeDays: z
|
||||
.number()
|
||||
.min(1, "Renewal days must be at least 1")
|
||||
.max(365, "Renewal days cannot exceed 365")
|
||||
.refine(
|
||||
(value) => value < ttlDays,
|
||||
(value) => ({
|
||||
message: `Renewal days (${value}) must be less than certificate TTL (${ttlDays} days)`
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["configureRenewal"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["configureRenewal"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CertificateRenewalConfigModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentProject } = useProject();
|
||||
const { mutateAsync: updateRenewalConfig, isPending: isSubmitting } = useUpdateRenewalConfig();
|
||||
|
||||
const certificateData = popUp.configureRenewal.data as {
|
||||
certificateId: string;
|
||||
commonName: string;
|
||||
profileId: string;
|
||||
renewBeforeDays?: number;
|
||||
ttlDays: number;
|
||||
};
|
||||
|
||||
const formSchema = createFormSchema(certificateData.ttlDays);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
renewBeforeDays: certificateData?.renewBeforeDays || 1
|
||||
}
|
||||
});
|
||||
|
||||
const renewBeforeDays = watch("renewBeforeDays");
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
try {
|
||||
if (!currentProject?.slug) {
|
||||
createNotification({
|
||||
text: "Project not found",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await updateRenewalConfig({
|
||||
certificateId: certificateData.certificateId,
|
||||
renewBeforeDays: data.renewBeforeDays,
|
||||
projectSlug: currentProject.slug
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated auto-renewal configuration",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpToggle("configureRenewal", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update auto-renewal configuration",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.configureRenewal?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("configureRenewal", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`Configure Auto-Renewal: ${certificateData?.commonName || ""}`}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mb-4">
|
||||
<p className="mb-4 text-sm text-mineshaft-300">
|
||||
Configure when this certificate should be automatically renewed. The certificate will
|
||||
be renewed when it has the specified number of days remaining before expiration.
|
||||
</p>
|
||||
|
||||
<div className="mb-4 rounded border bg-mineshaft-800 p-3">
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
<strong>Certificate TTL:</strong> {certificateData?.ttlDays} days
|
||||
</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
<strong>Current Setting:</strong>{" "}
|
||||
{certificateData?.renewBeforeDays
|
||||
? `${certificateData.renewBeforeDays} days before expiration`
|
||||
: "Disabled"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="renewBeforeDays"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Renew Before Days"
|
||||
isError={Boolean(errors.renewBeforeDays)}
|
||||
errorText={errors.renewBeforeDays?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
max={certificateData?.ttlDays ? certificateData.ttlDays - 1 : undefined}
|
||||
placeholder="Enter days before expiration"
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
field.onChange(Number.isNaN(value) ? 0 : value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{renewBeforeDays && certificateData?.ttlDays && (
|
||||
<div className="mt-2 rounded bg-primary-900/20 p-2">
|
||||
<p className="text-sm text-primary-300">
|
||||
{renewBeforeDays >= certificateData.ttlDays
|
||||
? "⚠️ Renewal days must be less than certificate TTL"
|
||||
: `✓ Certificate will be renewed ${renewBeforeDays} days before expiration`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || renewBeforeDays >= (certificateData?.ttlDays || 0)}
|
||||
>
|
||||
Update Configuration
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("configureRenewal", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useUpdateRenewalConfig } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["disableRenewal"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["disableRenewal"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const CertificateRenewalDisableModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentProject } = useProject();
|
||||
const { mutateAsync: updateRenewalConfig, isPending: isSubmitting } = useUpdateRenewalConfig();
|
||||
|
||||
const certificateData = popUp.disableRenewal.data as {
|
||||
certificateId: string;
|
||||
commonName: string;
|
||||
};
|
||||
|
||||
const onDisableConfirm = async () => {
|
||||
try {
|
||||
if (!currentProject?.slug) {
|
||||
createNotification({
|
||||
text: "Project not found",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await updateRenewalConfig({
|
||||
certificateId: certificateData.certificateId,
|
||||
projectSlug: currentProject.slug,
|
||||
enableAutoRenewal: false
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully disabled auto-renewal",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpToggle("disableRenewal", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to disable auto-renewal",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.disableRenewal?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("disableRenewal", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`Disable Auto-Renewal: ${certificateData?.commonName || ""}`}>
|
||||
<div className="mb-4">
|
||||
<p className="mb-3 text-sm text-mineshaft-300">
|
||||
Are you sure you want to disable auto-renewal for this certificate?
|
||||
</p>
|
||||
<div className="rounded border border-yellow-700/50 bg-yellow-900/20 p-3">
|
||||
<p className="text-sm text-yellow-300">
|
||||
<strong>Warning:</strong> Once disabled, this certificate will not be automatically
|
||||
renewed and may expire without notice. You can re-enable auto-renewal at any time.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
onClick={onDisableConfirm}
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Disable Auto-Renewal
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("disableRenewal", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { faRedo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useRenewCertificate } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["renewCertificate"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["renewCertificate"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CertificateRenewalModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { mutateAsync: renewCertificate, isPending: isRenewing } = useRenewCertificate();
|
||||
|
||||
const onRenewConfirm = async () => {
|
||||
try {
|
||||
const { certificateId } = popUp.renewCertificate.data as { certificateId: string };
|
||||
|
||||
await renewCertificate({
|
||||
certificateId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Certificate renewed successfully",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpToggle("renewCertificate", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const certificateData = popUp.renewCertificate.data as {
|
||||
certificateId: string;
|
||||
commonName: string;
|
||||
profileId: string;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.renewCertificate?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("renewCertificate", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`Renew Certificate: ${certificateData?.commonName || ""}`}>
|
||||
<div className="mb-6">
|
||||
<p className="mb-4 text-sm text-mineshaft-300">
|
||||
Are you sure you want to renew this certificate now?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
onClick={onRenewConfirm}
|
||||
colorSchema="primary"
|
||||
isLoading={isRenewing}
|
||||
isDisabled={isRenewing}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRedo} className="mr-2" />
|
||||
Renew Now
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("renewCertificate", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+12
-4
@@ -172,10 +172,14 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
||||
ttl,
|
||||
keyUsages: Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertKeyUsage),
|
||||
.map(([key]) =>
|
||||
key === CertKeyUsage.CRL_SIGN
|
||||
? "cRLSign"
|
||||
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
),
|
||||
extendedKeyUsages: Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage)
|
||||
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -193,10 +197,14 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
||||
ttl,
|
||||
keyUsages: Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertKeyUsage),
|
||||
.map(([key]) =>
|
||||
key === CertKeyUsage.CRL_SIGN
|
||||
? "cRLSign"
|
||||
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
),
|
||||
extendedKeyUsages: Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage)
|
||||
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
|
||||
});
|
||||
|
||||
createNotification({
|
||||
|
||||
+2
-1
@@ -104,7 +104,8 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="Managing template enrollment options for EST is only available on Infisical's Enterprise plan."
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
text="Your current plan does not include access to managing template enrollment options for EST. To unlock this feature, please upgrade to Infisical Enterprise plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+4
-1
@@ -36,6 +36,7 @@ type Props = {
|
||||
data?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
isEnterpriseFeature?: boolean;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
@@ -90,7 +91,9 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (!subscription?.pkiEst) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
isEnterpriseFeature: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+16
-5
@@ -7,8 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub,
|
||||
useProject,
|
||||
useSubscription
|
||||
useProject
|
||||
} from "@app/context";
|
||||
import { useDeleteCert } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
@@ -16,16 +15,19 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { CertificateCertModal } from "./CertificateCertModal";
|
||||
import { CertificateImportModal } from "./CertificateImportModal";
|
||||
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
||||
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
|
||||
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
|
||||
import { CertificateModal } from "./CertificateModal";
|
||||
import { CertificateRenewalModal } from "./CertificateRenewalModal";
|
||||
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
||||
import { CertificatesTable } from "./CertificatesTable";
|
||||
|
||||
export const CertificatesSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { subscription } = useSubscription();
|
||||
const { mutateAsync: deleteCert } = useDeleteCert();
|
||||
|
||||
const isLegacyTemplatesEnabled = subscription.pkiLegacyTemplates;
|
||||
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
|
||||
const isLegacyTemplatesEnabled = true;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificateIssuance",
|
||||
@@ -33,7 +35,10 @@ export const CertificatesSection = () => {
|
||||
"certificateImport",
|
||||
"certificateCert",
|
||||
"deleteCertificate",
|
||||
"revokeCertificate"
|
||||
"revokeCertificate",
|
||||
"manageRenewal",
|
||||
"renewCertificate",
|
||||
"managePkiSyncs"
|
||||
] as const);
|
||||
|
||||
const onRemoveCertificateSubmit = async (serialNumber: string) => {
|
||||
@@ -98,7 +103,13 @@ export const CertificatesSection = () => {
|
||||
)}
|
||||
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateManagePkiSyncsModal
|
||||
popUp={popUp.managePkiSyncs}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCertificate.isOpen}
|
||||
title={`Are you sure you want to remove the certificate ${
|
||||
|
||||
+345
-34
@@ -2,18 +2,25 @@ import { useMemo, useState } from "react";
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
faClockRotateLeft,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFileExport,
|
||||
faLink,
|
||||
faRedo,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
CertificateDisplayName,
|
||||
getCertificateDisplayName
|
||||
} from "@app/components/utilities/certificateDisplayUtils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -30,28 +37,53 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub,
|
||||
useProject
|
||||
} from "@app/context";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api";
|
||||
import { useUpdateRenewalConfig } from "@app/hooks/api";
|
||||
import { caSupportsCapability } from "@app/hooks/api/ca/constants";
|
||||
import { CaCapability, CaType } from "@app/hooks/api/ca/enums";
|
||||
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||
import { CertStatus } from "@app/hooks/api/certificates/enums";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
|
||||
|
||||
const isExpiringWithinOneDay = (notAfter: string): boolean => {
|
||||
const expiryDate = new Date(notAfter);
|
||||
const now = new Date();
|
||||
const oneDayFromNow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
return expiryDate <= oneDayFromNow;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["certificate", "deleteCertificate", "revokeCertificate", "certificateCert"]
|
||||
[
|
||||
"certificate",
|
||||
"deleteCertificate",
|
||||
"revokeCertificate",
|
||||
"certificateCert",
|
||||
"manageRenewal",
|
||||
"renewCertificate",
|
||||
"managePkiSyncs"
|
||||
]
|
||||
>,
|
||||
data?: {
|
||||
serialNumber?: string;
|
||||
commonName?: string;
|
||||
certificateId?: string;
|
||||
profileId?: string;
|
||||
renewBeforeDays?: number;
|
||||
ttlDays?: number;
|
||||
notAfter?: string;
|
||||
renewalError?: string;
|
||||
renewedFromCertificateId?: string;
|
||||
renewedByCertificateId?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
@@ -69,10 +101,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
limit: perPage
|
||||
});
|
||||
|
||||
// Fetch CA data to determine capabilities
|
||||
const { mutateAsync: updateRenewalConfig } = useUpdateRenewalConfig();
|
||||
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
|
||||
const isLegacyTemplatesEnabled = true;
|
||||
|
||||
const { data: caData } = useListCasByProjectId(currentProject?.id ?? "");
|
||||
|
||||
// Create mapping from caId to CA type for capability checking
|
||||
const caCapabilityMap = useMemo(() => {
|
||||
if (!caData) return {};
|
||||
|
||||
@@ -83,26 +117,78 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
return map;
|
||||
}, [caData]);
|
||||
|
||||
const handleDisableAutoRenewal = async (certificateId: string, commonName: string) => {
|
||||
try {
|
||||
if (!currentProject?.slug) {
|
||||
createNotification({
|
||||
text: "Unable to disable auto-renewal: Project not found. Please refresh the page and try again.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await updateRenewalConfig({
|
||||
certificateId,
|
||||
projectSlug: currentProject.slug,
|
||||
enableAutoRenewal: false
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Auto-renewal disabled for ${commonName}`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to disable auto-renewal. Please try again or contact support if the issue persists.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Common Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Not Before</Th>
|
||||
<Th>Not After</Th>
|
||||
<Th />
|
||||
<Th className="w-1/2">SAN / CN</Th>
|
||||
<Th className="w-1/6">Status</Th>
|
||||
<Th className="w-1/6">Not Before</Th>
|
||||
<Th className="w-1/6">Not After</Th>
|
||||
<Th className="w-12" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="project-cas" />}
|
||||
{isPending && <TableSkeleton columns={4} innerKey="project-cas" />}
|
||||
{!isPending &&
|
||||
data?.certificates.map((certificate) => {
|
||||
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
||||
|
||||
const isRevoked = certificate.status === CertStatus.REVOKED;
|
||||
const isExpired = new Date(certificate.notAfter) < new Date();
|
||||
const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter);
|
||||
const hasFailed = Boolean(certificate.renewalError);
|
||||
const isAutoRenewalEnabled = Boolean(
|
||||
certificate.renewBeforeDays && certificate.renewBeforeDays > 0
|
||||
);
|
||||
|
||||
const canShowAutoRenewalIcon = Boolean(
|
||||
certificate.profileId &&
|
||||
certificate.hasPrivateKey !== false &&
|
||||
!certificate.renewedByCertificateId &&
|
||||
!isRevoked &&
|
||||
!isExpired &&
|
||||
!isExpiringWithinDay
|
||||
);
|
||||
|
||||
// Still need originalDisplayName for other uses in the component
|
||||
const { originalDisplayName } = getCertificateDisplayName(certificate, 64, "—");
|
||||
|
||||
return (
|
||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td>{certificate.commonName}</Td>
|
||||
<Tr className="group h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td className="max-w-0">
|
||||
<CertificateDisplayName cert={certificate} maxLength={64} fallback="—" />
|
||||
</Td>
|
||||
<Td>
|
||||
{certificate.status === CertStatus.REVOKED ? (
|
||||
<Badge variant="danger">Revoked</Badge>
|
||||
@@ -120,7 +206,64 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
<Td className="flex items-center justify-end gap-2">
|
||||
<div
|
||||
className={`transition-opacity ${(() => {
|
||||
if (!canShowAutoRenewalIcon) return "";
|
||||
if (isAutoRenewalEnabled) return "opacity-100";
|
||||
return "opacity-0 group-hover:opacity-100";
|
||||
})()}`}
|
||||
>
|
||||
{canShowAutoRenewalIcon && (
|
||||
<Tooltip
|
||||
content={(() => {
|
||||
if (hasFailed && certificate.renewalError) {
|
||||
return `Auto-renewal failed: ${certificate.renewalError}`;
|
||||
}
|
||||
if (isAutoRenewalEnabled) {
|
||||
const expiryDate = new Date(certificate.notAfter);
|
||||
const now = new Date();
|
||||
const daysUntilExpiry = Math.ceil(
|
||||
(expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
|
||||
);
|
||||
const daysUntilRenewal = Math.max(
|
||||
0,
|
||||
daysUntilExpiry - (certificate.renewBeforeDays || 0)
|
||||
);
|
||||
return `Auto-renews in ${daysUntilRenewal}d`;
|
||||
}
|
||||
return "Set auto renewal";
|
||||
})()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={(() => {
|
||||
if (hasFailed) return "pr-1 text-red-500 hover:text-red-400";
|
||||
return "pr-1 text-primary-500 hover:text-primary-400";
|
||||
})()}
|
||||
aria-label="Certificate auto-renewal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (hasFailed) return;
|
||||
|
||||
handlePopUpOpen("manageRenewal", {
|
||||
certificateId: certificate.id,
|
||||
commonName: originalDisplayName,
|
||||
profileId: certificate.profileId || "",
|
||||
renewBeforeDays: certificate.renewBeforeDays || 7,
|
||||
ttlDays: Math.ceil(
|
||||
(new Date(certificate.notAfter).getTime() -
|
||||
new Date(certificate.notBefore).getTime()) /
|
||||
(24 * 60 * 60 * 1000)
|
||||
)
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClockRotateLeft} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
@@ -151,31 +294,199 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEye} />}
|
||||
{isLegacyTemplatesEnabled && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEye} />}
|
||||
>
|
||||
View Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
{/* Manage auto renewal option - not shown for failed renewals */}
|
||||
{(() => {
|
||||
const canManageRenewal =
|
||||
certificate.profileId &&
|
||||
certificate.hasPrivateKey !== false &&
|
||||
!certificate.renewedByCertificateId &&
|
||||
!isRevoked &&
|
||||
!isExpired &&
|
||||
!hasFailed &&
|
||||
!isExpiringWithinDay;
|
||||
|
||||
if (!canManageRenewal) return null;
|
||||
|
||||
return (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Edit}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
View Details
|
||||
</DropdownMenuItem>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
const notAfterDate = new Date(certificate.notAfter);
|
||||
const notBeforeDate = certificate.notBefore
|
||||
? new Date(certificate.notBefore)
|
||||
: new Date(
|
||||
notAfterDate.getTime() - 365 * 24 * 60 * 60 * 1000
|
||||
);
|
||||
const ttlDays = Math.max(
|
||||
1,
|
||||
Math.ceil(
|
||||
(notAfterDate.getTime() - notBeforeDate.getTime()) /
|
||||
(24 * 60 * 60 * 1000)
|
||||
)
|
||||
);
|
||||
handlePopUpOpen("manageRenewal", {
|
||||
certificateId: certificate.id,
|
||||
commonName: certificate.commonName,
|
||||
profileId: certificate.profileId,
|
||||
renewBeforeDays: certificate.renewBeforeDays,
|
||||
ttlDays,
|
||||
notAfter: certificate.notAfter,
|
||||
renewalError: certificate.renewalError,
|
||||
renewedFromCertificateId:
|
||||
certificate.renewedFromCertificateId,
|
||||
renewedByCertificateId: certificate.renewedByCertificateId
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faRedo} />}
|
||||
>
|
||||
{isAutoRenewalEnabled
|
||||
? "Manage auto renewal"
|
||||
: "Enable auto renewal"}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
);
|
||||
})()}
|
||||
{/* Disable auto renewal option - only shown when auto renewal is active */}
|
||||
{(() => {
|
||||
const canDisableRenewal =
|
||||
certificate.profileId &&
|
||||
certificate.hasPrivateKey !== false &&
|
||||
!certificate.renewedByCertificateId &&
|
||||
!isRevoked &&
|
||||
!isExpired &&
|
||||
!isExpiringWithinDay &&
|
||||
isAutoRenewalEnabled;
|
||||
|
||||
if (!canDisableRenewal) return null;
|
||||
|
||||
return (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Edit}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
await handleDisableAutoRenewal(
|
||||
certificate.id,
|
||||
certificate.commonName
|
||||
);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faBan} />}
|
||||
>
|
||||
Disable auto renewal
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
);
|
||||
})()}
|
||||
{/* Manual renewal action for profile-issued certificates that are not revoked/expired (including failed ones) */}
|
||||
{(() => {
|
||||
const canRenew =
|
||||
certificate.profileId &&
|
||||
certificate.hasPrivateKey !== false &&
|
||||
!certificate.renewedByCertificateId &&
|
||||
!isRevoked &&
|
||||
!isExpired;
|
||||
|
||||
if (!canRenew) return null;
|
||||
|
||||
return (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Edit}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("renewCertificate", {
|
||||
certificateId: certificate.id,
|
||||
commonName: certificate.commonName
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faRedo} />}
|
||||
>
|
||||
Renew Now
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
);
|
||||
})()}
|
||||
{/* PKI Sync management - only for active certificates that are not renewed */}
|
||||
{certificate.status === CertStatus.ACTIVE &&
|
||||
!certificate.renewedByCertificateId && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Edit}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("managePkiSyncs", {
|
||||
certificateId: certificate.id,
|
||||
commonName: certificate.commonName
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faLink} />}
|
||||
>
|
||||
Manage PKI Syncs
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{/* Only show revoke button if CA supports revocation */}
|
||||
{(() => {
|
||||
const caType = caCapabilityMap[certificate.caId];
|
||||
// If caId not found in map, assume CA supports revocation to avoid hiding revoke option
|
||||
const supportsRevocation =
|
||||
!caType ||
|
||||
caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES);
|
||||
|
||||
+2
-2
@@ -5,13 +5,13 @@ export const getCertValidUntilBadgeDetails = (notAfter: string) => {
|
||||
const notAfterDate = new Date(notAfter).getTime();
|
||||
const diffInMs = notAfterDate - currentDate;
|
||||
|
||||
let variant: "success" | "primary" | "danger" = "success";
|
||||
let variant: "success" | "warning" | "danger" = "success";
|
||||
let label = "Healthy";
|
||||
|
||||
if (diffInMs > ms("60d")) {
|
||||
variant = "success";
|
||||
} else if (diffInMs > ms("30d")) {
|
||||
variant = "primary";
|
||||
variant = "warning";
|
||||
} else {
|
||||
variant = "danger";
|
||||
}
|
||||
|
||||
+21
-1
@@ -11,6 +11,26 @@ import {
|
||||
mapTemplateSignatureAlgorithmToApi
|
||||
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||
|
||||
const convertTemplateTtlToCertificateTtl = (templateTtl: string): string => {
|
||||
const match = templateTtl.match(/^(\d+)([dmyh])$/);
|
||||
if (!match) return templateTtl;
|
||||
|
||||
const [, value, unit] = match;
|
||||
const numValue = parseInt(value, 10);
|
||||
|
||||
switch (unit) {
|
||||
case "m":
|
||||
return `${numValue * 30}d`;
|
||||
case "y":
|
||||
return `${numValue * 365}d`;
|
||||
case "d":
|
||||
case "h":
|
||||
return templateTtl;
|
||||
default:
|
||||
return templateTtl;
|
||||
}
|
||||
};
|
||||
|
||||
export type TemplateConstraints = {
|
||||
allowedKeyUsages: string[];
|
||||
allowedExtendedKeyUsages: string[];
|
||||
@@ -118,7 +138,7 @@ export const useCertificateTemplate = (
|
||||
|
||||
// Set TTL if available
|
||||
if (templateData.validity?.max) {
|
||||
setValue("ttl", templateData.validity.max);
|
||||
setValue("ttl", convertTemplateTtlToCertificateTtl(templateData.validity.max));
|
||||
}
|
||||
|
||||
// Handle SAN types
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { CertificatesPage } from "./CertificatesPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
|
||||
)({
|
||||
component: CertificatesPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Certificates"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
+6
-30
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faBan,
|
||||
faCalendarCheck,
|
||||
faCheck,
|
||||
faCopy,
|
||||
@@ -13,12 +12,12 @@ import {
|
||||
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 { BanIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -29,7 +28,6 @@ import {
|
||||
PkiSyncStatusBadge
|
||||
} from "@app/components/pki-syncs";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -39,6 +37,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
@@ -47,7 +46,6 @@ import { useToggle } from "@app/hooks";
|
||||
import { PkiSyncStatus, TPkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
|
||||
import { PkiSyncTableCell } from "./PkiSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
@@ -163,26 +161,6 @@ export const PkiSyncRow = ({
|
||||
<p className="truncate text-xs leading-4 text-bunker-300">{destinationDetails.name}</p>
|
||||
</div>
|
||||
</Td>
|
||||
{subscriberId ? (
|
||||
<PkiSyncTableCell
|
||||
primaryText={pkiSync.subscriber?.name || 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">
|
||||
@@ -234,12 +212,10 @@ export const PkiSyncRow = ({
|
||||
className="text-xs"
|
||||
content="Auto-Sync is disabled. Certificate changes in the PKI subscriber will not be automatically synced to the destination."
|
||||
>
|
||||
<div>
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
{!syncStatus && "Auto-Sync Disabled"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="neutral">
|
||||
<BanIcon />
|
||||
{!syncStatus && "Auto-Sync Disabled"}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
{syncOption?.canImportCertificates && <PkiSyncImportStatusBadge mini pkiSync={pkiSync} />}
|
||||
|
||||
+3
-25
@@ -57,7 +57,6 @@ import { PkiSyncRow } from "./PkiSyncRow";
|
||||
|
||||
enum PkiSyncsOrderBy {
|
||||
Destination = "destination",
|
||||
Source = "source",
|
||||
Name = "name",
|
||||
Status = "status"
|
||||
}
|
||||
@@ -160,14 +159,6 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
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()
|
||||
@@ -370,7 +361,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2" />
|
||||
<Th className="w-1/4">
|
||||
<Th className="w-1/2">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
@@ -383,20 +374,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
</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">
|
||||
<Th className="w-1/4">
|
||||
<div className="flex items-center">
|
||||
Destination
|
||||
<IconButton
|
||||
@@ -409,7 +387,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="min-w-42">
|
||||
<Th className="w-1/4 min-w-42">
|
||||
<div className="flex items-center">
|
||||
Status
|
||||
<IconButton
|
||||
|
||||
+46
-18
@@ -1,23 +1,25 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { 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 { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useListPkiSyncs } from "@app/hooks/api/pkiSyncs";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
|
||||
import { PkiSyncsTable } from "./PkiSyncTable";
|
||||
|
||||
export const PkiSyncsTab = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const);
|
||||
|
||||
const { addSync, ...search } = useSearch({
|
||||
const { addSync, connectionId, connectionName, ...search } = useSearch({
|
||||
from: ROUTE_PATHS.CertManager.IntegrationsListPage.id
|
||||
});
|
||||
|
||||
@@ -44,6 +46,42 @@ export const PkiSyncsTab = () => {
|
||||
navigateToBase();
|
||||
}, [addSync, handlePopUpOpen, navigateToBase]);
|
||||
|
||||
useEffect(() => {
|
||||
const storedFormData = localStorage.getItem("pkiSyncFormData");
|
||||
if (storedFormData && !popUp.addSync.isOpen) {
|
||||
try {
|
||||
const parsedData = JSON.parse(storedFormData);
|
||||
if (connectionId && connectionName) {
|
||||
const initialData = {
|
||||
...parsedData,
|
||||
connection: { id: connectionId, name: connectionName }
|
||||
};
|
||||
handlePopUpOpen("addSync", { destination: parsedData.destination, initialData });
|
||||
navigate({
|
||||
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
|
||||
params: { projectId: currentProject?.id },
|
||||
search: { selectedTab: IntegrationsListPageTabs.PkiSyncs },
|
||||
replace: true
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("addSync", { destination: parsedData.destination });
|
||||
}
|
||||
localStorage.removeItem("pkiSyncFormData");
|
||||
} catch (error) {
|
||||
console.error("Failed to parse stored PKI sync form data:", error);
|
||||
localStorage.removeItem("pkiSyncFormData");
|
||||
handlePopUpOpen("addSync");
|
||||
}
|
||||
}
|
||||
}, [
|
||||
handlePopUpOpen,
|
||||
popUp.addSync.isOpen,
|
||||
connectionId,
|
||||
connectionName,
|
||||
navigate,
|
||||
currentProject?.id
|
||||
]);
|
||||
|
||||
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
|
||||
currentProject?.id || "",
|
||||
{
|
||||
@@ -64,21 +102,10 @@ export const PkiSyncsTab = () => {
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Certificate Syncs</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/pki-syncs/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.32rem] ml-1 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 className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Certificate Syncs</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/pki/certificate-syncs/overview" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Use App Connections to sync certificates to third-party services.
|
||||
@@ -104,7 +131,8 @@ export const PkiSyncsTab = () => {
|
||||
<PkiSyncsTable pkiSyncs={pkiSyncs} />
|
||||
</div>
|
||||
<CreatePkiSyncModal
|
||||
selectSync={popUp.addSync.data}
|
||||
selectSync={popUp.addSync.data?.destination || popUp.addSync.data}
|
||||
initialData={popUp.addSync.data?.initialData}
|
||||
isOpen={popUp.addSync.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSync", isOpen)}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,9 @@ import { IntegrationsListPage } from "./IntegrationsListPage";
|
||||
|
||||
const IntegrationsListPageQuerySchema = z.object({
|
||||
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(),
|
||||
addSync: z.nativeEnum(PkiSync).optional()
|
||||
addSync: z.nativeEnum(PkiSync).optional(),
|
||||
connectionId: z.string().optional(),
|
||||
connectionName: z.string().optional()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
|
||||
+4
-4
@@ -59,9 +59,9 @@ export const PkiCollectionPage = () => {
|
||||
});
|
||||
handlePopUpClose("deletePkiCollection");
|
||||
navigate({
|
||||
to: "/projects/cert-management/$projectId/certificates",
|
||||
to: "/projects/cert-management/$projectId/policies",
|
||||
params: {
|
||||
projectId
|
||||
projectId: params.projectId
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
@@ -77,9 +77,9 @@ export const PkiCollectionPage = () => {
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificates"
|
||||
to="/projects/cert-management/$projectId/policies"
|
||||
params={{
|
||||
projectId
|
||||
projectId: params.projectId
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
|
||||
+2
-6
@@ -5,13 +5,9 @@ import { z } from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useAddItemToPkiCollection,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates
|
||||
} from "@app/hooks/api";
|
||||
import { CaStatus, useAddItemToPkiCollection, useListWorkspaceCas } from "@app/hooks/api";
|
||||
import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Certificate Collections",
|
||||
link: linkOptions({
|
||||
to: "/projects/cert-management/$projectId/certificates",
|
||||
to: "/projects/cert-management/$projectId/policies",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,7 +7,6 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
@@ -93,7 +93,7 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
|
||||
}
|
||||
|
||||
if (daysUntilExpiry < 30) {
|
||||
return <Badge variant="primary">Expiring Soon</Badge>;
|
||||
return <Badge variant="warning">Expiring Soon</Badge>;
|
||||
}
|
||||
|
||||
return <Badge variant="success">Valid</Badge>;
|
||||
|
||||
+16
-4
@@ -351,8 +351,14 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
commonName,
|
||||
subjectAlternativeNames: subjectAlternativeNamesList,
|
||||
ttl,
|
||||
keyUsages: keyUsagesList,
|
||||
extendedKeyUsages: extendedKeyUsagesList,
|
||||
keyUsages: keyUsagesList.map((key) =>
|
||||
key === CertKeyUsage.CRL_SIGN
|
||||
? "cRLSign"
|
||||
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
),
|
||||
extendedKeyUsages: extendedKeyUsagesList.map((key) =>
|
||||
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
),
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays,
|
||||
properties: Object.keys(properties).length > 0 ? properties : undefined
|
||||
@@ -365,8 +371,14 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
commonName,
|
||||
subjectAlternativeNames: subjectAlternativeNamesList,
|
||||
ttl,
|
||||
keyUsages: keyUsagesList,
|
||||
extendedKeyUsages: extendedKeyUsagesList,
|
||||
keyUsages: keyUsagesList.map((key) =>
|
||||
key === CertKeyUsage.CRL_SIGN
|
||||
? "cRLSign"
|
||||
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
),
|
||||
extendedKeyUsages: extendedKeyUsagesList.map((key) =>
|
||||
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
),
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays,
|
||||
properties: Object.keys(properties).length > 0 ? properties : undefined
|
||||
|
||||
+3
-4
@@ -7,8 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
useProject,
|
||||
useSubscription
|
||||
useProject
|
||||
} from "@app/context";
|
||||
import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api";
|
||||
import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types";
|
||||
@@ -19,10 +18,10 @@ import { PkiSubscribersTable } from "./PkiSubscribersTable";
|
||||
|
||||
export const PkiSubscriberSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { subscription } = useSubscription();
|
||||
const projectId = currentProject.id;
|
||||
|
||||
const canCreateLegacySubscribers = subscription.pkiLegacyTemplates;
|
||||
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
|
||||
const canCreateLegacySubscribers = true;
|
||||
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
|
||||
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,6 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
|
||||
@@ -18,10 +18,10 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import {
|
||||
PkiSyncActionTriggers,
|
||||
PkiSyncAuditLogsSection,
|
||||
PkiSyncCertificatesSection,
|
||||
PkiSyncDestinationSection,
|
||||
PkiSyncDetailsSection,
|
||||
PkiSyncOptionsSection,
|
||||
PkiSyncSourceSection
|
||||
PkiSyncOptionsSection
|
||||
} from "./components";
|
||||
|
||||
const PageContent = () => {
|
||||
@@ -62,7 +62,6 @@ const PageContent = () => {
|
||||
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);
|
||||
|
||||
@@ -103,7 +102,6 @@ const PageContent = () => {
|
||||
<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">
|
||||
@@ -111,6 +109,7 @@ const PageContent = () => {
|
||||
pkiSync={pkiSync}
|
||||
onEditDestination={handleEditDestination}
|
||||
/>
|
||||
<PkiSyncCertificatesSection pkiSync={pkiSync} />
|
||||
<PkiSyncAuditLogsSection pkiSync={pkiSync} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+9
-14
@@ -1,7 +1,6 @@
|
||||
import { useCallback } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faBan,
|
||||
faCheck,
|
||||
faCopy,
|
||||
faDownload,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { BanIcon, RefreshCwIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
PkiSyncRemoveStatusBadge
|
||||
} from "@app/components/pki-syncs";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
IconButton,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
@@ -136,24 +136,19 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
|
||||
{syncOption?.canImportCertificates && <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 variant="info">
|
||||
<RefreshCwIcon />
|
||||
Auto-Sync Enabled
|
||||
</Badge>
|
||||
) : (
|
||||
<Tooltip
|
||||
className="text-xs"
|
||||
content="Auto-Sync is disabled. Certificate changes in the PKI subscriber will not be automatically synced to the destination."
|
||||
>
|
||||
<div>
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
<span>Auto-Sync Disabled</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="neutral">
|
||||
<BanIcon />
|
||||
Auto-Sync Disabled
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div>
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export const PkiSyncAuditLogsSection = ({ pkiSync }: Props) => {
|
||||
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-medium text-mineshaft-100">Sync Logs</h3>
|
||||
<h3 className="text-lg font-medium 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
|
||||
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
import { useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faCertificate,
|
||||
faClockRotateLeft,
|
||||
faEdit,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { CertificateManagementModal } from "@app/components/pki-syncs/CertificateManagementModal";
|
||||
import {
|
||||
CertificateDisplayName,
|
||||
getCertificateDisplayName
|
||||
} from "@app/components/utilities/certificateDisplayUtils";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useListPkiSyncCertificates, useRemoveCertificatesFromPkiSync } from "@app/hooks/api";
|
||||
import { CertificateSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
};
|
||||
|
||||
const getSyncStatusVariant = (status?: CertificateSyncStatus | null) => {
|
||||
if (status === CertificateSyncStatus.Succeeded) return "success";
|
||||
if (status === CertificateSyncStatus.Failed) return "danger";
|
||||
if (status === CertificateSyncStatus.Syncing) return "neutral";
|
||||
return "project";
|
||||
};
|
||||
|
||||
const getSyncStatusText = (status?: CertificateSyncStatus | null) => {
|
||||
if (status === CertificateSyncStatus.Succeeded) return "Synced";
|
||||
if (status === CertificateSyncStatus.Failed) return "Failed";
|
||||
if (status === CertificateSyncStatus.Syncing) return "Syncing";
|
||||
if (status === CertificateSyncStatus.Pending) return "Pending";
|
||||
return "Unknown";
|
||||
};
|
||||
|
||||
const getCertificateStatusVariant = (isExpired: boolean, isRevoked: boolean) => {
|
||||
if (isRevoked) return "danger";
|
||||
if (isExpired) return "danger";
|
||||
return "success";
|
||||
};
|
||||
|
||||
const getCertificateStatusText = (isExpired: boolean, isRevoked: boolean) => {
|
||||
if (isRevoked) return "Revoked";
|
||||
if (isExpired) return "Expired";
|
||||
return "Active";
|
||||
};
|
||||
|
||||
export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
const [isManageModalOpen, setIsManageModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [certificateToDelete, setCertificateToDelete] = useState<{
|
||||
id: string;
|
||||
displayName: string;
|
||||
} | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
|
||||
const { data, refetch: refetchSyncCertificates } = useListPkiSyncCertificates(pkiSync.id, {
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
limit: pageSize
|
||||
});
|
||||
const syncCertificates = data?.certificates || [];
|
||||
const totalCount = data?.totalCount || 0;
|
||||
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
|
||||
|
||||
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberId: pkiSync.subscriberId || ""
|
||||
});
|
||||
|
||||
const handleRemoveCertificate = async (certificateId: string) => {
|
||||
try {
|
||||
await removeCertificatesFromSync.mutateAsync({
|
||||
pkiSyncId: pkiSync.id,
|
||||
certificateIds: [certificateId]
|
||||
});
|
||||
|
||||
await refetchSyncCertificates();
|
||||
|
||||
createNotification({
|
||||
text: "Certificate removed from sync",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
setIsDeleteModalOpen(false);
|
||||
setCertificateToDelete(null);
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to remove certificate from sync",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (certificateId: string, displayName: string) => {
|
||||
setCertificateToDelete({ id: certificateId, displayName });
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
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="text-lg font-medium text-mineshaft-100">Certificates</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="Edit certificates"
|
||||
onClick={() => setIsManageModalOpen(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2/8">SAN / CN</Th>
|
||||
<Th className="w-3/16">Certificate Status</Th>
|
||||
<Th className="w-3/16">Serial Number</Th>
|
||||
<Th className="w-1/8">Sync Status</Th>
|
||||
<Th className="w-1/8">Expires At</Th>
|
||||
<Th className="w-1/8" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{syncCertificates.map((syncCert) => {
|
||||
const isExpired = syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter) < new Date()
|
||||
: false;
|
||||
const isRevoked = syncCert.certificateStatus === "revoked";
|
||||
|
||||
// Calculate auto-renewal timeline
|
||||
const hasAutoRenewal = Boolean(
|
||||
syncCert.certificateRenewBeforeDays &&
|
||||
syncCert.certificateRenewBeforeDays > 0 &&
|
||||
!syncCert.certificateRenewalError &&
|
||||
syncCert.certificateNotAfter
|
||||
);
|
||||
|
||||
const daysUntilRenewal =
|
||||
hasAutoRenewal && syncCert.certificateNotAfter
|
||||
? (() => {
|
||||
const expiryDate = new Date(syncCert.certificateNotAfter);
|
||||
const renewalDate = new Date(
|
||||
expiryDate.getTime() -
|
||||
syncCert.certificateRenewBeforeDays! * 24 * 60 * 60 * 1000
|
||||
);
|
||||
const now = new Date();
|
||||
const diffInMs = renewalDate.getTime() - now.getTime();
|
||||
return Math.max(0, Math.ceil(diffInMs / (24 * 60 * 60 * 1000)));
|
||||
})()
|
||||
: null;
|
||||
|
||||
const { originalDisplayName } = getCertificateDisplayName(
|
||||
{
|
||||
altNames: syncCert.certificateAltNames,
|
||||
commonName: syncCert.certificateCommonName
|
||||
},
|
||||
34,
|
||||
"Unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr key={syncCert.id}>
|
||||
<Td className="max-w-0">
|
||||
<CertificateDisplayName
|
||||
cert={{
|
||||
altNames: syncCert.certificateAltNames,
|
||||
commonName: syncCert.certificateCommonName
|
||||
}}
|
||||
maxLength={34}
|
||||
fallback="Unknown"
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant={getCertificateStatusVariant(isExpired, isRevoked)}>
|
||||
{getCertificateStatusText(isExpired, isRevoked)}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate text-xs"
|
||||
title={syncCert.certificateSerialNumber || "Unknown"}
|
||||
>
|
||||
{(() => {
|
||||
const serial = syncCert.certificateSerialNumber;
|
||||
if (!serial || serial === "Unknown") return "Unknown";
|
||||
if (serial.length <= 8) return serial;
|
||||
return `${serial.substring(0, 4)}...${serial.substring(serial.length - 4)}`;
|
||||
})()}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{syncCert.lastSyncMessage &&
|
||||
syncCert.syncStatus === CertificateSyncStatus.Failed ? (
|
||||
<Tooltip content={syncCert.lastSyncMessage}>
|
||||
<Badge variant="danger">Failed</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge variant={getSyncStatusVariant(syncCert.syncStatus)}>
|
||||
{getSyncStatusText(syncCert.syncStatus)}
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter).toLocaleDateString()
|
||||
: "Unknown"}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="flex items-center justify-end gap-2 pr-4">
|
||||
{hasAutoRenewal && daysUntilRenewal !== null && (
|
||||
<Tooltip content={`Auto-renews in ${daysUntilRenewal}d`}>
|
||||
<div className="text-primary-500">
|
||||
<FontAwesomeIcon icon={faClockRotateLeft} size="sm" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSyncActions.Edit}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="Remove certificate"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() =>
|
||||
handleDeleteClick(syncCert.certificateId, originalDisplayName)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{syncCertificates.length === 0 && (
|
||||
<EmptyState
|
||||
title="No certificates are part of this certificate sync"
|
||||
icon={faCertificate}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={currentPage}
|
||||
perPage={pageSize}
|
||||
onChangePage={(page: number) => setCurrentPage(page)}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CertificateManagementModal
|
||||
pkiSync={pkiSync}
|
||||
isOpen={isManageModalOpen}
|
||||
onClose={() => setIsManageModalOpen(false)}
|
||||
onCertificatesUpdated={() => {
|
||||
refetchSyncCertificates();
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setCertificateToDelete(null);
|
||||
}}
|
||||
title="Remove Certificate from Sync"
|
||||
subTitle={`Are you sure you want to remove "${certificateToDelete?.displayName}" from this PKI sync?`}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
if (certificateToDelete) {
|
||||
await handleRemoveCertificate(certificateToDelete.id);
|
||||
}
|
||||
}}
|
||||
buttonText="Remove Certificate"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+12
-6
@@ -11,12 +11,15 @@ import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionC
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { AzureKeyVaultPkiSyncDestinationSection } from "./PkiSyncDestinationSection/index";
|
||||
import {
|
||||
AwsCertificateManagerPkiSyncDestinationSection,
|
||||
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 className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -32,6 +35,9 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
|
||||
let DestinationComponents: ReactNode;
|
||||
switch (destination) {
|
||||
case PkiSync.AwsCertificateManager:
|
||||
DestinationComponents = <AwsCertificateManagerPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
case PkiSync.AzureKeyVault:
|
||||
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
@@ -47,7 +53,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
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-medium text-mineshaft-100">Destination Configuration</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Destination Configuration</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -62,7 +68,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="flex w-full flex-wrap gap-8">
|
||||
<div className="flex w-full flex-wrap gap-8 pt-2">
|
||||
<GenericFieldLabel label={`${destinationDetails.name} Connection`}>
|
||||
{pkiSync.appConnectionName || "Default Connection"}
|
||||
</GenericFieldLabel>
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
};
|
||||
|
||||
export const AwsCertificateManagerPkiSyncDestinationSection = ({ pkiSync }: Props) => {
|
||||
const region =
|
||||
pkiSync.destinationConfig && "region" in pkiSync.destinationConfig
|
||||
? pkiSync.destinationConfig.region
|
||||
: undefined;
|
||||
|
||||
return <GenericFieldLabel label="AWS Region">{region || "Not specified"}</GenericFieldLabel>;
|
||||
};
|
||||
+3
-3
@@ -2,9 +2,9 @@
|
||||
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 className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
+1
@@ -1 +1,2 @@
|
||||
export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection";
|
||||
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
|
||||
|
||||
+24
-28
@@ -21,9 +21,9 @@ const GenericFieldLabel = ({
|
||||
children: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
}) => (
|
||||
<div>
|
||||
<label className={`text-sm text-bunker-300 ${labelClassName || ""}`}>{label}</label>
|
||||
<div className="mt-1">{children}</div>
|
||||
<div className="mb-4">
|
||||
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
|
||||
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-medium text-mineshaft-100">Details</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -72,31 +72,27 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
|
||||
)}
|
||||
</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"}
|
||||
<div className="pt-2">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description || "None"}</GenericFieldLabel>
|
||||
{subscriber && (
|
||||
<GenericFieldLabel label="Source Subscriber">{subscriber.name}</GenericFieldLabel>
|
||||
)}
|
||||
{syncStatus && (
|
||||
<GenericFieldLabel label="Status">
|
||||
<PkiSyncStatusBadge status={syncStatus} />
|
||||
</GenericFieldLabel>
|
||||
{syncStatus && (
|
||||
<GenericFieldLabel label="Status">
|
||||
<PkiSyncStatusBadge status={syncStatus} />
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{lastSyncedAt && (
|
||||
<GenericFieldLabel label="Last Synced">
|
||||
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
|
||||
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
|
||||
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
|
||||
{failureMessage}
|
||||
</p>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{lastSyncedAt && (
|
||||
<GenericFieldLabel label="Last Synced">
|
||||
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
|
||||
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
|
||||
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">{failureMessage}</p>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+25
-12
@@ -3,12 +3,27 @@ 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 { IconButton } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
const GenericFieldLabel = ({
|
||||
label,
|
||||
children,
|
||||
labelClassName
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
}) => (
|
||||
<div className="mb-4">
|
||||
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
onEditOptions: VoidFunction;
|
||||
@@ -27,7 +42,7 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
|
||||
<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-medium text-mineshaft-100">Sync Options</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Sync Options</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -42,21 +57,19 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
{/* Hidden for now - Import certificates functionality disabled
|
||||
<div className="pt-1">
|
||||
{/* Hidden for now - Import certificates functionality disabled
|
||||
<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>
|
||||
<GenericFieldLabel label="Inactive Certificate Removal" labelClassName="mb-1">
|
||||
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
|
||||
{canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+8
-11
@@ -1,10 +1,12 @@
|
||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||
import { subject } from "@casl/ability";
|
||||
import { faEdit, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Badge, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
@@ -36,15 +38,10 @@ export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
|
||||
<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>
|
||||
<Badge variant="danger" className="mr-1">
|
||||
<AlertTriangleIcon />
|
||||
Source Deleted
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { PkiSyncActionTriggers } from "./PkiSyncActionTriggers";
|
||||
export { PkiSyncAuditLogsSection } from "./PkiSyncAuditLogsSection";
|
||||
export { PkiSyncCertificatesSection } from "./PkiSyncCertificatesSection";
|
||||
export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection";
|
||||
export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection";
|
||||
export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection";
|
||||
|
||||
@@ -114,30 +114,29 @@ export const PkiTemplateListPage = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="container mx-auto mb-6 max-w-8xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
{subscription?.pkiLegacyTemplates && (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
|
||||
<div className="flex w-full justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Create}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{/* TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation */}
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
|
||||
<div className="flex w-full justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Create}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
@@ -203,7 +202,9 @@ export const PkiTemplateListPage = () => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!subscription.pkiEst) {
|
||||
handlePopUpOpen("estUpgradePlan");
|
||||
handlePopUpOpen("estUpgradePlan", {
|
||||
isEnterpriseFeature: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("enrollmentOptions", {
|
||||
@@ -295,7 +296,8 @@ export const PkiTemplateListPage = () => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.estUpgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("estUpgradePlan", isOpen)}
|
||||
text="You can only configure template enrollment methods if you switch to Infisical's Enterprise plan."
|
||||
text="Your current plan does not include access to configuring template enrollment methods. To unlock this feature, please upgrade to Infisical Enterprise plan."
|
||||
isEnterpriseFeature={popUp.estUpgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -140,10 +140,14 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
|
||||
ttl,
|
||||
keyUsages: Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertKeyUsage),
|
||||
.map(([key]) =>
|
||||
key === CertKeyUsage.CRL_SIGN
|
||||
? "cRLSign"
|
||||
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
),
|
||||
extendedKeyUsages: Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage)
|
||||
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -160,10 +164,14 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
|
||||
ttl,
|
||||
keyUsages: Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertKeyUsage),
|
||||
.map(([key]) =>
|
||||
key === CertKeyUsage.CRL_SIGN
|
||||
? "cRLSign"
|
||||
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
),
|
||||
extendedKeyUsages: Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage)
|
||||
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
|
||||
});
|
||||
|
||||
createNotification({
|
||||
|
||||
@@ -2,17 +2,20 @@ import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { useProject } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
|
||||
import { CertificatesTab } from "./components/CertificatesTab";
|
||||
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
|
||||
import { PkiCollectionsTab } from "./components/PkiCollectionsTab";
|
||||
|
||||
enum TabSections {
|
||||
CertificateProfiles = "profiles",
|
||||
CertificateTemplatesV2 = "templates-v2"
|
||||
CertificateTemplatesV2 = "templates-v2",
|
||||
Certificates = "certificates",
|
||||
PkiCollections = "pki-collections"
|
||||
}
|
||||
|
||||
export const PoliciesPage = () => {
|
||||
@@ -25,59 +28,54 @@ export const PoliciesPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
if (!isAllowed) {
|
||||
return (
|
||||
<div className="mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl text-center">
|
||||
<p>You don't have permission to access certificate policies.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificate Management" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Management"
|
||||
description="Streamline certificate management by creating and maintaining templates, profiles, and certificates in one place"
|
||||
/>
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificate Policies" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Policies"
|
||||
description="Manage certificate templates and profiles for unified certificate issuance"
|
||||
/>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as TabSections)}
|
||||
>
|
||||
<TabList>
|
||||
<Tab variant="project" value={TabSections.CertificateProfiles}>
|
||||
Certificate Profiles
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.CertificateTemplatesV2}>
|
||||
Certificate Templates
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.Certificates}>
|
||||
Certificates
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.PkiCollections}>
|
||||
Certificate Collections
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as TabSections)}
|
||||
>
|
||||
<TabList>
|
||||
<Tab variant="project" value={TabSections.CertificateProfiles}>
|
||||
Certificate Profiles
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.CertificateTemplatesV2}>
|
||||
Certificate Templates
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.CertificateProfiles}>
|
||||
<CertificateProfilesTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.CertificateProfiles}>
|
||||
<CertificateProfilesTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
||||
<CertificateTemplatesV2Tab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
||||
<CertificateTemplatesV2Tab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
<TabPanel value={TabSections.Certificates}>
|
||||
<CertificatesTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.PkiCollections}>
|
||||
<PkiCollectionsTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+26
-14
@@ -1,5 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -13,7 +15,8 @@ import {
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||
@@ -67,7 +70,7 @@ const createSchema = z
|
||||
apiConfig: z
|
||||
.object({
|
||||
autoRenew: z.boolean().optional(),
|
||||
autoRenewDays: z.number().min(1).max(365).optional()
|
||||
renewBeforeDays: z.number().min(1).max(365).optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -115,7 +118,7 @@ const editSchema = z
|
||||
apiConfig: z
|
||||
.object({
|
||||
autoRenew: z.boolean().optional(),
|
||||
autoRenewDays: z.number().min(1).max(365).optional()
|
||||
renewBeforeDays: z.number().min(1).max(365).optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -183,7 +186,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
profile.enrollmentType === "api"
|
||||
? {
|
||||
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
|
||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
@@ -195,7 +198,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
certificateTemplateId: "",
|
||||
apiConfig: {
|
||||
autoRenew: false,
|
||||
autoRenewDays: 30
|
||||
renewBeforeDays: 30
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -225,7 +228,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
profile.enrollmentType === "api"
|
||||
? {
|
||||
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
|
||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
@@ -389,7 +392,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
} else {
|
||||
setValue("apiConfig", {
|
||||
autoRenew: false,
|
||||
autoRenewDays: 30
|
||||
renewBeforeDays: 30
|
||||
});
|
||||
setValue("estConfig", undefined);
|
||||
}
|
||||
@@ -415,7 +418,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
name="enrollmentType"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Enrollment Type"
|
||||
label="Enrollment Method"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
@@ -433,7 +436,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
setValue("estConfig", undefined);
|
||||
setValue("apiConfig", {
|
||||
autoRenew: false,
|
||||
autoRenewDays: 30
|
||||
renewBeforeDays: 30
|
||||
});
|
||||
}
|
||||
onChange(value);
|
||||
@@ -535,9 +538,18 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
name="apiConfig.autoRenew"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Checkbox id="autoRenew" isChecked={value} onCheckedChange={onChange}>
|
||||
Enable Auto-Renewal
|
||||
</Checkbox>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="autoRenew" isChecked={value} onCheckedChange={onChange}>
|
||||
Enable Auto-Renewal By Default
|
||||
</Checkbox>
|
||||
<Tooltip content="If enabled, certificates issued against this profile will auto-renew at specified days before expiration.">
|
||||
<FontAwesomeIcon
|
||||
icon={faQuestionCircle}
|
||||
className="cursor-help text-mineshaft-400 hover:text-mineshaft-300"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -548,10 +560,10 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
<div className="mb-4 space-y-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="apiConfig.autoRenewDays"
|
||||
name="apiConfig.renewBeforeDays"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Auto-Renewal Days"
|
||||
label="Auto-Renewal Days Before Expiration"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
|
||||
+3
-6
@@ -29,8 +29,7 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
projectId: currentProject?.id || "",
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
includeConfigs: true,
|
||||
includeMetrics: true
|
||||
includeConfigs: true
|
||||
});
|
||||
|
||||
const profiles = data?.certificateProfiles || [];
|
||||
@@ -42,10 +41,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Enrollment Type</Th>
|
||||
<Th>Enrollment Method</Th>
|
||||
<Th>Issuing CA</Th>
|
||||
<Th>Certificate Template</Th>
|
||||
<Th>Certificates</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
@@ -67,10 +65,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Enrollment Type</Th>
|
||||
<Th>Enrollment Method</Th>
|
||||
<Th>Issuing CA</Th>
|
||||
<Th>Certificate Template</Th>
|
||||
<Th>Certificates</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
|
||||
+3
-61
@@ -12,7 +12,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useProjectPermission } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
@@ -33,59 +33,6 @@ import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
|
||||
|
||||
const MetricsBadges = ({
|
||||
metrics
|
||||
}: {
|
||||
metrics?: {
|
||||
totalCertificates: number;
|
||||
activeCertificates: number;
|
||||
expiringCertificates: number;
|
||||
expiredCertificates: number;
|
||||
revokedCertificates: number;
|
||||
};
|
||||
}) => {
|
||||
if (!metrics) {
|
||||
return (
|
||||
<Badge variant="primary" className="text-xs">
|
||||
No metrics
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (metrics.totalCertificates === 0) {
|
||||
return (
|
||||
<Badge variant="primary" className="text-xs">
|
||||
No certificates
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{metrics.activeCertificates > 0 && (
|
||||
<Badge variant="success" className="text-xs">
|
||||
{metrics.activeCertificates} active
|
||||
</Badge>
|
||||
)}
|
||||
{metrics.expiringCertificates > 0 && (
|
||||
<Badge variant="primary" className="text-xs">
|
||||
{metrics.expiringCertificates} expiring
|
||||
</Badge>
|
||||
)}
|
||||
{metrics.expiredCertificates > 0 && (
|
||||
<Badge variant="danger" className="text-xs">
|
||||
{metrics.expiredCertificates} expired
|
||||
</Badge>
|
||||
)}
|
||||
{metrics.revokedCertificates > 0 && (
|
||||
<Badge variant="danger" className="text-xs">
|
||||
{metrics.revokedCertificates} revoked
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
profile: TCertificateProfile;
|
||||
onEditProfile: (profile: TCertificateProfile) => void;
|
||||
@@ -134,8 +81,8 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
|
||||
const getEnrollmentTypeBadge = (enrollmentType: string) => {
|
||||
const config = {
|
||||
api: { variant: "success" as const, label: "API" },
|
||||
est: { variant: "primary" as const, label: "EST" }
|
||||
api: { variant: "ghost" as const, label: "API" },
|
||||
est: { variant: "ghost" as const, label: "EST" }
|
||||
} as const;
|
||||
|
||||
const configKey = Object.keys(config).includes(enrollmentType)
|
||||
@@ -169,11 +116,6 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
{templateData?.name || profile.certificateTemplateId}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<MetricsBadges metrics={profile.metrics} />
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { CertificatesSection } from "../../../CertificatesPage/components/CertificatesSection";
|
||||
|
||||
export const CertificatesTab = () => {
|
||||
return <CertificatesSection />;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CertificatesTab } from "./CertificatesTab";
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { PkiCollectionSection } from "../../../AlertingPage/components/PkiCollectionSection";
|
||||
|
||||
export const PkiCollectionsTab = () => {
|
||||
return <PkiCollectionSection />;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { PkiCollectionsTab } from "./PkiCollectionsTab";
|
||||
@@ -1,2 +1,4 @@
|
||||
export { CertificateProfilesTab } from "./CertificateProfilesTab";
|
||||
export { CertificatesTab } from "./CertificatesTab";
|
||||
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
|
||||
export { PkiCollectionsTab } from "./PkiCollectionsTab";
|
||||
|
||||
@@ -170,7 +170,9 @@ export const KmipClientTable = () => {
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (subscription && !subscription.kmip) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
isEnterpriseFeature: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,7 +344,8 @@ export const KmipClientTable = () => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="KMIP requires an enterprise plan."
|
||||
text="Your current plan does not include access to KMIP. To unlock this feature, please upgrade to Infisical Enterprise plan."
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -23,7 +23,6 @@ import { motion } from "framer-motion";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -44,7 +43,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import { Badge, TBadgeProps } from "@app/components/v3";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCmekActions,
|
||||
@@ -72,7 +71,7 @@ import { DeleteCmekModal } from "./DeleteCmekModal";
|
||||
|
||||
const getStatusBadgeProps = (
|
||||
isDisabled: boolean
|
||||
): { variant: BadgeProps["variant"]; label: string } => {
|
||||
): { variant: TBadgeProps["variant"]; label: string } => {
|
||||
if (isDisabled) {
|
||||
return {
|
||||
variant: "danger",
|
||||
|
||||
@@ -7,7 +7,6 @@ import { z } from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { decodeBase64 } from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
FormControl,
|
||||
Modal,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { SigningAlgorithm, TCmek, useCmekVerify } from "@app/hooks/api/cmeks";
|
||||
import { isBase64 } from "@app/lib/fn/base64";
|
||||
|
||||
@@ -112,30 +112,22 @@ const VerifyForm = ({ cmek }: FormProps) => {
|
||||
<div className="mb-6 flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<span className="text-sm opacity-60">Signature Status:</span>
|
||||
<Badge variant={signatureValid ? "success" : "danger"}>
|
||||
<Tooltip
|
||||
content={
|
||||
signatureValid
|
||||
? "The signature is valid. signature was created using the same signing algorithm and key as the one used to sign the data."
|
||||
: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with."
|
||||
}
|
||||
>
|
||||
{signatureValid ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<p>Valid</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<p>Invalid</p>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
<Tooltip
|
||||
content={
|
||||
signatureValid
|
||||
? "The signature is valid. signature was created using the same signing algorithm and key as the one used to sign the data."
|
||||
: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with."
|
||||
}
|
||||
>
|
||||
<Badge variant={signatureValid ? "success" : "danger"}>
|
||||
{signatureValid ? "Valid" : "Invalid"}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm opacity-60">Signing Algorithm:</span>
|
||||
<Badge variant="primary">{signingAlgorithm}</Badge>
|
||||
<Badge variant="info">{signingAlgorithm}</Badge>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<span className="text-sm opacity-60">Signature:</span>{" "}
|
||||
|
||||
+9
-4
@@ -5,6 +5,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { useDeleteGroup } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
@@ -26,8 +27,8 @@ export const OrgGroupsSection = () => {
|
||||
const handleAddGroupModal = () => {
|
||||
if (!subscription?.groups) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"You can manage users more efficiently with groups if you upgrade your Infisical plan to an Enterprise license."
|
||||
text: "Your current plan does not allow adding groups. To unlock this feature, please upgrade to Infisical Enterprise plan.",
|
||||
isEnterpriseFeature: true
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("group");
|
||||
@@ -57,7 +58,10 @@ export const OrgGroupsSection = () => {
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Groups</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Groups</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
@@ -92,7 +96,8 @@ export const OrgGroupsSection = () => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
text={popUp.upgradePlan?.data?.text}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+13
-4
@@ -55,7 +55,10 @@ const schema = z
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -277,7 +280,9 @@ export const IdentityAliCloudAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -292,7 +297,9 @@ export const IdentityAliCloudAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -315,7 +322,9 @@ export const IdentityAliCloudAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+5
-3
@@ -4,7 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { Badge, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import { FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -277,7 +278,7 @@ export const IdentityAuthMethodModalContent = ({
|
||||
>
|
||||
{label}{" "}
|
||||
{alreadyConfigured && !isSelectedAuthAlreadyConfigured && (
|
||||
<Badge>Configured</Badge>
|
||||
<Badge variant="info">Configured</Badge>
|
||||
)}
|
||||
</SelectItem>
|
||||
</Tooltip>
|
||||
@@ -291,7 +292,8 @@ export const IdentityAuthMethodModalContent = ({
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp?.upgradePlan?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
||||
text={`Your current plan does not include access to ${popUp.upgradePlan.data?.featureName}. To unlock this feature, please upgrade to Infisical ${popUp.upgradePlan.data?.isEnterpriseFeature ? "Enterprise" : "Pro"} plan.`}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+13
-4
@@ -57,7 +57,10 @@ const schema = z
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -315,7 +318,9 @@ export const IdentityAwsAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -330,7 +335,9 @@ export const IdentityAwsAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -353,7 +360,9 @@ export const IdentityAwsAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+13
-4
@@ -52,7 +52,10 @@ const schema = z
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -311,7 +314,9 @@ export const IdentityAzureAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -326,7 +331,9 @@ export const IdentityAzureAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -349,7 +356,9 @@ export const IdentityAzureAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+13
-4
@@ -55,7 +55,10 @@ const schema = z
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -350,7 +353,9 @@ export const IdentityGcpAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -365,7 +370,9 @@ export const IdentityGcpAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -388,7 +395,9 @@ export const IdentityGcpAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+13
-4
@@ -88,7 +88,10 @@ const schema = z.discriminatedUnion("configurationType", [
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -619,7 +622,9 @@ export const IdentityJwtAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -634,7 +639,9 @@ export const IdentityJwtAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -657,7 +664,9 @@ export const IdentityJwtAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+38
-17
@@ -22,11 +22,12 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { useOrganization, useOrgPermission, useSubscription } from "@app/context";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||
import {
|
||||
gatewaysQueryKeys,
|
||||
useAddIdentityKubernetesAuth,
|
||||
@@ -95,7 +96,10 @@ const schema = z
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -129,6 +133,8 @@ export const IdentityKubernetesAuthForm = ({
|
||||
] as const);
|
||||
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
||||
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
||||
const { hasOrgRole } = useOrgPermission();
|
||||
const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -409,20 +415,29 @@ export const IdentityKubernetesAuthForm = ({
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mt-0.5 text-primary" />
|
||||
<span className="text-mineshaft-200">Load values from HashiCorp Vault</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
!isOrgAdmin
|
||||
? "Only organization admins can import configurations from HashiCorp Vault"
|
||||
: undefined
|
||||
}
|
||||
onClick={() => handleImportPopUpToggle("importFromVault", true)}
|
||||
>
|
||||
Load from Vault
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleImportPopUpToggle("importFromVault", true)}
|
||||
isDisabled={!isOrgAdmin}
|
||||
>
|
||||
Load from Vault
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full items-center gap-2">
|
||||
@@ -695,7 +710,9 @@ export const IdentityKubernetesAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -710,7 +727,9 @@ export const IdentityKubernetesAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -733,7 +752,9 @@ export const IdentityKubernetesAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+17
-5
@@ -147,7 +147,10 @@ const schema = z
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { isEnterpriseFeature?: boolean; featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -304,7 +307,10 @@ export const IdentityLdapAuthForm = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!subscription?.ldap) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
isEnterpriseFeature: true,
|
||||
featureName: "LDAP authentication"
|
||||
});
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
}
|
||||
}, [subscription, handlePopUpOpen, handlePopUpToggle]);
|
||||
@@ -831,7 +837,9 @@ export const IdentityLdapAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -846,7 +854,9 @@ export const IdentityLdapAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -869,7 +879,9 @@ export const IdentityLdapAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+13
-4
@@ -63,7 +63,10 @@ const schema = z
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -297,7 +300,9 @@ export const IdentityOciAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -312,7 +317,9 @@ export const IdentityOciAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -335,7 +342,9 @@ export const IdentityOciAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+13
-4
@@ -75,7 +75,10 @@ const schema = z.object({
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -606,7 +609,9 @@ export const IdentityOidcAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -621,7 +626,9 @@ export const IdentityOidcAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -644,7 +651,9 @@ export const IdentityOidcAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+13
-40
@@ -1,15 +1,11 @@
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faLink,
|
||||
faPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faLink, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -113,22 +109,9 @@ export const IdentitySection = withPermission(
|
||||
<div>
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex w-full items-center gap-4">
|
||||
<div className="flex flex-1 items-center gap-1">
|
||||
<div className="flex flex-1 items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.16rem] ml-1 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>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
||||
</div>
|
||||
{isSubOrganization && (
|
||||
<OrgPermissionCan
|
||||
@@ -162,8 +145,7 @@ export const IdentitySection = withPermission(
|
||||
onClick={() => {
|
||||
if (!isMoreIdentitiesAllowed && !isEnterprise) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"You can add more identities if you upgrade your Infisical plan."
|
||||
text: "You have reached the maximum number of identities allowed on your current plan. Upgrade to Infisical Pro plan to add more identities."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -181,22 +163,9 @@ export const IdentitySection = withPermission(
|
||||
{/* Identity Auth Templates Section */}
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identity Auth Templates</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/auth-templates"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.16rem] ml-1 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>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/auth-templates" />
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates}
|
||||
@@ -209,7 +178,10 @@ export const IdentitySection = withPermission(
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (subscription && !subscription.machineIdentityAuthTemplates) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
isEnterpriseFeature: true,
|
||||
text: "Your current plan does not include access to creating Identity Auth Templates. To unlock this feature, please upgrade to Infisical Enterprise plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("createTemplate");
|
||||
@@ -279,7 +251,8 @@ export const IdentitySection = withPermission(
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use Identity Auth Templates if you switch to Infisical's Enterprise plan."
|
||||
text={popUp.upgradePlan.data?.text}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+13
-4
@@ -50,7 +50,10 @@ const schema = z.object({
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -286,7 +289,9 @@ export const IdentityTlsCertAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -301,7 +306,9 @@ export const IdentityTlsCertAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -324,7 +331,9 @@ export const IdentityTlsCertAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+13
-4
@@ -49,7 +49,10 @@ const schema = z
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -248,7 +251,9 @@ export const IdentityTokenAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -263,7 +268,9 @@ export const IdentityTokenAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -286,7 +293,9 @@ export const IdentityTokenAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+22
-7
@@ -87,7 +87,10 @@ const schema = z
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
||||
data?: { featureName?: string }
|
||||
) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
@@ -409,7 +412,9 @@ export const IdentityUniversalAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -424,7 +429,9 @@ export const IdentityUniversalAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -447,7 +454,9 @@ export const IdentityUniversalAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
@@ -477,7 +486,9 @@ export const IdentityUniversalAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
@@ -492,7 +503,9 @@ export const IdentityUniversalAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
@@ -515,7 +528,9 @@ export const IdentityUniversalAuthForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
featureName: "IP allowlisting"
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
|
||||
+13
-14
@@ -1,13 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { faBan, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { BanIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmailServiceSetupModal,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ModalContent,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge, DocumentationLinkBadge } from "@app/components/v3";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -75,7 +76,7 @@ export const OrgMembersSection = () => {
|
||||
|
||||
if (!isMoreIdentitiesAllowed && !isEnterprise) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "You can add more members if you upgrade your Infisical plan."
|
||||
text: "You have reached the maximum number of members allowed on your current plan. Upgrade to Infisical Pro plan to add more members."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -207,7 +208,10 @@ export const OrgMembersSection = () => {
|
||||
</div>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Users</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Users</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
@@ -303,15 +307,10 @@ export const OrgMembersSection = () => {
|
||||
</p>
|
||||
{userId === member.user.id && (
|
||||
<Tooltip content="You cannot remove yourself from this organization">
|
||||
<div className="inline-block">
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
<span>Ignored</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="danger" className="ml-2">
|
||||
<BanIcon />
|
||||
Ignored
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
@@ -324,7 +323,7 @@ export const OrgMembersSection = () => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
text={popUp.upgradePlan?.data?.text}
|
||||
/>
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
|
||||
+11
-11
@@ -11,19 +11,18 @@ import {
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faUsers,
|
||||
faUserShield,
|
||||
faUserSlash,
|
||||
faUserXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { UserCogIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { LastLoginSection } from "@app/components/organization/LastLoginSection";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
@@ -50,6 +49,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -82,7 +82,7 @@ type Props = {
|
||||
data?: {
|
||||
orgMembershipId?: string;
|
||||
username?: string;
|
||||
description?: string;
|
||||
text?: string;
|
||||
selectedOrgMemberships?: OrgUser[];
|
||||
}
|
||||
) => void;
|
||||
@@ -133,7 +133,7 @@ export const OrgMembersTable = ({
|
||||
|
||||
if (isCustomRole && subscription && !subscription?.rbac) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "You can assign custom roles to members if you upgrade your Infisical plan."
|
||||
text: "Your current plan does not include access to assigning custom roles to members. To unlock this feature, please upgrade to Infisical Pro plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -519,17 +519,17 @@ export const OrgMembersTable = ({
|
||||
<Td
|
||||
className={twMerge("group max-w-0", isActive ? "" : "text-mineshaft-400")}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="truncate">
|
||||
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
||||
</p>
|
||||
{u.superAdmin && (
|
||||
<Badge variant="primary" className="ml-2 w-min whitespace-nowrap">
|
||||
<span className="mr-1 hidden xl:inline">Server Admin</span>
|
||||
<Tooltip content="Server Admin">
|
||||
<FontAwesomeIcon className="xl:hidden" icon={faUserShield} />
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
<Tooltip content="Server Admin">
|
||||
<Badge variant="info">
|
||||
<UserCogIcon />
|
||||
<span className="hidden xl:inline">Server Admin</span>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
{lastLoginAuthMethod && lastLoginTime && (
|
||||
<Tooltip
|
||||
|
||||
+25
-16
@@ -14,13 +14,13 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { ServerIcon, WrenchIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
@@ -41,13 +41,14 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge, DocumentationLinkBadge } from "@app/components/v3";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { isCustomOrgRole, isCustomProjectRole } from "@app/helpers/roles";
|
||||
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
@@ -103,8 +104,7 @@ export const OrgRoleTable = () => {
|
||||
|
||||
if (isCustomRole && subscription && !subscription?.rbac) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"You can set the default org role to a custom role if you upgrade your Infisical plan."
|
||||
text: "Your current plan does not include access to set a custom default organization role. To unlock this feature, please upgrade to Infisical Pro plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -200,9 +200,12 @@ export const OrgRoleTable = () => {
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">
|
||||
{isSubOrganization ? "Sub-" : ""}Organization Roles
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">
|
||||
{isSubOrganization ? "Sub-" : ""}Organization Roles
|
||||
</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/organization#roles-and-access-control" />
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
@@ -295,17 +298,13 @@ export const OrgRoleTable = () => {
|
||||
}
|
||||
>
|
||||
<Td className="max-w-md">
|
||||
<div className="flex">
|
||||
<div className="flex gap-x-1.5">
|
||||
<p className="overflow-hidden text-ellipsis whitespace-nowrap">{name}</p>
|
||||
{isDefaultOrgRole && (
|
||||
<Tooltip
|
||||
content={`Members joining your organization will be assigned the ${name} role unless otherwise specified.`}
|
||||
>
|
||||
<div>
|
||||
<Badge variant="success" className="ml-1">
|
||||
Default
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="info">Default</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
@@ -314,8 +313,18 @@ export const OrgRoleTable = () => {
|
||||
{slug}
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge className="w-min bg-mineshaft-400/50 whitespace-nowrap text-bunker-200">
|
||||
{isCustomProjectRole(slug) ? "Custom" : "Default"}
|
||||
<Badge variant="ghost">
|
||||
{isCustomOrgRole(slug) ? (
|
||||
<>
|
||||
<WrenchIcon />
|
||||
Custom
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ServerIcon />
|
||||
Platform
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
@@ -461,7 +470,7 @@ export const OrgRoleTable = () => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
text={popUp.upgradePlan?.data?.text}
|
||||
/>
|
||||
<DuplicateOrgRoleModal
|
||||
isOpen={popUp.duplicateRole.isOpen}
|
||||
|
||||
+16
-2
@@ -20,6 +20,7 @@ import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
||||
import { BitbucketConnectionForm } from "./BitbucketConnectionForm";
|
||||
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
||||
import { ChecklyConnectionForm } from "./ChecklyConnectionForm";
|
||||
import { ChefConnectionForm } from "./ChefConnectionForm";
|
||||
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
|
||||
@@ -36,6 +37,7 @@ import { LdapConnectionForm } from "./LdapConnectionForm";
|
||||
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
|
||||
import { MySqlConnectionForm } from "./MySqlConnectionForm";
|
||||
import { NetlifyConnectionForm } from "./NetlifyConnectionForm";
|
||||
import { NorthflankConnectionForm } from "./NorthflankConnectionForm";
|
||||
import { OCIConnectionForm } from "./OCIConnectionForm";
|
||||
import { OktaConnectionForm } from "./OktaConnectionForm";
|
||||
import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
|
||||
@@ -96,7 +98,7 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
|
||||
case AppConnection.AWS:
|
||||
return <AwsConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.GitHub:
|
||||
return <GitHubConnectionForm projectId={projectId} />;
|
||||
return <GitHubConnectionForm projectId={projectId} onSubmit={onSubmit} />;
|
||||
case AppConnection.GitHubRadar:
|
||||
return <GitHubRadarConnectionForm projectId={projectId} />;
|
||||
case AppConnection.GCP:
|
||||
@@ -163,12 +165,16 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
|
||||
return <RailwayConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Checkly:
|
||||
return <ChecklyConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Chef:
|
||||
return <ChefConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Supabase:
|
||||
return <SupabaseConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.DigitalOcean:
|
||||
return <DigitalOceanConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Netlify:
|
||||
return <NetlifyConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Northflank:
|
||||
return <NorthflankConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Okta:
|
||||
return <OktaConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Redis:
|
||||
@@ -213,7 +219,11 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
case AppConnection.GitHub:
|
||||
return (
|
||||
<GitHubConnectionForm appConnection={appConnection} projectId={appConnection.projectId} />
|
||||
<GitHubConnectionForm
|
||||
appConnection={appConnection}
|
||||
projectId={appConnection.projectId}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
case AppConnection.GitHubRadar:
|
||||
return (
|
||||
@@ -322,10 +332,14 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Checkly:
|
||||
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Chef:
|
||||
return <ChefConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Supabase:
|
||||
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.DigitalOcean:
|
||||
return <DigitalOceanConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Northflank:
|
||||
return <NorthflankConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Okta:
|
||||
return <OktaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Redis:
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { ChefConnectionMethod, TChefConnection } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TChefConnection;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.Chef)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(ChefConnectionMethod.UserKey),
|
||||
credentials: z.object({
|
||||
serverUrl: z.string().trim().url("Valid Chef Server URL required").optional(),
|
||||
orgName: z.string().trim().min(1, "Organization name required"),
|
||||
userName: z.string().trim().min(1, "User name required"),
|
||||
privateKey: z.string().trim().min(1, "Private key required")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ChefConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.Chef,
|
||||
method: ChefConnectionMethod.UserKey
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="credentials.serverUrl"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Chef Server URL (optional)"
|
||||
tooltipText="Will default to Chef Cloud if not specified."
|
||||
>
|
||||
<Input
|
||||
placeholder="https://api.chef.io"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Chef].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(ChefConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="credentials.orgName"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Organization Short Name"
|
||||
>
|
||||
<Input
|
||||
className="border border-mineshaft-500 bg-mineshaft-900"
|
||||
placeholder="your-org"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.userName"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="User Name"
|
||||
>
|
||||
<Input
|
||||
className="border border-mineshaft-500 bg-mineshaft-900"
|
||||
placeholder="your-username"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.privateKey"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Private Key"
|
||||
tooltipText="Your Chef user's private key (.pem file)"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to Chef"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+85
-18
@@ -16,6 +16,7 @@ import {
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
@@ -48,28 +49,58 @@ import {
|
||||
type Props = {
|
||||
appConnection?: TGitHubConnection;
|
||||
projectId: string | undefined | null;
|
||||
onSubmit: (formData: PatSchemaForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.GitHub),
|
||||
method: z.nativeEnum(GitHubConnectionMethod),
|
||||
credentials: z
|
||||
.union([
|
||||
z.object({
|
||||
instanceType: z.literal("cloud").optional(),
|
||||
host: z.string().optional()
|
||||
}),
|
||||
z.object({
|
||||
instanceType: z.literal("server"),
|
||||
host: z.string().min(1, "Required")
|
||||
})
|
||||
])
|
||||
.optional()
|
||||
method: z.nativeEnum(GitHubConnectionMethod)
|
||||
});
|
||||
|
||||
const baseCredentialsSchema = z.union([
|
||||
z.object({
|
||||
instanceType: z.literal("server"),
|
||||
host: z.string().min(1, "Host is required for server instance type")
|
||||
}),
|
||||
z.object({
|
||||
instanceType: z.literal("cloud").optional(),
|
||||
host: z.string().optional()
|
||||
})
|
||||
]);
|
||||
|
||||
const appSchema = rootSchema.extend({
|
||||
method: z.literal(GitHubConnectionMethod.App),
|
||||
credentials: baseCredentialsSchema
|
||||
});
|
||||
|
||||
const oauthSchema = rootSchema.extend({
|
||||
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||
credentials: baseCredentialsSchema
|
||||
});
|
||||
|
||||
const patSchema = rootSchema.extend({
|
||||
method: z.literal(GitHubConnectionMethod.Pat),
|
||||
credentials: z.union([
|
||||
z.object({
|
||||
instanceType: z.literal("server"),
|
||||
host: z.string().min(1, "Host is required for server instance type"),
|
||||
personalAccessToken: z.string().min(1, "Personal Access Token is required")
|
||||
}),
|
||||
z.object({
|
||||
instanceType: z.literal("cloud").optional(),
|
||||
host: z.string().optional(),
|
||||
personalAccessToken: z.string().min(1, "Personal Access Token is required")
|
||||
})
|
||||
])
|
||||
});
|
||||
|
||||
type PatSchemaForm = z.infer<typeof patSchema>;
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [appSchema, oauthSchema, patSchema]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
|
||||
export const GitHubConnectionForm = ({ appConnection, projectId, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
@@ -106,7 +137,12 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
|
||||
|
||||
const returnUrl = useGetAppConnectionOauthReturnUrl();
|
||||
|
||||
const onSubmit = (formData: FormData) => {
|
||||
const submitHandler = async (formData: FormData) => {
|
||||
if (formData.method === GitHubConnectionMethod.Pat) {
|
||||
await onSubmit(formData);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRedirecting(true);
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
@@ -151,15 +187,26 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
|
||||
case GitHubConnectionMethod.App:
|
||||
isMissingConfig = !appClientSlug;
|
||||
break;
|
||||
case GitHubConnectionMethod.Pat:
|
||||
isMissingConfig = false;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled GitHub Connection method: ${selectedMethod}`);
|
||||
}
|
||||
|
||||
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||
|
||||
const getButtonText = () => {
|
||||
if (selectedMethod === GitHubConnectionMethod.Pat) {
|
||||
return isUpdate ? "Update Connection" : "Create Connection";
|
||||
}
|
||||
|
||||
return isUpdate ? "Reconnect to GitHub" : "Connect to GitHub";
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
@@ -201,6 +248,26 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedMethod === GitHubConnectionMethod.Pat && (
|
||||
<Controller
|
||||
name="credentials.personalAccessToken"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Personal Access Token"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="enterprise-options" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
@@ -310,7 +377,7 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
|
||||
isLoading={isSubmitting || isRedirecting}
|
||||
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
|
||||
>
|
||||
{isUpdate ? "Reconnect to GitHub" : "Connect to GitHub"}
|
||||
{getButtonText()}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
NorthflankConnectionMethod,
|
||||
TNorthflankConnection
|
||||
} from "@app/hooks/api/appConnections/types/northflank-connection";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TNorthflankConnection;
|
||||
onSubmit: (formData: FormData) => void;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.Northflank)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(NorthflankConnectionMethod.ApiToken),
|
||||
credentials: z.object({
|
||||
apiToken: z.string().trim().min(1, "API Token required")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const NorthflankConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.Northflank,
|
||||
method: NorthflankConnectionMethod.ApiToken
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Northflank].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(NorthflankConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.apiToken"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="API Token"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 group-focus-within:border-primary-400/50! border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to Northflank"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+4
-16
@@ -1,6 +1,6 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
@@ -29,23 +29,11 @@ export const AppConnectionHeader = ({ app, isConnected, onBack }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
<div className="flex items-center gap-x-2 text-mineshaft-300">
|
||||
{appDetails.name}
|
||||
<a
|
||||
<DocumentationLinkBadge
|
||||
href={`https://infisical.com/docs/integrations/app-connections/${app}`}
|
||||
target="_blank"
|
||||
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="mr-1 mb-[0.03rem] 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">
|
||||
{isConnected ? `${appDetails.name} Connection` : `Connect to ${appDetails.name}`}
|
||||
|
||||
+6
-2
@@ -72,10 +72,13 @@ export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.app}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseAppConnections
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
? handlePopUpOpen("upgradePlan", {
|
||||
isEnterpriseFeature: true
|
||||
})
|
||||
: onSelect(option.app)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
@@ -166,7 +169,8 @@ export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use every App Connection if you switch to Infisical's Enterprise plan."
|
||||
text="All App Connections can be unlocked if you switch to Infisical Enterprise plan."
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+5
-5
@@ -8,18 +8,17 @@ import {
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faInfoCircle,
|
||||
faServer,
|
||||
faTable,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { ServerIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { VariablePermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
|
||||
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
@@ -157,9 +157,9 @@ export const AppConnectionRow = ({
|
||||
{isPlatformManagedCredentials && (
|
||||
<Tooltip side="left" content="This connection's credentials are managed by Infisical.">
|
||||
<div>
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap">
|
||||
<FontAwesomeIcon icon={faServer} />
|
||||
<span>Platform Managed Credentials</span>
|
||||
<Badge variant="info">
|
||||
<ServerIcon />
|
||||
Platform Managed Credentials
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
+3
-17
@@ -2,8 +2,6 @@ import { useMemo, useState } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCheckCircle,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
@@ -34,6 +32,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
|
||||
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
@@ -212,22 +211,9 @@ export const AppConnectionsTable = ({ projectId, projectType }: Props) => {
|
||||
<div className="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-center gap-1">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">App Connections</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/app-connections/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.12rem] ml-1 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>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/integrations/app-connections/overview" />
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Create and configure connections with third-party apps for re-use across your project
|
||||
|
||||
+3
-1
@@ -633,7 +633,9 @@ export const OAuthCallbackPage = () => {
|
||||
connectionName: data.connection.name,
|
||||
...(data.returnUrl.includes("integrations")
|
||||
? {
|
||||
selectedTab: IntegrationsListPageTabs.SecretSyncs
|
||||
selectedTab: localStorage.getItem("pkiSyncFormData")
|
||||
? IntegrationsListPageTabs.PkiSyncs
|
||||
: IntegrationsListPageTabs.SecretSyncs
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetUserProjects } from "@app/hooks/api";
|
||||
import {
|
||||
@@ -124,7 +124,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
<Button variant="outline_bg" colorSchema="primary" className="relative">
|
||||
<FontAwesomeIcon icon={faFilterCircleXmark} />
|
||||
{activeFilterCount > 0 && (
|
||||
<Badge className="absolute right-0 bottom-0" variant="primary">
|
||||
<Badge className="absolute -top-2 -right-2" variant="info">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -137,7 +137,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Filters</span>
|
||||
<Badge className="px-1.5 py-0.5" variant="primary">
|
||||
<Badge isSquare variant="info">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import ms from "ms";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import {
|
||||
OrgPermissionAuditLogsActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -76,22 +75,9 @@ const LogsSectionComponent = ({
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-y-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||
<div className="flex items-center gap-x-2 whitespace-nowrap">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Audit History</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/audit-logs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mt-[0.1rem] ml-1 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>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/audit-logs" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||
@@ -137,7 +123,7 @@ const LogsSectionComponent = ({
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text="You can use audit logs if you switch to a paid Infisical plan."
|
||||
text="Your current plan does not include access to audit logs. To unlock this feature, please upgrade to Infisical Pro plan."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,7 +167,7 @@ const LogsSectionComponent = ({
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text="You can use audit logs if you switch to a paid Infisical plan."
|
||||
text="Your current plan does not include access to audit logs. To unlock this feature, please upgrade to Infisical Pro plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
@@ -51,8 +50,7 @@ const Page = () => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"groupCreateUpdate",
|
||||
"deleteGroup",
|
||||
"upgradePlan"
|
||||
"deleteGroup"
|
||||
] as const);
|
||||
|
||||
const onDeleteGroupSubmit = async ({ name, id }: { name: string; id: string }) => {
|
||||
@@ -184,11 +182,6 @@ const Page = () => {
|
||||
onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; id: string })
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -120,7 +120,8 @@ const Page = () => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
text={`Your current plan does not include access to ${popUp.upgradePlan.data?.featureName}. To unlock this feature, please upgrade to Infisical ${popUp.upgradePlan.data?.isEnterpriseFeature ? "Enterprise" : "Pro"} plan.`}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteIdentity.isOpen}
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ export const Content = ({
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
text={`Your current plan does not include access to ${popUp.upgradePlan.data?.featureName}. To unlock this feature, please upgrade to Infisical ${popUp.upgradePlan.data?.isEnterpriseFeature ? "Enterprise" : "Pro"} plan.`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+16
-21
@@ -1,7 +1,8 @@
|
||||
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { EyeIcon } from "lucide-react";
|
||||
|
||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useGetIdentityJwtAuth } from "@app/hooks/api";
|
||||
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
|
||||
import { IdentityJwtAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm";
|
||||
@@ -78,12 +79,10 @@ export const ViewIdentityJwtAuthContent = ({
|
||||
<p className="rounded-sm bg-mineshaft-600 p-2 break-words">{data.jwksCaCert}</p>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="neutral">
|
||||
<EyeIcon />
|
||||
Reveal
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -103,12 +102,10 @@ export const ViewIdentityJwtAuthContent = ({
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="inline-block w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Key {index + 1}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="neutral">
|
||||
<EyeIcon />
|
||||
Key {index + 1}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
@@ -138,12 +135,10 @@ export const ViewIdentityJwtAuthContent = ({
|
||||
</pre>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="neutral">
|
||||
<EyeIcon />
|
||||
Reveal
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
|
||||
+12
-15
@@ -1,9 +1,10 @@
|
||||
import { useMemo } from "react";
|
||||
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { EyeIcon } from "lucide-react";
|
||||
|
||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { gatewaysQueryKeys, useGetIdentityKubernetesAuth } from "@app/hooks/api";
|
||||
import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm";
|
||||
|
||||
@@ -89,12 +90,10 @@ export const ViewIdentityKubernetesAuthContent = ({
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="neutral">
|
||||
<EyeIcon />
|
||||
Reveal
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<p className="text-base leading-4 text-bunker-400 italic">Not set</p>
|
||||
@@ -122,12 +121,10 @@ export const ViewIdentityKubernetesAuthContent = ({
|
||||
className="max-w-xl p-2"
|
||||
content={<p className="rounded-sm bg-mineshaft-600 p-2 break-words">{data.caCert}</p>}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="neutral">
|
||||
<EyeIcon />
|
||||
Reveal
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
|
||||
+12
-15
@@ -1,7 +1,8 @@
|
||||
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { EyeIcon } from "lucide-react";
|
||||
|
||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useClearIdentityLdapAuthLockouts, useGetIdentityLdapAuth } from "@app/hooks/api";
|
||||
import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm";
|
||||
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
|
||||
@@ -72,12 +73,10 @@ export const ViewIdentityLdapAuthContent = ({
|
||||
className="max-w-xl p-2"
|
||||
content={<p className="rounded-sm bg-mineshaft-600 p-2 break-words">{data.bindPass}</p>}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="neutral">
|
||||
<EyeIcon />
|
||||
Reveal
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Search Base / DN">
|
||||
@@ -95,12 +94,10 @@ export const ViewIdentityLdapAuthContent = ({
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="neutral">
|
||||
<EyeIcon />
|
||||
Reveal
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user