mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 21:25:45 +00:00
feat(workflow-integration): enhance project workflow integration with secret sync error notifications and improve Slack notification formatting
This commit is contained in:
@@ -243,7 +243,8 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
);
|
||||
|
||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||
const approvalPath = `/projects/secret-management/${project.id}/approval`;
|
||||
const projectPath = `/projects/secret-management/${project.id}`;
|
||||
const approvalPath = `${projectPath}/approval`;
|
||||
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
||||
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
@@ -252,6 +253,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
type: TriggerFeature.ACCESS_REQUEST,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
projectPath,
|
||||
requesterFullName,
|
||||
isTemporary,
|
||||
requesterEmail: requestedByUser.email as string,
|
||||
|
||||
@@ -32,6 +32,7 @@ export type TNotification =
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectName: string;
|
||||
projectPath: string;
|
||||
permissions: string[];
|
||||
approvalUrl: string;
|
||||
note?: string;
|
||||
@@ -61,6 +62,10 @@ export type TNotification =
|
||||
syncDestination: string;
|
||||
failureMessage: string;
|
||||
syncUrl: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
projectName: string;
|
||||
projectPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1244,7 +1244,10 @@ export const registerRoutes = async (
|
||||
licenseService,
|
||||
gatewayService,
|
||||
gatewayV2Service,
|
||||
notificationService
|
||||
notificationService,
|
||||
projectSlackConfigDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService
|
||||
});
|
||||
|
||||
const secretQueueService = secretQueueFactory({
|
||||
|
||||
@@ -769,7 +769,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
secretRequestChannels: true,
|
||||
isSecretSyncErrorNotificationEnabled: true,
|
||||
secretSyncErrorChannels: true
|
||||
}).merge(
|
||||
z.object({
|
||||
integration: z.literal(WorkflowIntegration.SLACK),
|
||||
@@ -873,7 +875,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
accessRequestChannels: validateSlackChannelsField,
|
||||
secretRequestChannels: validateSlackChannelsField,
|
||||
isAccessRequestNotificationEnabled: z.boolean(),
|
||||
isSecretRequestNotificationEnabled: z.boolean()
|
||||
isSecretRequestNotificationEnabled: z.boolean(),
|
||||
secretSyncErrorChannels: validateSlackChannelsField,
|
||||
isSecretSyncErrorNotificationEnabled: z.boolean()
|
||||
}),
|
||||
z.object({
|
||||
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||
@@ -891,7 +895,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
secretRequestChannels: true,
|
||||
isSecretSyncErrorNotificationEnabled: true,
|
||||
secretSyncErrorChannels: true
|
||||
}).merge(
|
||||
z.object({
|
||||
integration: z.literal(WorkflowIntegration.SLACK),
|
||||
|
||||
@@ -1568,8 +1568,14 @@ export const projectServiceFactory = ({
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
}: TUpdateProjectWorkflowIntegration) => {
|
||||
secretRequestChannels,
|
||||
secretSyncErrorChannels,
|
||||
isSecretSyncErrorNotificationEnabled
|
||||
}: TUpdateProjectWorkflowIntegration & {
|
||||
// workaround intersection type while we don't have the microsoft teams integration for failed secret syncs
|
||||
isSecretSyncErrorNotificationEnabled: boolean;
|
||||
secretSyncErrorChannels: string;
|
||||
}) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
@@ -1591,6 +1597,7 @@ export const projectServiceFactory = ({
|
||||
|
||||
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
|
||||
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
|
||||
const sanitizedSecretSyncErrorChannels = validateSlackChannelsField.parse(secretSyncErrorChannels);
|
||||
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||
|
||||
@@ -1628,7 +1635,9 @@ export const projectServiceFactory = ({
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels
|
||||
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||
isSecretSyncErrorNotificationEnabled,
|
||||
secretSyncErrorChannels: sanitizedSecretSyncErrorChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -1641,7 +1650,9 @@ export const projectServiceFactory = ({
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels
|
||||
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||
isSecretSyncErrorNotificationEnabled,
|
||||
secretSyncErrorChannels: sanitizedSecretSyncErrorChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -1651,6 +1662,7 @@ export const projectServiceFactory = ({
|
||||
...updatedWorkflowIntegration,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||
secretSyncErrorChannels: sanitizedSecretSyncErrorChannels,
|
||||
integrationId: slackIntegration.id,
|
||||
integration: WorkflowIntegration.SLACK
|
||||
} as const;
|
||||
|
||||
@@ -184,8 +184,10 @@ export type TUpdateProjectWorkflowIntegration = (
|
||||
integration: WorkflowIntegration.SLACK;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
isSecretSyncErrorNotificationEnabled: boolean;
|
||||
accessRequestChannels?: string;
|
||||
secretRequestChannels?: string;
|
||||
secretSyncErrorChannels?: string;
|
||||
}
|
||||
| {
|
||||
integrationId: string;
|
||||
|
||||
@@ -932,7 +932,9 @@ export const secretSyncQueueFactory = ({
|
||||
break;
|
||||
}
|
||||
|
||||
const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`;
|
||||
const baseProjectPath = `/projects/secret-management/${projectId}`;
|
||||
const overviewPath = `${baseProjectPath}/overview`;
|
||||
const syncPath = `${baseProjectPath}/integrations/secret-syncs/${destination}/${secretSync.id}`;
|
||||
|
||||
const notifications = [
|
||||
triggerWorkflowIntegrationNotification({
|
||||
@@ -944,10 +946,14 @@ export const secretSyncQueueFactory = ({
|
||||
syncDestination,
|
||||
failureMessage: failureMessage || "An unknown error occurred",
|
||||
syncUrl: `${appCfg.SITE_URL}${syncPath}`,
|
||||
syncActionLabel: actionLabel
|
||||
syncActionLabel: actionLabel,
|
||||
environment: environment?.name || "-",
|
||||
secretPath: folder?.path || "-",
|
||||
projectName: project.name,
|
||||
projectPath: overviewPath
|
||||
}
|
||||
},
|
||||
projectId: project.id
|
||||
projectId
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
|
||||
@@ -76,6 +76,7 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo
|
||||
];
|
||||
|
||||
return {
|
||||
headerBlocks: [],
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks,
|
||||
color: COMPANY_BRAND_COLOR
|
||||
@@ -83,20 +84,15 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo
|
||||
}
|
||||
case TriggerFeature.ACCESS_REQUEST: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||
payload.isTemporary ? "temporary" : "permanent"
|
||||
} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
|
||||
|
||||
The following permissions are requested: ${payload.permissions.join(", ")}
|
||||
const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}`;
|
||||
const accessType = payload.isTemporary ? "temporary" : "permanent";
|
||||
const permissionsFormatted = payload.permissions.map((p) => `*${p}*`).join(", ");
|
||||
|
||||
View the request and approve or deny it <${payload.approvalUrl}|here>.${
|
||||
payload.note
|
||||
? `
|
||||
User Note: ${payload.note}`
|
||||
: ""
|
||||
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${accessType} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.\n\nThe following permissions are requested: ${payload.permissions.join(", ")}${
|
||||
payload.note ? `\n\nUser note\n${payload.note}` : ""
|
||||
}`;
|
||||
|
||||
const payloadBlocks = [
|
||||
const headerBlocks = [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
@@ -104,17 +100,38 @@ User Note: ${payload.note}`
|
||||
text: "New access approval request pending for review",
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const payloadBlocks = [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: messageBody
|
||||
text: `*${payload.requesterFullName}* (${payload.requesterEmail}) has requested *${accessType}* access to *${payload.secretPath}* in the *${payload.environment}* environment of *<${projectUrl}|${payload.projectName}>*.\n\nThe following permissions are requested: ${permissionsFormatted}${
|
||||
payload.note ? `\n\n*User note*\n${payload.note}` : ""
|
||||
}`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "View request",
|
||||
emoji: true
|
||||
},
|
||||
style: "primary",
|
||||
url: payload.approvalUrl
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
headerBlocks,
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks,
|
||||
color: COMPANY_BRAND_COLOR
|
||||
@@ -125,7 +142,7 @@ User Note: ${payload.note}`
|
||||
const messageBody = `${payload.editorFullName} (${payload.editorEmail}) has updated the ${
|
||||
payload.isTemporary ? "temporary" : "permanent"
|
||||
} access request from ${payload.requesterFullName} (${payload.requesterEmail}) to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
|
||||
|
||||
|
||||
The following permissions are requested: ${payload.permissions.join(", ")}
|
||||
|
||||
View the request and approve or deny it <${payload.approvalUrl}|here>.${
|
||||
@@ -154,6 +171,7 @@ Editor Note: ${payload.editNote}`
|
||||
];
|
||||
|
||||
return {
|
||||
headerBlocks: [],
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks,
|
||||
color: COMPANY_BRAND_COLOR
|
||||
@@ -161,24 +179,26 @@ Editor Note: ${payload.editNote}`
|
||||
}
|
||||
case TriggerFeature.SECRET_SYNC_ERROR: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}
|
||||
const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}`;
|
||||
const messageBody = `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}\n\n\nEnvironment: ${payload.environment}\n\n\nSecret Path: ${payload.secretPath}\n\n\nProject: ${payload.projectName} (${projectUrl})\n\n\nReason:\n${payload.failureMessage}`;
|
||||
|
||||
Sync Error: ${payload.failureMessage}`;
|
||||
|
||||
const payloadBlocks = [
|
||||
const headerBlocks = [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`,
|
||||
text: `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`,
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const payloadBlocks = [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Sync Error:* ${payload.failureMessage}`
|
||||
text: `*Environment*\n${payload.environment}\n\n\n*Secret Path*\n${payload.secretPath}\n\n\n*Project*\n<${projectUrl}|${payload.projectName}>\n\n\n*Reason*\n${payload.failureMessage}`
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -188,9 +208,10 @@ Sync Error: ${payload.failureMessage}`;
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: `Open ${payload.syncName}`,
|
||||
text: "Open secret sync",
|
||||
emoji: true
|
||||
},
|
||||
style: "primary",
|
||||
url: payload.syncUrl
|
||||
}
|
||||
]
|
||||
@@ -199,6 +220,7 @@ Sync Error: ${payload.failureMessage}`;
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
headerBlocks,
|
||||
payloadBlocks,
|
||||
color: ERROR_COLOR
|
||||
};
|
||||
@@ -227,14 +249,16 @@ export const sendSlackNotification = async ({
|
||||
}).toString("utf8");
|
||||
const slackWebClient = new WebClient(botKey);
|
||||
|
||||
const { payloadMessage, payloadBlocks, color } = buildSlackPayload(notification);
|
||||
const { payloadMessage, payloadBlocks, color, headerBlocks } = buildSlackPayload(notification);
|
||||
|
||||
for await (const conversationId of targetChannelIds) {
|
||||
// we send both text and blocks for compatibility with barebone clients
|
||||
|
||||
await slackWebClient.chat
|
||||
.postMessage({
|
||||
channel: conversationId,
|
||||
text: payloadMessage,
|
||||
blocks: headerBlocks,
|
||||
attachments: [
|
||||
{
|
||||
color,
|
||||
|
||||
@@ -86,6 +86,8 @@ export type ProjectWorkflowIntegrationConfig =
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
isSecretSyncErrorNotificationEnabled: boolean;
|
||||
secretSyncErrorChannels: string;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
@@ -112,6 +114,8 @@ export type TUpdateProjectWorkflowIntegrationConfigDTO =
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
isSecretSyncErrorNotificationEnabled: boolean;
|
||||
secretSyncErrorChannels: string;
|
||||
}
|
||||
| {
|
||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS;
|
||||
|
||||
+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
@@ -92,7 +92,9 @@ export const MicrosoftTeamsConfigRow = ({
|
||||
<Badge variant="danger">Disabled</Badge>
|
||||
)}
|
||||
</Td>
|
||||
|
||||
<Td>
|
||||
<Badge variant="danger">Coming Soon</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
|
||||
+16
-1
@@ -89,7 +89,22 @@ export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackCon
|
||||
<Badge variant="danger">Disabled</Badge>
|
||||
)}
|
||||
</Td>
|
||||
|
||||
<Td>
|
||||
{slackConfig.isSecretSyncErrorNotificationEnabled &&
|
||||
!isLoadingConfig &&
|
||||
slackConfig.secretSyncErrorChannels.length > 0 ? (
|
||||
<Badge>
|
||||
{slackConfig.secretSyncErrorChannels
|
||||
.split(", ")
|
||||
.map((channel) => slackChannelIdToName[channel])
|
||||
.join(", ")}
|
||||
</Badge>
|
||||
) : isLoadingConfig ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<Badge variant="danger">Disabled</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
|
||||
+141
-114
@@ -37,7 +37,9 @@ 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>;
|
||||
@@ -71,7 +73,9 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
isAccessRequestNotificationEnabled: false,
|
||||
accessRequestChannels: [],
|
||||
isSecretRequestNotificationEnabled: false,
|
||||
secretRequestChannels: []
|
||||
secretRequestChannels: [],
|
||||
isSecretSyncErrorNotificationEnabled: false,
|
||||
secretSyncErrorChannels: []
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87,7 +91,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({
|
||||
@@ -107,6 +112,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(
|
||||
@@ -117,7 +123,7 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (slackConfig) {
|
||||
if (slackConfig && slackConfig.integration === WorkflowIntegrationPlatform.SLACK) {
|
||||
setValue("slackIntegrationId", slackConfig.integrationId);
|
||||
setValue(
|
||||
"isSecretRequestNotificationEnabled",
|
||||
@@ -127,22 +133,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]);
|
||||
@@ -215,54 +229,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"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -288,54 +262,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"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -353,3 +320,63 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
Reference in New Issue
Block a user