diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 5dceba784..527e74fe2 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -33,15 +33,30 @@ const createCspHeader = (isEmbed = false) => { // Frame sources "frame-src 'self' https://vercel.live", // Connect sources for API calls - `connect-src 'self' ${localDomains} https://*.vercel.app https://*.supabase.co wss://*.supabase.co https://*.posthog.com ${apiUrl} ${api2Url} ${profilePictureURL}` - .replace(/\s+/g, ' ') - .trim(), + (() => { + const connectSources = [ + "'self'", + localDomains, + 'https://*.vercel.app', + 'https://*.supabase.co', + 'wss://*.supabase.co', + 'https://*.posthog.com', + 'https://*.slack.com', + apiUrl, + api2Url, + profilePictureURL + ] + .map((source) => source.replace(/\s+/g, ' ').trim()) + .filter(Boolean); + + return `connect-src ${connectSources.join(' ')}`; + })(), // Media "media-src 'self'", // Object "object-src 'none'", // Form actions - "form-action 'self'", + "form-action 'self' https://*.slack.com", // Base URI "base-uri 'self'", // Manifest diff --git a/apps/web/src/api/buster_rest/slack/queryRequests.ts b/apps/web/src/api/buster_rest/slack/queryRequests.ts index 9403d0673..6fcd7f7fb 100644 --- a/apps/web/src/api/buster_rest/slack/queryRequests.ts +++ b/apps/web/src/api/buster_rest/slack/queryRequests.ts @@ -1,11 +1,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { queryKeys } from '@/api/query_keys'; +import { slackQueryKeys } from '@/api/query_keys/slack'; import { useBusterNotifications } from '@/context/BusterNotifications'; import { useMemoizedFn } from '@/hooks'; -import type { - InitiateOAuthRequest, - UpdateIntegrationRequest -} from '@buster/server-shared/slack'; import { initiateSlackOAuth, getSlackIntegration, @@ -17,7 +13,7 @@ import { // GET /api/v2/slack/integration export const useGetSlackIntegration = (enabled = true) => { return useQuery({ - ...queryKeys.slackGetIntegration, + ...slackQueryKeys.slackGetIntegration, queryFn: getSlackIntegration, enabled }); @@ -26,7 +22,7 @@ export const useGetSlackIntegration = (enabled = true) => { // GET /api/v2/slack/channels export const useGetSlackChannels = (enabled = true) => { return useQuery({ - ...queryKeys.slackGetChannels, + ...slackQueryKeys.slackGetChannels, queryFn: getSlackChannels, enabled }); @@ -34,21 +30,35 @@ export const useGetSlackChannels = (enabled = true) => { // POST /api/v2/slack/auth/init export const useInitiateSlackOAuth = () => { + const { openErrorNotification } = useBusterNotifications(); + const mutationFn = useMemoizedFn(async () => { + const result = await initiateSlackOAuth(); + if (result.auth_url) { + window.location.href = result.auth_url; + } else { + openErrorNotification({ + title: 'Failed to initiate Slack OAuth', + description: 'Please try again later', + duration: 5000 + }); + } + }); + return useMutation({ - mutationFn: initiateSlackOAuth + mutationFn }); }; // PUT /api/v2/slack/integration export const useUpdateSlackIntegration = () => { const queryClient = useQueryClient(); - + return useMutation({ mutationFn: updateSlackIntegration, onSuccess: () => { // Invalidate the integration query to refetch the updated data queryClient.invalidateQueries({ - queryKey: queryKeys.slackGetIntegration.queryKey, + queryKey: slackQueryKeys.slackGetIntegration.queryKey, refetchType: 'all' }); } @@ -60,37 +70,38 @@ export const useRemoveSlackIntegration = () => { const queryClient = useQueryClient(); const { openConfirmModal } = useBusterNotifications(); - const mutationFn = useMemoizedFn( - async ({ ignoreConfirm = false }: { ignoreConfirm?: boolean } = {}) => { - const method = async () => { - return await removeSlackIntegration(); - }; + const mutationFn = useMemoizedFn(async () => { + const ignoreConfirm = false; - if (ignoreConfirm) { - return method(); - } + const method = async () => { + return await removeSlackIntegration(); + }; - return openConfirmModal({ - title: 'Remove Slack Integration', - content: 'Are you sure you want to remove the Slack integration? This will disconnect your workspace from Slack.', - primaryButtonProps: { - text: 'Remove' - }, - onOk: method - }); + if (ignoreConfirm) { + return method(); } - ); + + return openConfirmModal({ + title: 'Remove Slack Integration', + content: + 'Are you sure you want to remove the Slack integration? This will disconnect your workspace from Slack.', + primaryButtonProps: { + text: 'Remove' + }, + onOk: method + }); + }); return useMutation({ mutationFn, onSuccess: () => { // Invalidate both integration and channels queries queryClient.invalidateQueries({ - queryKey: queryKeys.slackGetIntegration.queryKey, + queryKey: slackQueryKeys.slackGetIntegration.queryKey, refetchType: 'all' }); queryClient.invalidateQueries({ - queryKey: queryKeys.slackGetChannels.queryKey, + queryKey: slackQueryKeys.slackGetChannels.queryKey, refetchType: 'all' }); } diff --git a/apps/web/src/api/buster_rest/slack/request.ts b/apps/web/src/api/buster_rest/slack/request.ts index 5accad851..6742e6c54 100644 --- a/apps/web/src/api/buster_rest/slack/request.ts +++ b/apps/web/src/api/buster_rest/slack/request.ts @@ -10,7 +10,7 @@ import type { } from '@buster/server-shared/slack'; // POST /api/v2/slack/auth/init -export const initiateSlackOAuth = async (data: InitiateOAuthRequest) => { +export const initiateSlackOAuth = async (data?: InitiateOAuthRequest) => { return mainApiV2.post('/slack/auth/init', data).then((res) => res.data); }; @@ -26,7 +26,9 @@ export const removeSlackIntegration = async () => { // PUT /api/v2/slack/integration export const updateSlackIntegration = async (data: UpdateIntegrationRequest) => { - return mainApiV2.put('/slack/integration', data).then((res) => res.data); + return mainApiV2 + .put('/slack/integration', data) + .then((res) => res.data); }; // GET /api/v2/slack/channels diff --git a/apps/web/src/components/features/integrations/SlackIntegrations.tsx b/apps/web/src/components/features/integrations/SlackIntegrations.tsx index d546b2395..3b157ae4d 100644 --- a/apps/web/src/components/features/integrations/SlackIntegrations.tsx +++ b/apps/web/src/components/features/integrations/SlackIntegrations.tsx @@ -1,15 +1,37 @@ +'use client'; + import React from 'react'; import { SettingsCards } from '../settings/SettingsCard'; import { SlackIcon } from '@/components/ui/icons/customIcons/SlackIcon'; import { Text } from '@/components/ui/typography'; import { Button } from '@/components/ui/buttons'; +import { + useGetSlackChannels, + useGetSlackIntegration, + useInitiateSlackOAuth, + useRemoveSlackIntegration, + useUpdateSlackIntegration +} from '@/api/buster_rest/slack/queryRequests'; +import { Dropdown, type DropdownItems } from '@/components/ui/dropdown'; +import { LinkSlash, Refresh2 } from '@/components/ui/icons'; +import { Select } from '@/components/ui/select'; export const SlackIntegrations = React.memo(() => { + const { data: slackIntegration } = useGetSlackIntegration(); + const isConnected = slackIntegration?.connected ?? false; + return ( ] }]} + cards={[ + { + sections: [ + , + isConnected && + ].filter(Boolean) + } + ]} /> ); }); @@ -17,6 +39,11 @@ export const SlackIntegrations = React.memo(() => { SlackIntegrations.displayName = 'SlackIntegrations'; const ConnectSlackCard = React.memo(() => { + const { data: slackIntegration } = useGetSlackIntegration(); + const { mutate: initiateSlackOAuth } = useInitiateSlackOAuth(); + + const isConnected = slackIntegration?.connected; + return (
@@ -30,9 +57,110 @@ const ConnectSlackCard = React.memo(() => {
- + + {isConnected ? ( + + ) : ( + + )} ); }); + +ConnectSlackCard.displayName = 'ConnectSlackCard'; + +const ConnectedDropdown = React.memo(() => { + const { mutate: removeSlackIntegration, isPending } = useRemoveSlackIntegration(); + + const dropdownItems: DropdownItems = [ + { + value: 'disconnect', + label: 'Disconnect', + icon: , + onClick: () => { + removeSlackIntegration(); + }, + loading: isPending + } + ]; + + return ( + +
+
+ Connected +
+ + ); +}); + +ConnectedDropdown.displayName = 'ConnectedDropdown'; + +const ConnectedSlackChannels = React.memo(() => { + const { data: slackIntegration, isLoading: isLoadingSlackIntegration } = useGetSlackIntegration(); + const { + data: slackChannelsData, + isLoading: isLoadingSlackChannels, + refetch: refetchSlackChannels, + isFetched: isFetchedSlackChannels, + error: slackChannelsError + } = useGetSlackChannels(); + const { mutate: updateSlackIntegration } = useUpdateSlackIntegration(); + + const channels = slackChannelsData?.channels || []; + const selectedChannelId = slackIntegration?.integration?.default_channel?.id; + + return ( +
+
+ Alerts channels + + Select which slack channel Buster should send alerts to + +
+
+ {!slackChannelsError ? ( + <> + {isFetchedSlackChannels && ( + + )} + +