mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 08:27:36 +00:00
feat: slack channel suggestions
This commit is contained in:
@@ -576,16 +576,18 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
projectId: req.params.workspaceId
|
projectId: req.params.workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
if (slackConfig) {
|
||||||
...req.auditLogInfo,
|
await server.services.auditLog.createAuditLog({
|
||||||
projectId: req.params.workspaceId,
|
...req.auditLogInfo,
|
||||||
event: {
|
projectId: req.params.workspaceId,
|
||||||
type: EventType.GET_PROJECT_SLACK_CONFIG,
|
event: {
|
||||||
metadata: {
|
type: EventType.GET_PROJECT_SLACK_CONFIG,
|
||||||
id: slackConfig.id
|
metadata: {
|
||||||
|
id: slackConfig.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return slackConfig;
|
return slackConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,6 +232,44 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:slackIntegrationId/channels",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
slackIntegrationId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const slackChannels = await server.services.slack.getSlackIntegrationChannels({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.slackIntegrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return slackChannels;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:slackIntegrationId",
|
url: "/:slackIntegrationId",
|
||||||
|
|||||||
@@ -1,11 +1,44 @@
|
|||||||
import { Block, WebClient } from "@slack/web-api";
|
import { Block, WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
|
||||||
import { SlackTriggerFeature } from "./slack-types";
|
import { SlackTriggerFeature } from "./slack-types";
|
||||||
|
|
||||||
|
export const fetchSlackChannels = async (botKey: string) => {
|
||||||
|
const slackChannels: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
const slackWebClient = new WebClient(botKey);
|
||||||
|
let cursor;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response = await slackWebClient.conversations.list({
|
||||||
|
cursor,
|
||||||
|
limit: 1000,
|
||||||
|
types: "public_channel,private_channel"
|
||||||
|
});
|
||||||
|
|
||||||
|
response.channels?.forEach((channel) =>
|
||||||
|
slackChannels.push({
|
||||||
|
name: channel.name_normalized as string,
|
||||||
|
id: channel.id as string
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set the cursor for the next page
|
||||||
|
cursor = response.response_metadata?.next_cursor;
|
||||||
|
} while (cursor); // Continue while there is a cursor
|
||||||
|
|
||||||
|
return slackChannels;
|
||||||
|
};
|
||||||
|
|
||||||
export const triggerSlackNotification = async ({
|
export const triggerSlackNotification = async ({
|
||||||
projectId,
|
projectId,
|
||||||
payloadBlocks,
|
payloadBlocks,
|
||||||
@@ -30,15 +63,15 @@ export const triggerSlackNotification = async ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetChannels: string[] = [];
|
let targetChannelIds: string[] = [];
|
||||||
if (feature === SlackTriggerFeature.ACCESS_REQUEST) {
|
if (feature === SlackTriggerFeature.ACCESS_REQUEST) {
|
||||||
targetChannels = slackIntegration.accessRequestChannels?.split(", ") || [];
|
targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || [];
|
||||||
if (!targetChannels.length || !slackIntegration.isAccessRequestNotificationEnabled) {
|
if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (feature === SlackTriggerFeature.SECRET_APPROVAL) {
|
} else if (feature === SlackTriggerFeature.SECRET_APPROVAL) {
|
||||||
targetChannels = slackIntegration.secretRequestChannels?.split(", ") || [];
|
targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || [];
|
||||||
if (!targetChannels.length || !slackIntegration.isSecretRequestNotificationEnabled) {
|
if (!targetChannelIds.length || !slackIntegration.isSecretRequestNotificationEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,35 +85,16 @@ export const triggerSlackNotification = async ({
|
|||||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||||
}).toString("utf8");
|
}).toString("utf8");
|
||||||
|
|
||||||
const targetChannelSet = new Set<string>(targetChannels);
|
|
||||||
const slackWebClient = new WebClient(botKey);
|
const slackWebClient = new WebClient(botKey);
|
||||||
const channelIdsToSendNotif: string[] = [];
|
|
||||||
let cursor;
|
|
||||||
|
|
||||||
do {
|
for await (const conversationId of targetChannelIds) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const response = await slackWebClient.conversations.list({
|
|
||||||
cursor,
|
|
||||||
limit: 1000,
|
|
||||||
types: "public_channel,private_channel"
|
|
||||||
});
|
|
||||||
|
|
||||||
response.channels?.forEach((channel) => {
|
|
||||||
if (channel.name_normalized && targetChannelSet.has(channel.name_normalized)) {
|
|
||||||
channelIdsToSendNotif.push(channel.id as string);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set the cursor for the next page
|
|
||||||
cursor = response.response_metadata?.next_cursor;
|
|
||||||
} while (cursor); // Continue while there is a cursor
|
|
||||||
|
|
||||||
for await (const conversationId of channelIdsToSendNotif) {
|
|
||||||
// we send both text and blocks for compatibility with barebone clients
|
// we send both text and blocks for compatibility with barebone clients
|
||||||
await slackWebClient.chat.postMessage({
|
await slackWebClient.chat
|
||||||
channel: conversationId,
|
.postMessage({
|
||||||
text: payloadMessage,
|
channel: conversationId,
|
||||||
blocks: payloadBlocks
|
text: payloadMessage,
|
||||||
});
|
blocks: payloadBlocks
|
||||||
|
})
|
||||||
|
.catch((err) => void logger.error(err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
|
import { fetchSlackChannels } from "./slack-fns";
|
||||||
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
||||||
import {
|
import {
|
||||||
TCompleteSlackIntegrationDTO,
|
TCompleteSlackIntegrationDTO,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
TGetSlackInstallUrlDTO,
|
TGetSlackInstallUrlDTO,
|
||||||
TGetSlackIntegrationByIdDTO,
|
TGetSlackIntegrationByIdDTO,
|
||||||
TGetSlackIntegrationByOrgDTO,
|
TGetSlackIntegrationByOrgDTO,
|
||||||
|
TGetSlackIntegrationChannelsDTO,
|
||||||
TReinstallSlackIntegrationDTO,
|
TReinstallSlackIntegrationDTO,
|
||||||
TUpdateSlackIntegrationDTO
|
TUpdateSlackIntegrationDTO
|
||||||
} from "./slack-types";
|
} from "./slack-types";
|
||||||
@@ -285,6 +287,42 @@ export const slackServiceFactory = ({
|
|||||||
return slackIntegration;
|
return slackIntegration;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSlackIntegrationChannels = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id
|
||||||
|
}: TGetSlackIntegrationChannelsDTO) => {
|
||||||
|
const slackIntegration = await slackIntegrationDAL.findById(id);
|
||||||
|
if (!slackIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Slack integration not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
slackIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId: slackIntegration.orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const botKey = orgDataKeyDecryptor({
|
||||||
|
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||||
|
}).toString("utf8");
|
||||||
|
|
||||||
|
return fetchSlackChannels(botKey);
|
||||||
|
};
|
||||||
|
|
||||||
const updateSlackIntegration = async ({
|
const updateSlackIntegration = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@@ -352,6 +390,7 @@ export const slackServiceFactory = ({
|
|||||||
completeSlackIntegration,
|
completeSlackIntegration,
|
||||||
getSlackInstaller,
|
getSlackInstaller,
|
||||||
updateSlackIntegration,
|
updateSlackIntegration,
|
||||||
deleteSlackIntegration
|
deleteSlackIntegration,
|
||||||
|
getSlackIntegrationChannels
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export type TGetSlackIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
|||||||
|
|
||||||
export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetSlackIntegrationChannelsDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TUpdateSlackIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
export type TUpdateSlackIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
||||||
TOrgPermission,
|
TOrgPermission,
|
||||||
"orgId"
|
"orgId"
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ export {
|
|||||||
fetchSlackInstallUrl,
|
fetchSlackInstallUrl,
|
||||||
fetchSlackReinstallUrl,
|
fetchSlackReinstallUrl,
|
||||||
useGetSlackIntegrationById,
|
useGetSlackIntegrationById,
|
||||||
|
useGetSlackIntegrationChannels,
|
||||||
useGetSlackIntegrations
|
useGetSlackIntegrations
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export const useUpdateSlackIntegration = () => {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { orgId, id }) => {
|
onSuccess: (_, { orgId, id }) => {
|
||||||
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegration(id));
|
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackIntegration(id));
|
||||||
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegrations(orgId));
|
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackIntegrations(orgId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -36,8 +36,8 @@ export const useDeleteSlackIntegration = () => {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { orgId, id }) => {
|
onSuccess: (_, { orgId, id }) => {
|
||||||
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegration(id));
|
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackIntegration(id));
|
||||||
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegrations(orgId));
|
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackIntegrations(orgId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { SlackIntegration } from "./types";
|
import { SlackIntegration, SlackIntegrationChannel } from "./types";
|
||||||
|
|
||||||
export const workflowIntegrationKeys = {
|
export const workflowIntegrationKeys = {
|
||||||
getSlackWorkflowIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
|
getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
|
||||||
getSlackWorkflowIntegration: (id?: string) => [{ id }, "slack-workflow-integration"]
|
getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"],
|
||||||
|
getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchSlackInstallUrl = async ({
|
export const fetchSlackInstallUrl = async ({
|
||||||
@@ -54,16 +55,31 @@ export const fetchSlackIntegrationById = async (id?: string) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchSlackIntegrationChannels = async (id?: string) => {
|
||||||
|
const { data } = await apiRequest.get<SlackIntegrationChannel[]>(
|
||||||
|
`/api/v1/workflow-integrations/slack/${id}/channels`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetSlackIntegrations = (orgId?: string) =>
|
export const useGetSlackIntegrations = (orgId?: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: workflowIntegrationKeys.getSlackWorkflowIntegrations(orgId),
|
queryKey: workflowIntegrationKeys.getSlackIntegrations(orgId),
|
||||||
queryFn: () => fetchSlackIntegrations(),
|
queryFn: () => fetchSlackIntegrations(),
|
||||||
enabled: Boolean(orgId)
|
enabled: Boolean(orgId)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGetSlackIntegrationById = (id?: string) =>
|
export const useGetSlackIntegrationById = (id?: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: workflowIntegrationKeys.getSlackWorkflowIntegration(id),
|
queryKey: workflowIntegrationKeys.getSlackIntegration(id),
|
||||||
queryFn: () => fetchSlackIntegrationById(id),
|
queryFn: () => fetchSlackIntegrationById(id),
|
||||||
enabled: Boolean(id)
|
enabled: Boolean(id)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useGetSlackIntegrationChannels = (id?: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workflowIntegrationKeys.getSlackIntegrationChannels(id),
|
||||||
|
queryFn: () => fetchSlackIntegrationChannels(id),
|
||||||
|
enabled: Boolean(id)
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ export type SlackIntegration = {
|
|||||||
teamName: string;
|
teamName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SlackIntegrationChannel = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TUpdateSlackIntegrationDTO = {
|
export type TUpdateSlackIntegrationDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
|||||||
+116
-27
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -9,6 +11,10 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ContentLoader,
|
ContentLoader,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
@@ -18,6 +24,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
|
useGetSlackIntegrationChannels,
|
||||||
useGetSlackIntegrations,
|
useGetSlackIntegrations,
|
||||||
useGetWorkspaceSlackConfig,
|
useGetWorkspaceSlackConfig,
|
||||||
useUpdateProjectSlackConfig
|
useUpdateProjectSlackConfig
|
||||||
@@ -26,9 +33,9 @@ import {
|
|||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
slackIntegrationId: z.string(),
|
slackIntegrationId: z.string(),
|
||||||
isSecretRequestNotificationEnabled: z.boolean(),
|
isSecretRequestNotificationEnabled: z.boolean(),
|
||||||
secretRequestChannels: z.string().default(""),
|
secretRequestChannels: z.string().array(),
|
||||||
isAccessRequestNotificationEnabled: z.boolean(),
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
accessRequestChannels: z.string().default("")
|
accessRequestChannels: z.string().array()
|
||||||
});
|
});
|
||||||
|
|
||||||
type TSlackConfigForm = z.infer<typeof formSchema>;
|
type TSlackConfigForm = z.infer<typeof formSchema>;
|
||||||
@@ -51,9 +58,9 @@ export const WorkflowIntegrationTab = () => {
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
isAccessRequestNotificationEnabled: false,
|
isAccessRequestNotificationEnabled: false,
|
||||||
accessRequestChannels: "",
|
accessRequestChannels: [],
|
||||||
isSecretRequestNotificationEnabled: false,
|
isSecretRequestNotificationEnabled: false,
|
||||||
secretRequestChannels: ""
|
secretRequestChannels: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,6 +68,14 @@ export const WorkflowIntegrationTab = () => {
|
|||||||
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
||||||
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
||||||
|
|
||||||
|
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
|
||||||
|
const slackChannelIdToName = Object.fromEntries(
|
||||||
|
(slackChannels || []).map((channel) => [channel.id, channel.name])
|
||||||
|
);
|
||||||
|
const sortedSlackChannels = slackChannels?.sort((a, b) =>
|
||||||
|
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
const handleIntegrationSave = async (data: TSlackConfigForm) => {
|
const handleIntegrationSave = async (data: TSlackConfigForm) => {
|
||||||
if (!currentWorkspace) {
|
if (!currentWorkspace) {
|
||||||
return;
|
return;
|
||||||
@@ -68,7 +83,9 @@ export const WorkflowIntegrationTab = () => {
|
|||||||
|
|
||||||
await updateProjectSlackConfig({
|
await updateProjectSlackConfig({
|
||||||
workspaceId: currentWorkspace.id,
|
workspaceId: currentWorkspace.id,
|
||||||
...data
|
...data,
|
||||||
|
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
|
||||||
|
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -84,14 +101,27 @@ export const WorkflowIntegrationTab = () => {
|
|||||||
"isSecretRequestNotificationEnabled",
|
"isSecretRequestNotificationEnabled",
|
||||||
slackConfig.isSecretRequestNotificationEnabled
|
slackConfig.isSecretRequestNotificationEnabled
|
||||||
);
|
);
|
||||||
setValue("secretRequestChannels", slackConfig.secretRequestChannels);
|
|
||||||
setValue(
|
setValue(
|
||||||
"isAccessRequestNotificationEnabled",
|
"isAccessRequestNotificationEnabled",
|
||||||
slackConfig.isAccessRequestNotificationEnabled
|
slackConfig.isAccessRequestNotificationEnabled
|
||||||
);
|
);
|
||||||
setValue("accessRequestChannels", slackConfig.accessRequestChannels);
|
|
||||||
|
if (slackChannels) {
|
||||||
|
setValue(
|
||||||
|
"secretRequestChannels",
|
||||||
|
slackConfig.secretRequestChannels
|
||||||
|
.split(", ")
|
||||||
|
.filter((channel) => channel in slackChannelIdToName)
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
"accessRequestChannels",
|
||||||
|
slackConfig.accessRequestChannels
|
||||||
|
.split(", ")
|
||||||
|
.filter((channel) => channel in slackChannelIdToName)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [slackConfig]);
|
}, [slackConfig, slackChannels]);
|
||||||
|
|
||||||
if (isSlackConfigLoading) {
|
if (isSlackConfigLoading) {
|
||||||
return <ContentLoader />;
|
return <ContentLoader />;
|
||||||
@@ -173,19 +203,49 @@ export const WorkflowIntegrationTab = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="secretRequestChannels"
|
name="secretRequestChannels"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Slack channels"
|
label="Slack channels"
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
isRequired={false}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<DropdownMenu>
|
||||||
autoCorrect="off"
|
<DropdownMenuTrigger asChild>
|
||||||
spellCheck={false}
|
<Input
|
||||||
placeholder="general, bot"
|
isReadOnly
|
||||||
{...field}
|
value={value
|
||||||
/>
|
?.filter(Boolean)
|
||||||
|
.map((entry) => slackChannelIdToName[entry])
|
||||||
|
.join(", ")}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
||||||
|
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>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -211,20 +271,49 @@ export const WorkflowIntegrationTab = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="accessRequestChannels"
|
name="accessRequestChannels"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Slack channels"
|
label="Slack channels"
|
||||||
className="mt-0"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
isRequired={false}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<DropdownMenu>
|
||||||
autoCorrect="off"
|
<DropdownMenuTrigger asChild>
|
||||||
spellCheck={false}
|
<Input
|
||||||
placeholder="general, bot"
|
isReadOnly
|
||||||
{...field}
|
value={value
|
||||||
/>
|
?.filter(Boolean)
|
||||||
|
.map((entry) => slackChannelIdToName[entry])
|
||||||
|
.join(", ")}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
||||||
|
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>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user