From 604b0467f9369483726117004432b5ce7cfbca12 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 4 Sep 2024 00:34:03 +0800 Subject: [PATCH] feat: finalized integration selection in project settings --- backend/src/server/routes/index.ts | 7 +- .../src/server/routes/v1/project-router.ts | 79 +++++++ .../src/services/project/project-service.ts | 121 +++++++++- backend/src/services/project/project-types.ts | 10 + .../slack/project-slack-config-dal.ts | 11 + frontend/src/hooks/api/index.tsx | 1 - frontend/src/hooks/api/slack/index.ts | 2 - frontend/src/hooks/api/slack/mutation.tsx | 36 --- frontend/src/hooks/api/slack/queries.tsx | 29 --- frontend/src/hooks/api/slack/types.ts | 22 -- .../hooks/api/workflowIntegrations/index.ts | 6 +- .../api/workflowIntegrations/mutation.tsx | 24 +- .../hooks/api/workflowIntegrations/types.ts | 18 ++ frontend/src/hooks/api/workspace/index.tsx | 1 + frontend/src/hooks/api/workspace/queries.tsx | 19 +- .../ProjectSettingsPage.tsx | 6 +- .../components/NotificationSection/index.tsx | 1 - .../WorkflowIntegrationTab.tsx} | 209 ++++++++---------- .../WorkflowIntegrationSection/index.tsx | 1 + 19 files changed, 383 insertions(+), 220 deletions(-) create mode 100644 backend/src/services/slack/project-slack-config-dal.ts delete mode 100644 frontend/src/hooks/api/slack/index.ts delete mode 100644 frontend/src/hooks/api/slack/mutation.tsx delete mode 100644 frontend/src/hooks/api/slack/queries.tsx delete mode 100644 frontend/src/hooks/api/slack/types.ts delete mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/index.tsx rename frontend/src/views/Settings/ProjectSettingsPage/components/{NotificationSection/NotificationTab.tsx => WorkflowIntegrationSection/WorkflowIntegrationTab.tsx} (50%) create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/WorkflowIntegrationSection/index.tsx diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 8b9b8e329..ba69a5c51 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -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 { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal"; 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 { slackServiceFactory } from "@app/services/slack/slack-service"; import { TSmtpService } from "@app/services/smtp/smtp-service"; @@ -325,6 +326,7 @@ export const registerRoutes = async ( const kmsRootConfigDAL = kmsRootConfigDALFactory(db); const slackIntegrationDAL = slackIntegrationDALFactory(db); + const projectSlackConfigDAL = projectSlackConfigDALFactory(db); const permissionService = permissionServiceFactory({ permissionDAL, @@ -725,7 +727,9 @@ export const registerRoutes = async ( keyStore, kmsService, projectBotDAL, - certificateTemplateDAL + certificateTemplateDAL, + projectSlackConfigDAL, + slackIntegrationDAL }); const projectEnvService = projectEnvServiceFactory({ @@ -1156,7 +1160,6 @@ export const registerRoutes = async ( }); const slackService = slackServiceFactory({ - projectDAL, permissionService, kmsService, slackIntegrationDAL diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index a380eb934..dae8182a0 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -4,6 +4,7 @@ import { IntegrationsSchema, ProjectMembershipsSchema, ProjectRolesSchema, + ProjectSlackConfigsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -542,4 +543,82 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 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; + } + }); }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 3e8f7582e..5900ea351 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -34,6 +34,8 @@ import { TProjectUserMembershipRoleDALFactory } from "../project-membership/proj import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { getPredefinedRoles } from "../project-role/project-role-fns"; 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 { TProjectDALFactory } from "./project-dal"; import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns"; @@ -43,6 +45,7 @@ import { TDeleteProjectDTO, TGetProjectDTO, TGetProjectKmsKey, + TGetProjectSlackConfig, TListProjectAlertsDTO, TListProjectCasDTO, TListProjectCertificateTemplatesDTO, @@ -54,6 +57,7 @@ import { TUpdateProjectDTO, TUpdateProjectKmsDTO, TUpdateProjectNameDTO, + TUpdateProjectSlackConfig, TUpdateProjectVersionLimitDTO, TUpgradeProjectDTO } from "./project-types"; @@ -76,6 +80,8 @@ type TProjectServiceFactoryDep = { identityProjectMembershipRoleDAL: Pick; projectKeyDAL: Pick; projectMembershipDAL: Pick; + projectSlackConfigDAL: Pick; + slackIntegrationDAL: Pick; projectUserMembershipRoleDAL: Pick; certificateAuthorityDAL: Pick; certificateDAL: Pick; @@ -126,7 +132,9 @@ export const projectServiceFactory = ({ pkiAlertDAL, keyStore, kmsService, - projectBotDAL + projectBotDAL, + projectSlackConfigDAL, + slackIntegrationDAL }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -909,6 +917,113 @@ export const projectServiceFactory = ({ 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 { createProject, deleteProject, @@ -929,6 +1044,8 @@ export const projectServiceFactory = ({ updateProjectKmsKey, getProjectKmsBackup, loadProjectKmsBackup, - getProjectKmsKeys + getProjectKmsKeys, + getProjectSlackConfig, + updateProjectSlackConfig }; }; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index c0ef2579e..fbbd32d2b 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -123,3 +123,13 @@ export type TLoadProjectKmsBackupDTO = { export type TGetProjectKmsKey = TProjectPermission; export type TListProjectCertificateTemplatesDTO = TProjectPermission; + +export type TGetProjectSlackConfig = TProjectPermission; + +export type TUpdateProjectSlackConfig = { + slackIntegrationId: string; + isAccessRequestNotificationEnabled: boolean; + accessRequestChannels: string; + isSecretRequestNotificationEnabled: boolean; + secretRequestChannels: string; +} & TProjectPermission; diff --git a/backend/src/services/slack/project-slack-config-dal.ts b/backend/src/services/slack/project-slack-config-dal.ts new file mode 100644 index 000000000..2c08fdeb3 --- /dev/null +++ b/backend/src/services/slack/project-slack-config-dal.ts @@ -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; + +export const projectSlackConfigDALFactory = (db: TDbClient) => { + const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs); + + return projectSlackConfigOrm; +}; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 9607829f5..551822f09 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -38,7 +38,6 @@ export * from "./secretSharing"; export * from "./secretSnapshots"; export * from "./serverDetails"; export * from "./serviceTokens"; -export * from "./slack"; export * from "./ssoConfig"; export * from "./subscriptions"; export * from "./tags"; diff --git a/frontend/src/hooks/api/slack/index.ts b/frontend/src/hooks/api/slack/index.ts deleted file mode 100644 index e98ddce71..000000000 --- a/frontend/src/hooks/api/slack/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation"; -export { useGetSlackIntegrationByProject } from "./queries"; diff --git a/frontend/src/hooks/api/slack/mutation.tsx b/frontend/src/hooks/api/slack/mutation.tsx deleted file mode 100644 index 67f34d9a4..000000000 --- a/frontend/src/hooks/api/slack/mutation.tsx +++ /dev/null @@ -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)); - } - }); -}; diff --git a/frontend/src/hooks/api/slack/queries.tsx b/frontend/src/hooks/api/slack/queries.tsx deleted file mode 100644 index c59cf64f6..000000000 --- a/frontend/src/hooks/api/slack/queries.tsx +++ /dev/null @@ -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("/api/v1/slack", { - params: { - projectId: workspaceId - } - }); - - return data; -}; - -export const useGetSlackIntegrationByProject = (workspaceId?: string) => - useQuery({ - queryKey: slackKeys.getSlackIntegrationByProject(workspaceId), - queryFn: () => fetchSlackIntegrationByProject(workspaceId), - enabled: Boolean(workspaceId) - }); diff --git a/frontend/src/hooks/api/slack/types.ts b/frontend/src/hooks/api/slack/types.ts deleted file mode 100644 index 9adad3f1b..000000000 --- a/frontend/src/hooks/api/slack/types.ts +++ /dev/null @@ -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; -}; diff --git a/frontend/src/hooks/api/workflowIntegrations/index.ts b/frontend/src/hooks/api/workflowIntegrations/index.ts index 8d0529b2e..9de1406fc 100644 --- a/frontend/src/hooks/api/workflowIntegrations/index.ts +++ b/frontend/src/hooks/api/workflowIntegrations/index.ts @@ -1,4 +1,8 @@ -export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation"; +export { + useDeleteSlackIntegration, + useUpdateProjectSlackConfig, + useUpdateSlackIntegration +} from "./mutation"; export { fetchSlackInstallUrl, useGetSlackIntegrationById, diff --git a/frontend/src/hooks/api/workflowIntegrations/mutation.tsx b/frontend/src/hooks/api/workflowIntegrations/mutation.tsx index 0979e423c..6d779154f 100644 --- a/frontend/src/hooks/api/workflowIntegrations/mutation.tsx +++ b/frontend/src/hooks/api/workflowIntegrations/mutation.tsx @@ -2,8 +2,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { workspaceKeys } from "../workspace/queries"; import { workflowIntegrationKeys } from "./queries"; -import { TDeleteSlackIntegrationDTO, TUpdateSlackIntegrationDTO } from "./types"; +import { + TDeleteSlackIntegrationDTO, + TUpdateProjectSlackConfigDTO, + TUpdateSlackIntegrationDTO +} from "./types"; export const useUpdateSlackIntegration = () => { 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)); + } + }); +}; diff --git a/frontend/src/hooks/api/workflowIntegrations/types.ts b/frontend/src/hooks/api/workflowIntegrations/types.ts index 5fb1868e2..b56fdb23d 100644 --- a/frontend/src/hooks/api/workflowIntegrations/types.ts +++ b/frontend/src/hooks/api/workflowIntegrations/types.ts @@ -20,3 +20,21 @@ export type TDeleteSlackIntegrationDTO = { id: 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; +}; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index 3f320d0d9..e3c886258 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -22,6 +22,7 @@ export { useGetWorkspaceIndexStatus, useGetWorkspaceIntegrations, useGetWorkspaceSecrets, + useGetWorkspaceSlackConfig, useGetWorkspaceUsers, useListWorkspaceCas, useListWorkspaceCertificates, diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 9ca3bbf34..0557e5f6f 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -16,6 +16,7 @@ import { TPkiCollection } from "../pkiCollections/types"; import { EncryptedSecret } from "../secrets/types"; import { userKeys } from "../users/queries"; import { TWorkspaceUser } from "../users/types"; +import { ProjectSlackConfig } from "../workflowIntegrations/types"; import { CreateEnvironmentDTO, CreateWorkspaceDTO, @@ -71,7 +72,9 @@ export const workspaceKeys = { getWorkspacePkiCollections: (workspaceId: string) => [{ workspaceId }, "workspace-pki-collections"] as const, 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) => { @@ -667,3 +670,17 @@ export const useListWorkspaceCertificateTemplates = ({ workspaceId }: { workspac enabled: Boolean(workspaceId) }); }; + +export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/workspace/${workspaceId}/slack-config` + ); + + return data; + }, + enabled: Boolean(workspaceId) + }); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx index 4c15e8504..f24055278 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx @@ -6,9 +6,9 @@ import { useWorkspace } from "@app/context"; import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { EncryptionTab } from "./components/EncryptionTab"; -import { NotificationTab } from "./components/NotificationSection"; import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; import { WebhooksTab } from "./components/WebhooksTab"; +import { WorkflowIntegrationTab } from "./components/WorkflowIntegrationSection"; export const ProjectSettingsPage = () => { const { t } = useTranslation(); @@ -20,7 +20,7 @@ export const ProjectSettingsPage = () => { key: "tab-project-encryption", isHidden: currentWorkspace?.version !== ProjectVersion.V3 }, - { name: "Notification", key: "tab-project-notification" }, + { name: "Workflow Integrations", key: "tab-workflow-integrations" }, { name: "Webhooks", key: "tab-project-webhooks" } ]; @@ -59,7 +59,7 @@ export const ProjectSettingsPage = () => { )} - + diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/index.tsx deleted file mode 100644 index aafa62ff5..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from "./NotificationTab"; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/NotificationTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx similarity index 50% rename from frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/NotificationTab.tsx rename to frontend/src/views/Settings/ProjectSettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx index 3a6ae45fd..1bb302bde 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/NotificationTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx @@ -1,45 +1,45 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; -import { useRouter } from "next/router"; +import Link from "next/link"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, ContentLoader, - DeleteActionModal, + EmptyState, FormControl, Input, + Select, + SelectItem, Switch } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { usePopUp, useToggle } from "@app/hooks"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { - fetchSlackInstallUrl, - useDeleteSlackIntegration, - useGetSlackIntegrationByProject, - useUpdateSlackIntegration + useGetSlackIntegrations, + useGetWorkspaceSlackConfig, + useUpdateProjectSlackConfig } from "@app/hooks/api"; const formSchema = z.object({ + slackIntegrationId: z.string(), isSecretRequestNotificationEnabled: z.boolean(), - secretRequestChannels: z.string(), + secretRequestChannels: z.string().default(""), isAccessRequestNotificationEnabled: z.boolean(), - accessRequestChannels: z.string() + accessRequestChannels: z.string().default("") }); -type TSlackIntegrationForm = z.infer; +type TSlackConfigForm = z.infer; -export const NotificationTab = () => { +export const WorkflowIntegrationTab = () => { const { currentWorkspace } = useWorkspace(); - const { data: slackIntegration, isLoading: isSlackIntegrationLoading } = - useGetSlackIntegrationByProject(currentWorkspace?.id); - const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration(); - const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration(); - const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ - "deleteSlackIntegration" - ] as const); + const { data: slackConfig, isLoading: isSlackConfigLoading } = useGetWorkspaceSlackConfig({ + workspaceId: currentWorkspace?.id ?? "" + }); + const { data: slackIntegrations } = useGetSlackIntegrations(currentWorkspace?.orgId); + const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectSlackConfig(); const { control, @@ -47,29 +47,27 @@ export const NotificationTab = () => { handleSubmit, setValue, formState: { isDirty, isSubmitting } - } = useForm({ + } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - isSecretRequestNotificationEnabled: slackIntegration?.isSecretRequestNotificationEnabled, - secretRequestChannels: slackIntegration?.secretRequestChannels || "", - isAccessRequestNotificationEnabled: slackIntegration?.isAccessRequestNotificationEnabled, - accessRequestChannels: slackIntegration?.accessRequestChannels || "" + isAccessRequestNotificationEnabled: false, + accessRequestChannels: "", + isSecretRequestNotificationEnabled: false, + secretRequestChannels: "" } }); - const router = useRouter(); - const [isConnectToSlackLoading, setIsConnectToSlackLoading] = useToggle(false); - const [isReinstallLoading, setIsReinstallLoading] = useToggle(false); const secretRequestNotifState = watch("isSecretRequestNotificationEnabled"); + const selectedSlackIntegrationId = watch("slackIntegrationId"); const accessRequestNotifState = watch("isAccessRequestNotificationEnabled"); - const handleIntegrationSave = async (data: TSlackIntegrationForm) => { - if (!currentWorkspace || !slackIntegration) { + const handleIntegrationSave = async (data: TSlackConfigForm) => { + if (!currentWorkspace) { return; } - await updateSlackIntegration({ - workspaceId: currentWorkspace?.id, - id: slackIntegration?.id, + + await updateProjectSlackConfig({ + workspaceId: currentWorkspace.id, ...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(() => { - if (slackIntegration) { + if (slackConfig) { + setValue("slackIntegrationId", slackConfig.slackIntegrationId); setValue( "isSecretRequestNotificationEnabled", - slackIntegration.isSecretRequestNotificationEnabled + slackConfig.isSecretRequestNotificationEnabled ); - setValue("secretRequestChannels", slackIntegration.secretRequestChannels); + setValue("secretRequestChannels", slackConfig.secretRequestChannels); setValue( "isAccessRequestNotificationEnabled", - slackIntegration.isAccessRequestNotificationEnabled + slackConfig.isAccessRequestNotificationEnabled ); - setValue("accessRequestChannels", slackIntegration.accessRequestChannels); + setValue("accessRequestChannels", slackConfig.accessRequestChannels); } - }, [slackIntegration]); + }, [slackConfig]); - if (isSlackIntegrationLoading) { + if (isSlackConfigLoading) { return ; } - return ( - <> -
-
-

- Slack Integration -

+ return !slackIntegrations?.length ? ( + + +
+ Create one now
-

- This integration allows you to send notifications to your Slack workspace in response to - events in your project. -

- {!slackIntegration && ( - - )} - {slackIntegration && ( -
-
Connected Slack workspace: {slackIntegration.teamName}
-
- - -
+ + + ) : ( +
+
+

Slack Integration

+
+

+ This integration allows you to send notifications to your Slack workspace in response to + events in your project. +

+ +
+ + {(isAllowed) => ( + ( + + + + )} + control={control} + name="slackIntegrationId" + /> + )} + +
+ {selectedSlackIntegrationId && ( + <>

Events

{ > Save - + )} -
- handlePopUpToggle("deleteSlackIntegration", isOpen)} - deleteKey="confirm" - onDeleteApproved={handleIntegrationDelete} - /> - + +
); }; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/WorkflowIntegrationSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/WorkflowIntegrationSection/index.tsx new file mode 100644 index 000000000..937daaed5 --- /dev/null +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/WorkflowIntegrationSection/index.tsx @@ -0,0 +1 @@ +export * from "./WorkflowIntegrationTab";