mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: finalized integration selection in project settings
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<TIdentityProjectMembershipRoleDALFactory, "create">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
||||
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
11
backend/src/services/slack/project-slack-config-dal.ts
Normal file
11
backend/src/services/slack/project-slack-config-dal.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
|
||||
export { useGetSlackIntegrationByProject } from "./queries";
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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)
|
||||
});
|
||||
@@ -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 {
|
||||
fetchSlackInstallUrl,
|
||||
useGetSlackIntegrationById,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ export {
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceIntegrations,
|
||||
useGetWorkspaceSecrets,
|
||||
useGetWorkspaceSlackConfig,
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates,
|
||||
|
||||
@@ -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<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 { 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 = () => {
|
||||
</Tab.Panel>
|
||||
)}
|
||||
<Tab.Panel>
|
||||
<NotificationTab />
|
||||
<WorkflowIntegrationTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<WebhooksTab />
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./NotificationTab";
|
||||
@@ -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<typeof formSchema>;
|
||||
type TSlackConfigForm = z.infer<typeof formSchema>;
|
||||
|
||||
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<TSlackIntegrationForm>({
|
||||
} = useForm<TSlackConfigForm>({
|
||||
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 <ContentLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">
|
||||
Slack Integration
|
||||
</h2>
|
||||
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>
|
||||
<p className="mb-4 text-gray-400">
|
||||
This integration allows you to send notifications to your Slack workspace in response to
|
||||
events in your project.
|
||||
</p>
|
||||
{!slackIntegration && (
|
||||
<Button
|
||||
isLoading={isConnectToSlackLoading}
|
||||
onClick={async () => {
|
||||
setIsConnectToSlackLoading.on();
|
||||
await triggerSlackInstall();
|
||||
}}
|
||||
>
|
||||
Connect to Slack
|
||||
</Button>
|
||||
)}
|
||||
{slackIntegration && (
|
||||
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
||||
<div>Connected Slack workspace: {slackIntegration.teamName}</div>
|
||||
<div className="mt-2 mb-6">
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
size="xs"
|
||||
isLoading={isReinstallLoading}
|
||||
onClick={async () => {
|
||||
setIsReinstallLoading.on();
|
||||
await triggerSlackInstall();
|
||||
}}
|
||||
>
|
||||
Reinstall
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
size="xs"
|
||||
className="ml-2"
|
||||
onClick={() => handlePopUpOpen("deleteSlackIntegration")}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Link>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Slack Integration</h2>
|
||||
</div>
|
||||
<p className="mb-4 text-gray-400">
|
||||
This integration allows you to send notifications to your Slack workspace in response to
|
||||
events in your project.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
||||
<div className="max-w-md">
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
{...field}
|
||||
isDisabled={!isAllowed}
|
||||
onValueChange={onChange}
|
||||
defaultValue={slackConfig?.slackIntegrationId}
|
||||
className="w-3/4 bg-mineshaft-600"
|
||||
>
|
||||
{slackIntegrations?.map((slackIntegration) => (
|
||||
<SelectItem
|
||||
value={slackIntegration.id}
|
||||
key={`slack-integration-${slackIntegration.id}`}
|
||||
>
|
||||
{slackIntegration.slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="slackIntegrationId"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{selectedSlackIntegrationId && (
|
||||
<>
|
||||
<h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -260,16 +238,9 @@ export const NotificationTab = () => {
|
||||
>
|
||||
Save
|
||||
</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";
|
||||
Reference in New Issue
Block a user