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.">
<Link href={`/org/${currentWorkspace?.orgId}/settings?tab=workflowIntegrations`}>
<div className="mt-2 underline decoration-primary-800 underline-offset-4 duration-200 hover:cursor-pointer hover:text-mineshaft-100 hover:decoration-primary-600">
Create one now
</div>
</Link>
</EmptyState>
) : (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between"> <div className="flex justify-between">
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100"> <h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Slack Integration</h2>
Slack Integration
</h2>
</div> </div>
<p className="mb-4 text-gray-400"> <p className="mb-4 text-gray-400">
This integration allows you to send notifications to your Slack workspace in response to This integration allows you to send notifications to your Slack workspace in response to
events in your project. events in your project.
</p> </p>
{!slackIntegration && (
<Button
isLoading={isConnectToSlackLoading}
onClick={async () => {
setIsConnectToSlackLoading.on();
await triggerSlackInstall();
}}
>
Connect to Slack
</Button>
)}
{slackIntegration && (
<form onSubmit={handleSubmit(handleIntegrationSave)}> <form onSubmit={handleSubmit(handleIntegrationSave)}>
<div>Connected Slack workspace: {slackIntegration.teamName}</div> <div className="max-w-md">
<div className="mt-2 mb-6"> <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
<Button {(isAllowed) => (
colorSchema="secondary" <Controller
size="xs" render={({ field: { onChange, ...field }, fieldState: { error } }) => (
isLoading={isReinstallLoading} <FormControl errorText={error?.message} isError={Boolean(error)}>
onClick={async () => { <Select
setIsReinstallLoading.on(); {...field}
await triggerSlackInstall(); isDisabled={!isAllowed}
}} onValueChange={onChange}
defaultValue={slackConfig?.slackIntegrationId}
className="w-3/4 bg-mineshaft-600"
> >
Reinstall {slackIntegrations?.map((slackIntegration) => (
</Button> <SelectItem
<Button value={slackIntegration.id}
colorSchema="secondary" key={`slack-integration-${slackIntegration.id}`}
size="xs"
className="ml-2"
onClick={() => handlePopUpOpen("deleteSlackIntegration")}
> >
Delete {slackIntegration.slug}
</Button> </SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="slackIntegrationId"
/>
)}
</ProjectPermissionCan>
</div> </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>
<DeleteActionModal
isOpen={popUp.deleteSlackIntegration.isOpen}
title="Are you sure want to delete your Slack integration?"
onChange={(isOpen) => handlePopUpToggle("deleteSlackIntegration", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleIntegrationDelete}
/>
</> </>
)}
</form>
</div>
); );
}; };
@@ -0,0 +1 @@
export * from "./WorkflowIntegrationTab";