mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 20:07:53 +00:00
Merge branch 'main' into feature/secret-reference-shortcut
This commit is contained in:
@@ -1,15 +1,27 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { useListWorkspacePkiAlerts } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { PkiAlertsV2Page } from "@app/views/PkiAlertsV2Page";
|
||||
|
||||
import { PkiAlertsSection } from "./components";
|
||||
import { PkiAlertsSection, PkiCollectionSection } from "./components";
|
||||
|
||||
export const AlertingPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentProject } = useProject();
|
||||
const [selectedTab, setSelectedTab] = useState("rule-based");
|
||||
|
||||
const { data: v1AlertsData } = useListWorkspacePkiAlerts({
|
||||
projectId: currentProject?.id || ""
|
||||
});
|
||||
|
||||
const hasV1Alerts = v1AlertsData?.alerts && v1AlertsData.alerts.length > 0;
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
@@ -26,7 +38,33 @@ export const AlertingPage = () => {
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.PkiAlerts}
|
||||
>
|
||||
<PkiAlertsSection />
|
||||
{!hasV1Alerts ? (
|
||||
<div>
|
||||
<PkiAlertsV2Page hideContainer />
|
||||
</div>
|
||||
) : (
|
||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
<Tab variant="project" value="rule-based">
|
||||
Certificate Alerts
|
||||
</Tab>
|
||||
<Tab variant="project" value="legacy">
|
||||
Collection Alerts (Legacy)
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel value="rule-based">
|
||||
<PkiAlertsV2Page />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value="legacy">
|
||||
<div className="space-y-6">
|
||||
<PkiAlertsSection />
|
||||
<PkiCollectionSection />
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@ 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",
|
||||
@@ -54,9 +53,6 @@ export const PoliciesPage = () => {
|
||||
<Tab variant="project" value={TabSections.Certificates}>
|
||||
Certificates
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.PkiCollections}>
|
||||
Certificate Collections
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel value={TabSections.CertificateProfiles}>
|
||||
@@ -70,10 +66,6 @@ export const PoliciesPage = () => {
|
||||
<TabPanel value={TabSections.Certificates}>
|
||||
<CertificatesTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.PkiCollections}>
|
||||
<PkiCollectionsTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+24
-1
@@ -16,6 +16,7 @@ import {
|
||||
|
||||
import { CreateProfileModal } from "./CreateProfileModal";
|
||||
import { ProfileList } from "./ProfileList";
|
||||
import { RevealAcmeEabSecretModal } from "./RevealAcmeEabSecretModal";
|
||||
|
||||
export const CertificateProfilesTab = () => {
|
||||
const { permission } = useProjectPermission();
|
||||
@@ -23,6 +24,8 @@ export const CertificateProfilesTab = () => {
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isRevealProfileAcmeEabSecretModalOpen, setIsRevealProfileAcmeEabSecretModalOpen] =
|
||||
useState(false);
|
||||
const [selectedProfile, setSelectedProfile] = useState<TCertificateProfileWithDetails | null>(
|
||||
null
|
||||
);
|
||||
@@ -43,6 +46,11 @@ export const CertificateProfilesTab = () => {
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleRevealProfileAcmeEabSecret = (profile: TCertificateProfileWithDetails) => {
|
||||
setSelectedProfile(profile);
|
||||
setIsRevealProfileAcmeEabSecretModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteProfile = (profile: TCertificateProfileWithDetails) => {
|
||||
setSelectedProfile(profile);
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -85,7 +93,11 @@ export const CertificateProfilesTab = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ProfileList onEditProfile={handleEditProfile} onDeleteProfile={handleDeleteProfile} />
|
||||
<ProfileList
|
||||
onEditProfile={handleEditProfile}
|
||||
onRevealProfileAcmeEabSecret={handleRevealProfileAcmeEabSecret}
|
||||
onDeleteProfile={handleDeleteProfile}
|
||||
/>
|
||||
|
||||
<CreateProfileModal isOpen={isCreateModalOpen} onClose={() => setIsCreateModalOpen(false)} />
|
||||
|
||||
@@ -101,6 +113,17 @@ export const CertificateProfilesTab = () => {
|
||||
mode="edit"
|
||||
/>
|
||||
|
||||
{selectedProfile.enrollmentType === "acme" && (
|
||||
<RevealAcmeEabSecretModal
|
||||
isOpen={isRevealProfileAcmeEabSecretModalOpen}
|
||||
onClose={() => {
|
||||
setIsRevealProfileAcmeEabSecretModalOpen(false);
|
||||
setSelectedProfile(null);
|
||||
}}
|
||||
profile={selectedProfile}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title={`Delete Certificate Profile ${selectedProfile.slug}?`}
|
||||
|
||||
+54
-11
@@ -18,6 +18,7 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { useProject } from "@app/context";
|
||||
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||
import {
|
||||
@@ -45,7 +46,7 @@ const createSchema = z
|
||||
.trim()
|
||||
.max(1000, "Description must be less than 1000 characters")
|
||||
.optional(),
|
||||
enrollmentType: z.enum(["api", "est"]),
|
||||
enrollmentType: z.enum(["api", "est", "acme"]),
|
||||
certificateAuthorityId: z.string().min(1, "Certificate Authority is required"),
|
||||
certificateTemplateId: z.string().min(1, "Certificate Template is required"),
|
||||
estConfig: z
|
||||
@@ -72,7 +73,8 @@ const createSchema = z
|
||||
autoRenew: z.boolean().optional(),
|
||||
renewBeforeDays: z.number().min(1).max(365).optional()
|
||||
})
|
||||
.optional()
|
||||
.optional(),
|
||||
acmeConfig: z.object({}).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -82,6 +84,9 @@ const createSchema = z
|
||||
if (data.enrollmentType === "api" && !data.apiConfig) {
|
||||
return false;
|
||||
}
|
||||
if (data.enrollmentType === "acme" && !data.acmeConfig) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
@@ -105,7 +110,7 @@ const editSchema = z
|
||||
.trim()
|
||||
.max(1000, "Description must be less than 1000 characters")
|
||||
.optional(),
|
||||
enrollmentType: z.enum(["api", "est"]),
|
||||
enrollmentType: z.enum(["api", "est", "acme"]),
|
||||
certificateAuthorityId: z.string().optional(),
|
||||
certificateTemplateId: z.string().optional(),
|
||||
estConfig: z
|
||||
@@ -120,7 +125,8 @@ const editSchema = z
|
||||
autoRenew: z.boolean().optional(),
|
||||
renewBeforeDays: z.number().min(1).max(365).optional()
|
||||
})
|
||||
.optional()
|
||||
.optional(),
|
||||
acmeConfig: z.object({}).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -130,6 +136,9 @@ const editSchema = z
|
||||
if (data.enrollmentType === "api" && !data.apiConfig) {
|
||||
return false;
|
||||
}
|
||||
if (data.enrollmentType === "acme" && !data.acmeConfig) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
@@ -188,7 +197,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||
}
|
||||
: undefined
|
||||
: undefined,
|
||||
acmeConfig: profile.enrollmentType === "acme" ? {} : undefined
|
||||
}
|
||||
: {
|
||||
slug: "",
|
||||
@@ -199,7 +209,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
apiConfig: {
|
||||
autoRenew: false,
|
||||
renewBeforeDays: 30
|
||||
}
|
||||
},
|
||||
acmeConfig: {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -230,7 +241,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||
}
|
||||
: undefined
|
||||
: undefined,
|
||||
acmeConfig: profile.enrollmentType === "acme" ? {} : undefined
|
||||
});
|
||||
}
|
||||
}, [isEdit, profile, reset]);
|
||||
@@ -249,6 +261,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
updateData.estConfig = data.estConfig;
|
||||
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
||||
updateData.apiConfig = data.apiConfig;
|
||||
} else if (data.enrollmentType === "acme" && data.acmeConfig) {
|
||||
updateData.acmeConfig = data.acmeConfig;
|
||||
}
|
||||
|
||||
await updateProfile.mutateAsync(updateData);
|
||||
@@ -274,6 +288,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
};
|
||||
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
||||
createData.apiConfig = data.apiConfig;
|
||||
} else if (data.enrollmentType === "acme" && data.acmeConfig) {
|
||||
createData.acmeConfig = data.acmeConfig;
|
||||
}
|
||||
|
||||
await createProfile.mutateAsync(createData);
|
||||
@@ -376,17 +392,23 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
if (watchedEnrollmentType === "est") {
|
||||
setValue("apiConfig", undefined);
|
||||
setValue("estConfig", {
|
||||
disableBootstrapCaValidation: false,
|
||||
passphrase: ""
|
||||
});
|
||||
setValue("apiConfig", undefined);
|
||||
} else {
|
||||
setValue("acmeConfig", undefined);
|
||||
} else if (watchedEnrollmentType === "api") {
|
||||
setValue("apiConfig", {
|
||||
autoRenew: false,
|
||||
renewBeforeDays: 30
|
||||
});
|
||||
setValue("estConfig", undefined);
|
||||
setValue("acmeConfig", undefined);
|
||||
} else if (watchedEnrollmentType === "acme") {
|
||||
setValue("estConfig", undefined);
|
||||
setValue("apiConfig", undefined);
|
||||
setValue("acmeConfig", {});
|
||||
}
|
||||
onChange(value);
|
||||
}}
|
||||
@@ -424,12 +446,18 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
disableBootstrapCaValidation: false,
|
||||
passphrase: ""
|
||||
});
|
||||
} else {
|
||||
setValue("estConfig", undefined);
|
||||
setValue("acmeConfig", undefined);
|
||||
} else if (value === "api") {
|
||||
setValue("apiConfig", {
|
||||
autoRenew: false,
|
||||
renewBeforeDays: 30
|
||||
});
|
||||
setValue("estConfig", undefined);
|
||||
setValue("acmeConfig", undefined);
|
||||
} else if (value === "acme") {
|
||||
setValue("apiConfig", undefined);
|
||||
setValue("estConfig", undefined);
|
||||
setValue("acmeConfig", {});
|
||||
}
|
||||
onChange(value);
|
||||
}}
|
||||
@@ -439,6 +467,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
>
|
||||
<SelectItem value="api">API</SelectItem>
|
||||
<SelectItem value="est">EST</SelectItem>
|
||||
{envConfig.ACME_FEATURE_ENABLED && <SelectItem value="acme">ACME</SelectItem>}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
@@ -548,6 +577,20 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ACME Configuration */}
|
||||
{watchedEnrollmentType === "acme" && (
|
||||
<div className="mb-4 space-y-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="acmeConfig"
|
||||
render={({ fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<div className="flex items-center gap-2">{/* FIXME: ACME configuration */}</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{watchedAutoRenew && (
|
||||
<div className="mb-4 space-y-4">
|
||||
<Controller
|
||||
|
||||
+7
-1
@@ -20,9 +20,14 @@ import { ProfileRow } from "./ProfileRow";
|
||||
interface Props {
|
||||
onEditProfile: (profile: TCertificateProfileWithDetails) => void;
|
||||
onDeleteProfile: (profile: TCertificateProfileWithDetails) => void;
|
||||
onRevealProfileAcmeEabSecret: (profile: TCertificateProfileWithDetails) => void;
|
||||
}
|
||||
|
||||
export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
export const ProfileList = ({
|
||||
onEditProfile,
|
||||
onRevealProfileAcmeEabSecret,
|
||||
onDeleteProfile
|
||||
}: Props) => {
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { data, isLoading } = useListCertificateProfiles({
|
||||
@@ -88,6 +93,7 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
onEditProfile={onEditProfile}
|
||||
onRevealProfileAcmeEabSecret={onRevealProfileAcmeEabSecret}
|
||||
onDeleteProfile={onDeleteProfile}
|
||||
/>
|
||||
))}
|
||||
|
||||
+30
-6
@@ -5,6 +5,7 @@ import {
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faPlus,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -36,10 +37,16 @@ import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPa
|
||||
interface Props {
|
||||
profile: TCertificateProfile;
|
||||
onEditProfile: (profile: TCertificateProfile) => void;
|
||||
onRevealProfileAcmeEabSecret: (profile: TCertificateProfile) => void;
|
||||
onDeleteProfile: (profile: TCertificateProfile) => void;
|
||||
}
|
||||
|
||||
export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) => {
|
||||
export const ProfileRow = ({
|
||||
profile,
|
||||
onEditProfile,
|
||||
onRevealProfileAcmeEabSecret,
|
||||
onDeleteProfile
|
||||
}: Props) => {
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const { data: caData } = useGetCaById(profile.caId);
|
||||
@@ -69,6 +76,11 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const canRevealProfileAcmeEabSecret = permission.can(
|
||||
ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const canIssueCertificate = permission.can(
|
||||
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
@@ -82,7 +94,8 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
const getEnrollmentTypeBadge = (enrollmentType: string) => {
|
||||
const config = {
|
||||
api: { variant: "ghost" as const, label: "API" },
|
||||
est: { variant: "ghost" as const, label: "EST" }
|
||||
est: { variant: "ghost" as const, label: "EST" },
|
||||
acme: { variant: "ghost" as const, label: "ACME" }
|
||||
} as const;
|
||||
|
||||
const configKey = Object.keys(config).includes(enrollmentType)
|
||||
@@ -127,7 +140,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} className="w-3" />}
|
||||
onClick={() => handleCopyId()}
|
||||
>
|
||||
Copy Profile ID
|
||||
@@ -138,18 +151,29 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
e.stopPropagation();
|
||||
onEditProfile(profile);
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />}
|
||||
>
|
||||
Edit Profile
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canRevealProfileAcmeEabSecret && profile.enrollmentType === "acme" && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRevealProfileAcmeEabSecret(profile);
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faEye} className="w-3" />}
|
||||
>
|
||||
Reveal ACME EAB
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canIssueCertificate && profile.enrollmentType === "api" && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpToggle("issueCertificate");
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
icon={<FontAwesomeIcon icon={faPlus} className="w-3" />}
|
||||
>
|
||||
Issue Certificate
|
||||
</DropdownMenuItem>
|
||||
@@ -160,7 +184,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
e.stopPropagation();
|
||||
onDeleteProfile(profile);
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
|
||||
>
|
||||
Delete Profile
|
||||
</DropdownMenuItem>
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Spinner
|
||||
} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { TCertificateProfileWithDetails } from "@app/hooks/api/certificateProfiles";
|
||||
import { useRevealAcmeEabSecret } from "@app/hooks/api/certificateProfiles/queries";
|
||||
|
||||
const RESET_COPIED_DELAY = 1 * 1000;
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
profile: TCertificateProfileWithDetails;
|
||||
};
|
||||
|
||||
export const RevealAcmeEabSecretModal = ({ isOpen, onClose, profile }: Props) => {
|
||||
const [isAcmeDirectoryUrlCopied, setIsAcmeDirectoryUrlCopied] = useToggle(false);
|
||||
const [isEabKidCopied, setIsEabKidCopied] = useToggle(false);
|
||||
const [isEabSecretCopied, setIsEabSecretCopied] = useToggle(false);
|
||||
const revealAcmeEabSecret = useRevealAcmeEabSecret({ profileId: profile.id });
|
||||
const { data, isLoading, isError, error } = revealAcmeEabSecret;
|
||||
const { directoryUrl } = profile.acmeConfig!;
|
||||
const { eabKid, eabSecret } = data ?? { eabKid: "", eabSecret: "" };
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Reveal ACME EAB"
|
||||
subTitle="To issue certificates automatically, your ACME client needs the following details."
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
)}
|
||||
{isError && (
|
||||
<Alert variant="danger">
|
||||
<AlertDescription>Failed to reveal EAB secret: {error.message}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{data && (
|
||||
<>
|
||||
<FormLabel
|
||||
label="ACME Directory URL"
|
||||
tooltipText="The ACME directory URL for your ACME client to issue certificates."
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input value={directoryUrl} disabled />
|
||||
<IconButton
|
||||
ariaLabel="copy"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(directoryUrl);
|
||||
setIsAcmeDirectoryUrlCopied.on();
|
||||
setTimeout(() => {
|
||||
setIsAcmeDirectoryUrlCopied.off();
|
||||
}, RESET_COPIED_DELAY);
|
||||
}}
|
||||
className="w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={isAcmeDirectoryUrlCopied ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<FormLabel
|
||||
label="EAB KID"
|
||||
className="mt-4"
|
||||
tooltipText="The EAB Key Identifier (KID) for your ACME client to authenticate when registering a new account."
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input value={eabKid} disabled />
|
||||
<IconButton
|
||||
ariaLabel="copy"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(eabKid);
|
||||
setIsEabKidCopied.on();
|
||||
setTimeout(() => {
|
||||
setIsEabKidCopied.off();
|
||||
}, RESET_COPIED_DELAY);
|
||||
}}
|
||||
className="w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={isEabKidCopied ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<FormLabel
|
||||
label="EAB Secret"
|
||||
className="mt-4"
|
||||
tooltipText="The EAB Secret for your ACME client to authenticate when registering a new account."
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input value={eabSecret} isDisabled />
|
||||
<IconButton
|
||||
ariaLabel="copy"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(eabSecret);
|
||||
setIsEabSecretCopied.on();
|
||||
setTimeout(() => {
|
||||
setIsEabSecretCopied.off();
|
||||
}, RESET_COPIED_DELAY);
|
||||
}}
|
||||
className="w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={isEabSecretCopied ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+103
-4
@@ -6,7 +6,15 @@ import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
APP_CONNECTION_MAP,
|
||||
getAppConnectionMethodDetails,
|
||||
@@ -27,10 +35,13 @@ import {
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
|
||||
type CertificateForm = z.infer<typeof certificateSchema>;
|
||||
|
||||
type TInputFormData = ClientSecretForm | CertificateForm;
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureClientSecretsConnection;
|
||||
onSubmit: (formData: ClientSecretForm) => Promise<void>;
|
||||
onSubmit: (formData: TInputFormData) => Promise<void>;
|
||||
projectId: string | undefined | null;
|
||||
};
|
||||
|
||||
@@ -53,7 +64,21 @@ const clientSecretSchema = baseSchema.extend({
|
||||
})
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [oauthSchema, clientSecretSchema]);
|
||||
const certificateSchema = baseSchema.extend({
|
||||
method: z.literal(AzureClientSecretsConnectionMethod.Certificate),
|
||||
credentials: z.object({
|
||||
clientId: z.string().trim().min(1, "Client ID is required"),
|
||||
certificateBody: z.string().trim().min(1, "Certificate is required"),
|
||||
privateKey: z.string().trim().min(1, "Private Key is required"),
|
||||
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
||||
})
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
oauthSchema,
|
||||
clientSecretSchema,
|
||||
certificateSchema
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -96,6 +121,20 @@ const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partia
|
||||
};
|
||||
}
|
||||
break;
|
||||
case AzureClientSecretsConnectionMethod.Certificate:
|
||||
if ("clientId" in credentials && "tenantId" in credentials) {
|
||||
return {
|
||||
...base,
|
||||
method: AzureClientSecretsConnectionMethod.Certificate,
|
||||
credentials: {
|
||||
clientId: credentials.clientId,
|
||||
tenantId: credentials.tenantId,
|
||||
certificateBody: "",
|
||||
privateKey: ""
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
@@ -152,6 +191,9 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, proj
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
await onSubmit(formData);
|
||||
break;
|
||||
case AzureClientSecretsConnectionMethod.Certificate:
|
||||
await onSubmit(formData);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||
}
|
||||
@@ -207,7 +249,11 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, proj
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="tenantId"
|
||||
name={
|
||||
selectedMethod === AzureClientSecretsConnectionMethod.OAuth
|
||||
? "tenantId"
|
||||
: "credentials.tenantId"
|
||||
}
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -264,6 +310,59 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, proj
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedMethod === AzureClientSecretsConnectionMethod.Certificate && (
|
||||
<>
|
||||
<Controller
|
||||
name="credentials.clientId"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
label="Client ID"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.certificateBody"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
label="Certificate"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<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)}
|
||||
placeholder="-----BEGIN CERTIFICATE-----..."
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.privateKey"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
label="Private Key"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
placeholder="-----BEGIN PRIVATE KEY-----..."
|
||||
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"
|
||||
|
||||
+7
-3
@@ -160,14 +160,18 @@ export const SecretApprovalRequestChanges = ({ approvalRequestId, onGoBack }: Pr
|
||||
secretApprovalRequestDetails.policy.bypassers.some(({ userId }) => userId === userSession.id);
|
||||
|
||||
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||
Record<string, { status: ApprovalStatus; comment: string; isOrgMembershipActive: boolean }>
|
||||
Record<
|
||||
string,
|
||||
{ status: ApprovalStatus; comment: string; isOrgMembershipActive: boolean; createdAt: Date }
|
||||
>
|
||||
>(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
[curr.userId]: {
|
||||
status: curr.status,
|
||||
comment: curr.comment,
|
||||
isOrgMembershipActive: curr.isOrgMembershipActive
|
||||
isOrgMembershipActive: curr.isOrgMembershipActive,
|
||||
createdAt: curr.createdAt
|
||||
}
|
||||
}),
|
||||
{}
|
||||
@@ -488,7 +492,7 @@ export const SecretApprovalRequestChanges = ({ approvalRequestId, onGoBack }: Pr
|
||||
</span>{" "}
|
||||
the request on{" "}
|
||||
{format(
|
||||
new Date(secretApprovalRequestDetails.createdAt),
|
||||
new Date(reviewer ? reviewer.createdAt : new Date()),
|
||||
"MM/dd/yyyy h:mm:ss aa"
|
||||
)}
|
||||
.
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ export const WorkflowIntegrationTab = () => {
|
||||
<Td>Provider</Td>
|
||||
<Td>Access Request Notifications Destination</Td>
|
||||
<Td>Secret Request Notifications Destination</Td>
|
||||
<Td />
|
||||
<Td>Secret Sync Error Notifications Destination</Td>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
|
||||
+3
-1
@@ -99,7 +99,9 @@ export const MicrosoftTeamsConfigRow = ({
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
|
||||
<Td>
|
||||
<Badge variant="danger">Disabled</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
|
||||
+19
-1
@@ -96,7 +96,25 @@ export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackCon
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
|
||||
<Td>
|
||||
{slackConfig.isSecretSyncErrorNotificationEnabled &&
|
||||
!isLoadingConfig &&
|
||||
slackConfig.secretSyncErrorChannels.length > 0 ? (
|
||||
<p>
|
||||
{slackConfig.secretSyncErrorChannels
|
||||
.split(", ")
|
||||
.map((channel) => slackChannelIdToName[channel])
|
||||
.join(", ")}
|
||||
</p>
|
||||
) : isLoadingConfig ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<Badge variant="neutral">
|
||||
<BanIcon />
|
||||
Disabled
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
|
||||
+144
-114
@@ -37,11 +37,76 @@ const formSchema = z.object({
|
||||
isSecretRequestNotificationEnabled: z.boolean(),
|
||||
secretRequestChannels: z.string().array(),
|
||||
isAccessRequestNotificationEnabled: z.boolean(),
|
||||
accessRequestChannels: z.string().array()
|
||||
accessRequestChannels: z.string().array(),
|
||||
isSecretSyncErrorNotificationEnabled: z.boolean(),
|
||||
secretSyncErrorChannels: z.string().array()
|
||||
});
|
||||
|
||||
type TSlackConfigForm = z.infer<typeof formSchema>;
|
||||
|
||||
type TChannelSelectorProps = {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
error?: { message?: string };
|
||||
slackChannelIdToName: Record<string, string>;
|
||||
sortedSlackChannels?: { id: string; name: string }[];
|
||||
keyPrefix: string;
|
||||
};
|
||||
|
||||
const ChannelSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
slackChannelIdToName,
|
||||
sortedSlackChannels,
|
||||
keyPrefix
|
||||
}: TChannelSelectorProps) => (
|
||||
<FormControl label="Slack channels" isError={Boolean(error)} errorText={error?.message}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={value
|
||||
?.filter(Boolean)
|
||||
.map((entry) => slackChannelIdToName[entry])
|
||||
.join(", ")}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||
maxHeight: "350px",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{sortedSlackChannels?.map((slackChannel) => {
|
||||
const isChecked = value?.includes(slackChannel.id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el: string) => el !== slackChannel.id)
|
||||
: [...(value || []), slackChannel.id]
|
||||
);
|
||||
}}
|
||||
key={`${keyPrefix}-slack-channel-${slackChannel.id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{slackChannel.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -71,7 +136,9 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
isAccessRequestNotificationEnabled: false,
|
||||
accessRequestChannels: [],
|
||||
isSecretRequestNotificationEnabled: false,
|
||||
secretRequestChannels: []
|
||||
secretRequestChannels: [],
|
||||
isSecretSyncErrorNotificationEnabled: false,
|
||||
secretSyncErrorChannels: []
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,7 +153,8 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
integration: WorkflowIntegrationPlatform.SLACK,
|
||||
integrationId: data.slackIntegrationId,
|
||||
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
|
||||
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
|
||||
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", "),
|
||||
secretSyncErrorChannels: data.secretSyncErrorChannels.filter(Boolean).join(", ")
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -100,6 +168,7 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
||||
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
||||
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
||||
const secretSyncErrorNotifState = watch("isSecretSyncErrorNotificationEnabled");
|
||||
|
||||
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
|
||||
const slackChannelIdToName = Object.fromEntries(
|
||||
@@ -110,7 +179,7 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (slackConfig) {
|
||||
if (slackConfig && slackConfig.integration === WorkflowIntegrationPlatform.SLACK) {
|
||||
setValue("slackIntegrationId", slackConfig.integrationId);
|
||||
setValue(
|
||||
"isSecretRequestNotificationEnabled",
|
||||
@@ -120,22 +189,30 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
"isAccessRequestNotificationEnabled",
|
||||
slackConfig.isAccessRequestNotificationEnabled
|
||||
);
|
||||
setValue(
|
||||
"isSecretSyncErrorNotificationEnabled",
|
||||
slackConfig.isSecretSyncErrorNotificationEnabled
|
||||
);
|
||||
|
||||
if (slackConfig.integration === WorkflowIntegrationPlatform.SLACK) {
|
||||
if (slackChannels) {
|
||||
setValue(
|
||||
"secretRequestChannels",
|
||||
slackConfig.secretRequestChannels
|
||||
.split(", ")
|
||||
.filter((channel) => channel in slackChannelIdToName)
|
||||
);
|
||||
setValue(
|
||||
"accessRequestChannels",
|
||||
slackConfig.accessRequestChannels
|
||||
.split(", ")
|
||||
.filter((channel) => channel in slackChannelIdToName)
|
||||
);
|
||||
}
|
||||
if (slackChannels) {
|
||||
setValue(
|
||||
"secretRequestChannels",
|
||||
(slackConfig.secretRequestChannels || "")
|
||||
.split(", ")
|
||||
.filter((channel) => channel in slackChannelIdToName)
|
||||
);
|
||||
setValue(
|
||||
"accessRequestChannels",
|
||||
(slackConfig.accessRequestChannels || "")
|
||||
.split(", ")
|
||||
.filter((channel) => channel in slackChannelIdToName)
|
||||
);
|
||||
setValue(
|
||||
"secretSyncErrorChannels",
|
||||
(slackConfig.secretSyncErrorChannels || "")
|
||||
.split(", ")
|
||||
.filter((channel) => channel in slackChannelIdToName)
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [slackConfig, slackChannels]);
|
||||
@@ -208,54 +285,14 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
control={control}
|
||||
name="secretRequestChannels"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Slack channels"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={value
|
||||
?.filter(Boolean)
|
||||
.map((entry) => slackChannelIdToName[entry])
|
||||
.join(", ")}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||
maxHeight: "350px",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{sortedSlackChannels?.map((slackChannel) => {
|
||||
const isChecked = value?.includes(slackChannel.id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el: string) => el !== slackChannel.id)
|
||||
: [...(value || []), slackChannel.id]
|
||||
);
|
||||
}}
|
||||
key={`secret-requests-slack-channel-${slackChannel.id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{slackChannel.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
<ChannelSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
slackChannelIdToName={slackChannelIdToName}
|
||||
sortedSlackChannels={sortedSlackChannels}
|
||||
keyPrefix="secret-requests"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -281,54 +318,47 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
control={control}
|
||||
name="accessRequestChannels"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Slack channels"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={value
|
||||
?.filter(Boolean)
|
||||
.map((entry) => slackChannelIdToName[entry])
|
||||
.join(", ")}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||
maxHeight: "350px",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{sortedSlackChannels?.map((slackChannel) => {
|
||||
const isChecked = value?.includes(slackChannel.id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el: string) => el !== slackChannel.id)
|
||||
: [...(value || []), slackChannel.id]
|
||||
);
|
||||
}}
|
||||
key={`access-requests-slack-channel-${slackChannel.id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{slackChannel.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ChannelSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
slackChannelIdToName={slackChannelIdToName}
|
||||
sortedSlackChannels={sortedSlackChannels}
|
||||
keyPrefix="access-requests"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="isSecretSyncErrorNotificationEnabled"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
|
||||
<Switch
|
||||
id="secret-sync-error-notification"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Secret Sync Errors</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{secretSyncErrorNotifState && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretSyncErrorChannels"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<ChannelSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
slackChannelIdToName={slackChannelIdToName}
|
||||
sortedSlackChannels={sortedSlackChannels}
|
||||
keyPrefix="secret-sync-errors"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user