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
-1
View File
@@ -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";
-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 {
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,
+18 -1
View File
@@ -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";