feat: finalized integration selection in project settings

This commit is contained in:
Sheen Capadngan
2024-09-04 00:34:03 +08:00
parent a2b555dd81
commit 604b0467f9
19 changed files with 383 additions and 220 deletions
+5 -2
View File
@@ -182,6 +182,7 @@ import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/
import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal"; import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service"; import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal"; import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
import { slackServiceFactory } from "@app/services/slack/slack-service"; import { slackServiceFactory } from "@app/services/slack/slack-service";
import { TSmtpService } from "@app/services/smtp/smtp-service"; import { TSmtpService } from "@app/services/smtp/smtp-service";
@@ -325,6 +326,7 @@ export const registerRoutes = async (
const kmsRootConfigDAL = kmsRootConfigDALFactory(db); const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
const slackIntegrationDAL = slackIntegrationDALFactory(db); const slackIntegrationDAL = slackIntegrationDALFactory(db);
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDAL, permissionDAL,
@@ -725,7 +727,9 @@ export const registerRoutes = async (
keyStore, keyStore,
kmsService, kmsService,
projectBotDAL, projectBotDAL,
certificateTemplateDAL certificateTemplateDAL,
projectSlackConfigDAL,
slackIntegrationDAL
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@@ -1156,7 +1160,6 @@ export const registerRoutes = async (
}); });
const slackService = slackServiceFactory({ const slackService = slackServiceFactory({
projectDAL,
permissionService, permissionService,
kmsService, kmsService,
slackIntegrationDAL slackIntegrationDAL
@@ -4,6 +4,7 @@ import {
IntegrationsSchema, IntegrationsSchema,
ProjectMembershipsSchema, ProjectMembershipsSchema,
ProjectRolesSchema, ProjectRolesSchema,
ProjectSlackConfigsSchema,
UserEncryptionKeysSchema, UserEncryptionKeysSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
@@ -542,4 +543,82 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return { serviceTokenData }; return { serviceTokenData };
} }
}); });
server.route({
method: "GET",
url: "/:workspaceId/slack-config",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: ProjectSlackConfigsSchema.pick({
id: true,
slackIntegrationId: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const slackConfig = await server.services.project.getProjectSlackConfig({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
return slackConfig;
}
});
server.route({
method: "PUT",
url: "/:workspaceId/slack-config",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
slackIntegrationId: z.string(),
isAccessRequestNotificationEnabled: z.boolean(),
accessRequestChannels: z.string(),
isSecretRequestNotificationEnabled: z.boolean(),
secretRequestChannels: z.string()
}),
response: {
200: ProjectSlackConfigsSchema.pick({
id: true,
slackIntegrationId: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const slackConfig = await server.services.project.updateProjectSlackConfig({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
...req.body
});
return slackConfig;
}
});
}; };
+119 -2
View File
@@ -34,6 +34,8 @@ import { TProjectUserMembershipRoleDALFactory } from "../project-membership/proj
import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { getPredefinedRoles } from "../project-role/project-role-fns"; import { getPredefinedRoles } from "../project-role/project-role-fns";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal"; import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns"; import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
@@ -43,6 +45,7 @@ import {
TDeleteProjectDTO, TDeleteProjectDTO,
TGetProjectDTO, TGetProjectDTO,
TGetProjectKmsKey, TGetProjectKmsKey,
TGetProjectSlackConfig,
TListProjectAlertsDTO, TListProjectAlertsDTO,
TListProjectCasDTO, TListProjectCasDTO,
TListProjectCertificateTemplatesDTO, TListProjectCertificateTemplatesDTO,
@@ -54,6 +57,7 @@ import {
TUpdateProjectDTO, TUpdateProjectDTO,
TUpdateProjectKmsDTO, TUpdateProjectKmsDTO,
TUpdateProjectNameDTO, TUpdateProjectNameDTO,
TUpdateProjectSlackConfig,
TUpdateProjectVersionLimitDTO, TUpdateProjectVersionLimitDTO,
TUpgradeProjectDTO TUpgradeProjectDTO
} from "./project-types"; } from "./project-types";
@@ -76,6 +80,8 @@ type TProjectServiceFactoryDep = {
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">; identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">; certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
@@ -126,7 +132,9 @@ export const projectServiceFactory = ({
pkiAlertDAL, pkiAlertDAL,
keyStore, keyStore,
kmsService, kmsService,
projectBotDAL projectBotDAL,
projectSlackConfigDAL,
slackIntegrationDAL
}: TProjectServiceFactoryDep) => { }: TProjectServiceFactoryDep) => {
/* /*
* Create workspace. Make user the admin * Create workspace. Make user the admin
@@ -909,6 +917,113 @@ export const projectServiceFactory = ({
return { secretManagerKmsKey: kmsKey }; return { secretManagerKmsKey: kmsKey };
}; };
const getProjectSlackConfig = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId
}: TGetProjectSlackConfig) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: "Project not found"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
return projectSlackConfigDAL.findOne({
projectId: project.id
});
};
const updateProjectSlackConfig = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
}: TUpdateProjectSlackConfig) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: "Project not found"
});
}
const slackIntegration = await slackIntegrationDAL.findById(slackIntegrationId);
if (!slackIntegration) {
throw new NotFoundError({
message: "Slack integration not found"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
if (slackIntegration.orgId !== project.orgId) {
throw new BadRequestError({
message: "Selected slack integration is not in the same organization"
});
}
return projectSlackConfigDAL.transaction(async (tx) => {
const slackConfig = await projectSlackConfigDAL.findOne(
{
projectId
},
tx
);
if (slackConfig) {
return projectSlackConfigDAL.updateById(
slackConfig.id,
{
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
},
tx
);
}
return projectSlackConfigDAL.create(
{
projectId,
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
},
tx
);
});
};
return { return {
createProject, createProject,
deleteProject, deleteProject,
@@ -929,6 +1044,8 @@ export const projectServiceFactory = ({
updateProjectKmsKey, updateProjectKmsKey,
getProjectKmsBackup, getProjectKmsBackup,
loadProjectKmsBackup, loadProjectKmsBackup,
getProjectKmsKeys getProjectKmsKeys,
getProjectSlackConfig,
updateProjectSlackConfig
}; };
}; };
@@ -123,3 +123,13 @@ export type TLoadProjectKmsBackupDTO = {
export type TGetProjectKmsKey = TProjectPermission; export type TGetProjectKmsKey = TProjectPermission;
export type TListProjectCertificateTemplatesDTO = TProjectPermission; export type TListProjectCertificateTemplatesDTO = TProjectPermission;
export type TGetProjectSlackConfig = TProjectPermission;
export type TUpdateProjectSlackConfig = {
slackIntegrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
} & TProjectPermission;
@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectSlackConfigDALFactory = ReturnType<typeof projectSlackConfigDALFactory>;
export const projectSlackConfigDALFactory = (db: TDbClient) => {
const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs);
return projectSlackConfigOrm;
};
-1
View File
@@ -38,7 +38,6 @@ export * from "./secretSharing";
export * from "./secretSnapshots"; export * from "./secretSnapshots";
export * from "./serverDetails"; export * from "./serverDetails";
export * from "./serviceTokens"; export * from "./serviceTokens";
export * from "./slack";
export * from "./ssoConfig"; export * from "./ssoConfig";
export * from "./subscriptions"; export * from "./subscriptions";
export * from "./tags"; export * from "./tags";
-2
View File
@@ -1,2 +0,0 @@
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
export { useGetSlackIntegrationByProject } from "./queries";
-36
View File
@@ -1,36 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { slackKeys } from "./queries";
import { TDeleteSlackIntegrationDTO, TUpdateSlackIntegrationDTO } from "./types";
export const useUpdateSlackIntegration = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSlackIntegrationDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.patch(`/api/v1/slack/${dto.id}`, dto);
return data;
},
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(slackKeys.getSlackIntegrationByProject(workspaceId));
}
});
};
export const useDeleteSlackIntegration = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSlackIntegrationDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.delete(`/api/v1/slack/${dto.id}`);
return data;
},
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(slackKeys.getSlackIntegrationByProject(workspaceId));
}
});
};
-29
View File
@@ -1,29 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { ProjectSlackIntegration } from "./types";
export const slackKeys = {
getSlackIntegrationByProject: (workspaceId?: string) => [
{ workspaceId },
"slack-integration-by-project"
]
};
export const fetchSlackIntegrationByProject = async (workspaceId?: string) => {
const { data } = await apiRequest.get<ProjectSlackIntegration>("/api/v1/slack", {
params: {
projectId: workspaceId
}
});
return data;
};
export const useGetSlackIntegrationByProject = (workspaceId?: string) =>
useQuery({
queryKey: slackKeys.getSlackIntegrationByProject(workspaceId),
queryFn: () => fetchSlackIntegrationByProject(workspaceId),
enabled: Boolean(workspaceId)
});
-22
View File
@@ -1,22 +0,0 @@
export type ProjectSlackIntegration = {
id: string;
teamName: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
};
export type TUpdateSlackIntegrationDTO = {
id: string;
workspaceId: string;
isAccessRequestNotificationEnabled?: boolean;
accessRequestChannels?: string;
isSecretRequestNotificationEnabled?: boolean;
secretRequestChannels?: string;
};
export type TDeleteSlackIntegrationDTO = {
id: string;
workspaceId: string;
};
@@ -1,4 +1,8 @@
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation"; export {
useDeleteSlackIntegration,
useUpdateProjectSlackConfig,
useUpdateSlackIntegration
} from "./mutation";
export { export {
fetchSlackInstallUrl, fetchSlackInstallUrl,
useGetSlackIntegrationById, useGetSlackIntegrationById,
@@ -2,8 +2,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { workflowIntegrationKeys } from "./queries"; import { workflowIntegrationKeys } from "./queries";
import { TDeleteSlackIntegrationDTO, TUpdateSlackIntegrationDTO } from "./types"; import {
TDeleteSlackIntegrationDTO,
TUpdateProjectSlackConfigDTO,
TUpdateSlackIntegrationDTO
} from "./types";
export const useUpdateSlackIntegration = () => { export const useUpdateSlackIntegration = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -36,3 +41,20 @@ export const useDeleteSlackIntegration = () => {
} }
}); });
}; };
export const useUpdateProjectSlackConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: TUpdateProjectSlackConfigDTO) => {
const { data } = await apiRequest.put(
`/api/v1/workspace/${dto.workspaceId}/slack-config`,
dto
);
return data;
},
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceSlackConfig(workspaceId));
}
});
};
@@ -20,3 +20,21 @@ export type TDeleteSlackIntegrationDTO = {
id: string; id: string;
orgId: string; orgId: string;
}; };
export type ProjectSlackConfig = {
id: string;
slackIntegrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
};
export type TUpdateProjectSlackConfigDTO = {
workspaceId: string;
slackIntegrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
};
@@ -22,6 +22,7 @@ export {
useGetWorkspaceIndexStatus, useGetWorkspaceIndexStatus,
useGetWorkspaceIntegrations, useGetWorkspaceIntegrations,
useGetWorkspaceSecrets, useGetWorkspaceSecrets,
useGetWorkspaceSlackConfig,
useGetWorkspaceUsers, useGetWorkspaceUsers,
useListWorkspaceCas, useListWorkspaceCas,
useListWorkspaceCertificates, useListWorkspaceCertificates,
+18 -1
View File
@@ -16,6 +16,7 @@ import { TPkiCollection } from "../pkiCollections/types";
import { EncryptedSecret } from "../secrets/types"; import { EncryptedSecret } from "../secrets/types";
import { userKeys } from "../users/queries"; import { userKeys } from "../users/queries";
import { TWorkspaceUser } from "../users/types"; import { TWorkspaceUser } from "../users/types";
import { ProjectSlackConfig } from "../workflowIntegrations/types";
import { import {
CreateEnvironmentDTO, CreateEnvironmentDTO,
CreateWorkspaceDTO, CreateWorkspaceDTO,
@@ -71,7 +72,9 @@ export const workspaceKeys = {
getWorkspacePkiCollections: (workspaceId: string) => getWorkspacePkiCollections: (workspaceId: string) =>
[{ workspaceId }, "workspace-pki-collections"] as const, [{ workspaceId }, "workspace-pki-collections"] as const,
getWorkspaceCertificateTemplates: (workspaceId: string) => getWorkspaceCertificateTemplates: (workspaceId: string) =>
[{ workspaceId }, "workspace-certificate-templates"] as const [{ workspaceId }, "workspace-certificate-templates"] as const,
getWorkspaceSlackConfig: (workspaceId: string) =>
[{ workspaceId }, "workspace-slack-config"] as const
}; };
const fetchWorkspaceById = async (workspaceId: string) => { const fetchWorkspaceById = async (workspaceId: string) => {
@@ -667,3 +670,17 @@ export const useListWorkspaceCertificateTemplates = ({ workspaceId }: { workspac
enabled: Boolean(workspaceId) enabled: Boolean(workspaceId)
}); });
}; };
export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId),
queryFn: async () => {
const { data } = await apiRequest.get<ProjectSlackConfig>(
`/api/v1/workspace/${workspaceId}/slack-config`
);
return data;
},
enabled: Boolean(workspaceId)
});
};
@@ -6,9 +6,9 @@ import { useWorkspace } from "@app/context";
import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { EncryptionTab } from "./components/EncryptionTab"; import { EncryptionTab } from "./components/EncryptionTab";
import { NotificationTab } from "./components/NotificationSection";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { WebhooksTab } from "./components/WebhooksTab"; import { WebhooksTab } from "./components/WebhooksTab";
import { WorkflowIntegrationTab } from "./components/WorkflowIntegrationSection";
export const ProjectSettingsPage = () => { export const ProjectSettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -20,7 +20,7 @@ export const ProjectSettingsPage = () => {
key: "tab-project-encryption", key: "tab-project-encryption",
isHidden: currentWorkspace?.version !== ProjectVersion.V3 isHidden: currentWorkspace?.version !== ProjectVersion.V3
}, },
{ name: "Notification", key: "tab-project-notification" }, { name: "Workflow Integrations", key: "tab-workflow-integrations" },
{ name: "Webhooks", key: "tab-project-webhooks" } { name: "Webhooks", key: "tab-project-webhooks" }
]; ];
@@ -59,7 +59,7 @@ export const ProjectSettingsPage = () => {
</Tab.Panel> </Tab.Panel>
)} )}
<Tab.Panel> <Tab.Panel>
<NotificationTab /> <WorkflowIntegrationTab />
</Tab.Panel> </Tab.Panel>
<Tab.Panel> <Tab.Panel>
<WebhooksTab /> <WebhooksTab />
@@ -1 +0,0 @@
export * from "./NotificationTab";
@@ -1,45 +1,45 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router"; import Link from "next/link";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
ContentLoader, ContentLoader,
DeleteActionModal, EmptyState,
FormControl, FormControl,
Input, Input,
Select,
SelectItem,
Switch Switch
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import { import {
fetchSlackInstallUrl, useGetSlackIntegrations,
useDeleteSlackIntegration, useGetWorkspaceSlackConfig,
useGetSlackIntegrationByProject, useUpdateProjectSlackConfig
useUpdateSlackIntegration
} from "@app/hooks/api"; } from "@app/hooks/api";
const formSchema = z.object({ const formSchema = z.object({
slackIntegrationId: z.string(),
isSecretRequestNotificationEnabled: z.boolean(), isSecretRequestNotificationEnabled: z.boolean(),
secretRequestChannels: z.string(), secretRequestChannels: z.string().default(""),
isAccessRequestNotificationEnabled: z.boolean(), isAccessRequestNotificationEnabled: z.boolean(),
accessRequestChannels: z.string() accessRequestChannels: z.string().default("")
}); });
type TSlackIntegrationForm = z.infer<typeof formSchema>; type TSlackConfigForm = z.infer<typeof formSchema>;
export const NotificationTab = () => { export const WorkflowIntegrationTab = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: slackIntegration, isLoading: isSlackIntegrationLoading } = const { data: slackConfig, isLoading: isSlackConfigLoading } = useGetWorkspaceSlackConfig({
useGetSlackIntegrationByProject(currentWorkspace?.id); workspaceId: currentWorkspace?.id ?? ""
const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration(); });
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration(); const { data: slackIntegrations } = useGetSlackIntegrations(currentWorkspace?.orgId);
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectSlackConfig();
"deleteSlackIntegration"
] as const);
const { const {
control, control,
@@ -47,29 +47,27 @@ export const NotificationTab = () => {
handleSubmit, handleSubmit,
setValue, setValue,
formState: { isDirty, isSubmitting } formState: { isDirty, isSubmitting }
} = useForm<TSlackIntegrationForm>({ } = useForm<TSlackConfigForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
isSecretRequestNotificationEnabled: slackIntegration?.isSecretRequestNotificationEnabled, isAccessRequestNotificationEnabled: false,
secretRequestChannels: slackIntegration?.secretRequestChannels || "", accessRequestChannels: "",
isAccessRequestNotificationEnabled: slackIntegration?.isAccessRequestNotificationEnabled, isSecretRequestNotificationEnabled: false,
accessRequestChannels: slackIntegration?.accessRequestChannels || "" secretRequestChannels: ""
} }
}); });
const router = useRouter();
const [isConnectToSlackLoading, setIsConnectToSlackLoading] = useToggle(false);
const [isReinstallLoading, setIsReinstallLoading] = useToggle(false);
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled"); const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
const selectedSlackIntegrationId = watch("slackIntegrationId");
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled"); const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
const handleIntegrationSave = async (data: TSlackIntegrationForm) => { const handleIntegrationSave = async (data: TSlackConfigForm) => {
if (!currentWorkspace || !slackIntegration) { if (!currentWorkspace) {
return; return;
} }
await updateSlackIntegration({
workspaceId: currentWorkspace?.id, await updateProjectSlackConfig({
id: slackIntegration?.id, workspaceId: currentWorkspace.id,
...data ...data
}); });
@@ -79,96 +77,76 @@ export const NotificationTab = () => {
}); });
}; };
const handleIntegrationDelete = async () => {
if (!currentWorkspace || !slackIntegration) {
return;
}
await deleteSlackIntegration({
workspaceId: currentWorkspace.id,
id: slackIntegration.id
});
handlePopUpToggle("deleteSlackIntegration", false);
createNotification({
type: "success",
text: "Successfully deleted slack integration"
});
};
const triggerSlackInstall = async () => {
const slackInstallUrl = await fetchSlackInstallUrl(currentWorkspace?.id);
if (slackInstallUrl) {
router.push(slackInstallUrl);
}
};
useEffect(() => { useEffect(() => {
if (slackIntegration) { if (slackConfig) {
setValue("slackIntegrationId", slackConfig.slackIntegrationId);
setValue( setValue(
"isSecretRequestNotificationEnabled", "isSecretRequestNotificationEnabled",
slackIntegration.isSecretRequestNotificationEnabled slackConfig.isSecretRequestNotificationEnabled
); );
setValue("secretRequestChannels", slackIntegration.secretRequestChannels); setValue("secretRequestChannels", slackConfig.secretRequestChannels);
setValue( setValue(
"isAccessRequestNotificationEnabled", "isAccessRequestNotificationEnabled",
slackIntegration.isAccessRequestNotificationEnabled slackConfig.isAccessRequestNotificationEnabled
); );
setValue("accessRequestChannels", slackIntegration.accessRequestChannels); setValue("accessRequestChannels", slackConfig.accessRequestChannels);
} }
}, [slackIntegration]); }, [slackConfig]);
if (isSlackIntegrationLoading) { if (isSlackConfigLoading) {
return <ContentLoader />; return <ContentLoader />;
} }
return ( return !slackIntegrations?.length ? (
<> <EmptyState title="You do not have any integrations configured.">
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <Link href={`/org/${currentWorkspace?.orgId}/settings?tab=workflowIntegrations`}>
<div className="flex justify-between"> <div className="mt-2 underline decoration-primary-800 underline-offset-4 duration-200 hover:cursor-pointer hover:text-mineshaft-100 hover:decoration-primary-600">
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100"> Create one now
Slack Integration
</h2>
</div> </div>
<p className="mb-4 text-gray-400"> </Link>
This integration allows you to send notifications to your Slack workspace in response to </EmptyState>
events in your project. ) : (
</p> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
{!slackIntegration && ( <div className="flex justify-between">
<Button <h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Slack Integration</h2>
isLoading={isConnectToSlackLoading} </div>
onClick={async () => { <p className="mb-4 text-gray-400">
setIsConnectToSlackLoading.on(); This integration allows you to send notifications to your Slack workspace in response to
await triggerSlackInstall(); events in your project.
}} </p>
> <form onSubmit={handleSubmit(handleIntegrationSave)}>
Connect to Slack <div className="max-w-md">
</Button> <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
)} {(isAllowed) => (
{slackIntegration && ( <Controller
<form onSubmit={handleSubmit(handleIntegrationSave)}> render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<div>Connected Slack workspace: {slackIntegration.teamName}</div> <FormControl errorText={error?.message} isError={Boolean(error)}>
<div className="mt-2 mb-6"> <Select
<Button {...field}
colorSchema="secondary" isDisabled={!isAllowed}
size="xs" onValueChange={onChange}
isLoading={isReinstallLoading} defaultValue={slackConfig?.slackIntegrationId}
onClick={async () => { className="w-3/4 bg-mineshaft-600"
setIsReinstallLoading.on(); >
await triggerSlackInstall(); {slackIntegrations?.map((slackIntegration) => (
}} <SelectItem
> value={slackIntegration.id}
Reinstall key={`slack-integration-${slackIntegration.id}`}
</Button> >
<Button {slackIntegration.slug}
colorSchema="secondary" </SelectItem>
size="xs" ))}
className="ml-2" </Select>
onClick={() => handlePopUpOpen("deleteSlackIntegration")} </FormControl>
> )}
Delete control={control}
</Button> name="slackIntegrationId"
</div> />
)}
</ProjectPermissionCan>
</div>
{selectedSlackIntegrationId && (
<>
<h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2> <h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2>
<Controller <Controller
control={control} control={control}
@@ -260,16 +238,9 @@ export const NotificationTab = () => {
> >
Save Save
</Button> </Button>
</form> </>
)} )}
</div> </form>
<DeleteActionModal </div>
isOpen={popUp.deleteSlackIntegration.isOpen}
title="Are you sure want to delete your Slack integration?"
onChange={(isOpen) => handlePopUpToggle("deleteSlackIntegration", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleIntegrationDelete}
/>
</>
); );
}; };
@@ -0,0 +1 @@
export * from "./WorkflowIntegrationTab";