diff --git a/ee/query-service/model/plans.go b/ee/query-service/model/plans.go index 2be68f1ea7..9f56f50655 100644 --- a/ee/query-service/model/plans.go +++ b/ee/query-service/model/plans.go @@ -60,6 +60,34 @@ var BasicPlan = basemodel.FeatureSet{ UsageLimit: 5, Route: "", }, + basemodel.Feature{ + Name: basemodel.AlertChannelSlack, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelWebhook, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelPagerduty, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelMsTeams, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, basemodel.Feature{ Name: basemodel.UseSpanMetrics, Active: false, @@ -112,6 +140,34 @@ var ProPlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: basemodel.AlertChannelSlack, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelWebhook, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelPagerduty, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelMsTeams, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, basemodel.Feature{ Name: basemodel.UseSpanMetrics, Active: false, @@ -164,6 +220,34 @@ var EnterprisePlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: basemodel.AlertChannelSlack, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelWebhook, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelPagerduty, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelMsTeams, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, basemodel.Feature{ Name: basemodel.UseSpanMetrics, Active: false, diff --git a/frontend/src/api/channels/createMsTeams.ts b/frontend/src/api/channels/createMsTeams.ts new file mode 100644 index 0000000000..9e06e275a0 --- /dev/null +++ b/frontend/src/api/channels/createMsTeams.ts @@ -0,0 +1,34 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/createMsTeams'; + +const create = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/channels', { + name: props.name, + msteams_configs: [ + { + send_resolved: true, + webhook_url: props.webhook_url, + title: props.title, + text: props.text, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default create; diff --git a/frontend/src/api/channels/editMsTeams.ts b/frontend/src/api/channels/editMsTeams.ts new file mode 100644 index 0000000000..ee6bd309c1 --- /dev/null +++ b/frontend/src/api/channels/editMsTeams.ts @@ -0,0 +1,34 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/editMsTeams'; + +const editMsTeams = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.put(`/channels/${props.id}`, { + name: props.name, + msteams_configs: [ + { + send_resolved: true, + webhook_url: props.webhook_url, + title: props.title, + text: props.text, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default editMsTeams; diff --git a/frontend/src/api/channels/testMsTeams.ts b/frontend/src/api/channels/testMsTeams.ts new file mode 100644 index 0000000000..3b4fc21b23 --- /dev/null +++ b/frontend/src/api/channels/testMsTeams.ts @@ -0,0 +1,34 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/createMsTeams'; + +const testMsTeams = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/testChannel', { + name: props.name, + msteams_configs: [ + { + send_resolved: true, + webhook_url: props.webhook_url, + title: props.title, + text: props.text, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default testMsTeams; diff --git a/frontend/src/components/Upgrade/UpgradePrompt.tsx b/frontend/src/components/Upgrade/UpgradePrompt.tsx new file mode 100644 index 0000000000..9281530056 --- /dev/null +++ b/frontend/src/components/Upgrade/UpgradePrompt.tsx @@ -0,0 +1,31 @@ +import { Alert, Space } from 'antd'; +import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app'; + +type UpgradePromptProps = { + title?: string; +}; + +function UpgradePrompt({ title }: UpgradePromptProps): JSX.Element { + return ( + + + This feature is available for paid plans only.{' '} + + Click here + {' '} + to Upgrade + + } + type="warning" + />{' '} + + ); +} + +UpgradePrompt.defaultProps = { + title: 'Upgrade to a Paid Plan', +}; +export default UpgradePrompt; diff --git a/frontend/src/constants/features.ts b/frontend/src/constants/features.ts index c5f4a1e7b8..0d444ff3f0 100644 --- a/frontend/src/constants/features.ts +++ b/frontend/src/constants/features.ts @@ -1,6 +1,12 @@ // keep this consistent with backend constants.go export enum FeatureKeys { SSO = 'SSO', + ENTERPRISE_PLAN = 'ENTERPRISE_PLAN', + BASIC_PLAN = 'BASIC_PLAN', + ALERT_CHANNEL_SLACK = 'ALERT_CHANNEL_SLACK', + ALERT_CHANNEL_WEBHOOK = 'ALERT_CHANNEL_WEBHOOK', + ALERT_CHANNEL_PAGERDUTY = 'ALERT_CHANNEL_PAGERDUTY', + ALERT_CHANNEL_MSTEAMS = 'ALERT_CHANNEL_MSTEAMS', DurationSort = 'DurationSort', TimestampSort = 'TimestampSort', SMART_TRACE_DETAIL = 'SMART_TRACE_DETAIL', @@ -9,4 +15,5 @@ export enum FeatureKeys { QUERY_BUILDER_ALERTS = 'QUERY_BUILDER_ALERTS', DISABLE_UPSELL = 'DISABLE_UPSELL', USE_SPAN_METRICS = 'USE_SPAN_METRICS', + OSS = 'OSS', } diff --git a/frontend/src/container/CreateAlertChannels/config.ts b/frontend/src/container/CreateAlertChannels/config.ts index 6c89764637..634dc95c41 100644 --- a/frontend/src/container/CreateAlertChannels/config.ts +++ b/frontend/src/container/CreateAlertChannels/config.ts @@ -63,10 +63,16 @@ export const ValidatePagerChannel = (p: PagerChannel): string => { return ''; }; -export type ChannelType = 'slack' | 'email' | 'webhook' | 'pagerduty'; +export type ChannelType = + | 'slack' + | 'email' + | 'webhook' + | 'pagerduty' + | 'msteams'; export const SlackType: ChannelType = 'slack'; export const WebhookType: ChannelType = 'webhook'; export const PagerType: ChannelType = 'pagerduty'; +export const MsTeamsType: ChannelType = 'msteams'; // LabelFilterStatement will be used for preparing filter conditions / matchers export interface LabelFilterStatement { @@ -81,3 +87,9 @@ export interface LabelFilterStatement { // filter value value: string; } + +export interface MsTeamsChannel extends Channel { + webhook_url?: string; + title?: string; + text?: string; +} diff --git a/frontend/src/container/CreateAlertChannels/index.tsx b/frontend/src/container/CreateAlertChannels/index.tsx index 1261f53567..70fe3fb7c6 100644 --- a/frontend/src/container/CreateAlertChannels/index.tsx +++ b/frontend/src/container/CreateAlertChannels/index.tsx @@ -1,7 +1,9 @@ import { Form } from 'antd'; +import createMsTeamsApi from 'api/channels/createMsTeams'; import createPagerApi from 'api/channels/createPager'; import createSlackApi from 'api/channels/createSlack'; import createWebhookApi from 'api/channels/createWebhook'; +import testMsTeamsApi from 'api/channels/testMsTeams'; import testPagerApi from 'api/channels/testPager'; import testSlackApi from 'api/channels/testSlack'; import testWebhookApi from 'api/channels/testWebhook'; @@ -14,6 +16,8 @@ import { useTranslation } from 'react-i18next'; import { ChannelType, + MsTeamsChannel, + MsTeamsType, PagerChannel, PagerType, SlackChannel, @@ -33,7 +37,7 @@ function CreateAlertChannels({ const [formInstance] = Form.useForm(); const [selectedConfig, setSelectedConfig] = useState< - Partial + Partial >({ text: `{{ range .Alerts -}} *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }} @@ -102,9 +106,7 @@ function CreateAlertChannels({ message: 'Success', description: t('channel_creation_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -165,9 +167,7 @@ function CreateAlertChannels({ message: 'Success', description: t('channel_creation_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -222,9 +222,7 @@ function CreateAlertChannels({ message: 'Success', description: t('channel_creation_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -241,26 +239,71 @@ function CreateAlertChannels({ setSavingState(false); }, [t, notifications, preparePagerRequest]); + const prepareMsTeamsRequest = useCallback( + () => ({ + webhook_url: selectedConfig?.webhook_url || '', + name: selectedConfig?.name || '', + send_resolved: true, + text: selectedConfig?.text || '', + title: selectedConfig?.title || '', + }), + [selectedConfig], + ); + + const onMsTeamsHandler = useCallback(async () => { + setSavingState(true); + + try { + const response = await createMsTeamsApi(prepareMsTeamsRequest()); + + if (response.statusCode === 200) { + notifications.success({ + message: 'Success', + description: t('channel_creation_done'), + }); + history.replace(ROUTES.ALL_CHANNELS); + } else { + notifications.error({ + message: 'Error', + description: response.error || t('channel_creation_failed'), + }); + } + } catch (error) { + notifications.error({ + message: 'Error', + description: t('channel_creation_failed'), + }); + } + setSavingState(false); + }, [prepareMsTeamsRequest, t, notifications]); + const onSaveHandler = useCallback( async (value: ChannelType) => { - switch (value) { - case SlackType: - onSlackHandler(); - break; - case WebhookType: - onWebhookHandler(); - break; - case PagerType: - onPagerHandler(); - break; - default: - notifications.error({ - message: 'Error', - description: t('selected_channel_invalid'), - }); + const functionMapper = { + [SlackType]: onSlackHandler, + [WebhookType]: onWebhookHandler, + [PagerType]: onPagerHandler, + [MsTeamsType]: onMsTeamsHandler, + }; + const functionToCall = functionMapper[value]; + + if (functionToCall) { + functionToCall(); + } else { + notifications.error({ + message: 'Error', + description: t('selected_channel_invalid'), + }); } }, - [onSlackHandler, t, onPagerHandler, onWebhookHandler, notifications], + [ + onSlackHandler, + onWebhookHandler, + onPagerHandler, + onMsTeamsHandler, + notifications, + t, + ], ); const performChannelTest = useCallback( @@ -282,6 +325,10 @@ function CreateAlertChannels({ request = preparePagerRequest(); if (request) response = await testPagerApi(request); break; + case MsTeamsType: + request = prepareMsTeamsRequest(); + response = await testMsTeamsApi(request); + break; default: notifications.error({ message: 'Error', @@ -315,6 +362,7 @@ function CreateAlertChannels({ t, preparePagerRequest, prepareSlackRequest, + prepareMsTeamsRequest, notifications, ], ); diff --git a/frontend/src/container/EditAlertChannels/index.tsx b/frontend/src/container/EditAlertChannels/index.tsx index 42ebd543c6..d6cba08381 100644 --- a/frontend/src/container/EditAlertChannels/index.tsx +++ b/frontend/src/container/EditAlertChannels/index.tsx @@ -1,13 +1,17 @@ import { Form } from 'antd'; +import editMsTeamsApi from 'api/channels/editMsTeams'; import editPagerApi from 'api/channels/editPager'; import editSlackApi from 'api/channels/editSlack'; import editWebhookApi from 'api/channels/editWebhook'; +import testMsTeamsApi from 'api/channels/testMsTeams'; import testPagerApi from 'api/channels/testPager'; import testSlackApi from 'api/channels/testSlack'; import testWebhookApi from 'api/channels/testWebhook'; import ROUTES from 'constants/routes'; import { ChannelType, + MsTeamsChannel, + MsTeamsType, PagerChannel, PagerType, SlackChannel, @@ -31,7 +35,7 @@ function EditAlertChannels({ const [formInstance] = Form.useForm(); const [selectedConfig, setSelectedConfig] = useState< - Partial + Partial >({ ...initialValue, }); @@ -81,9 +85,7 @@ function EditAlertChannels({ description: t('channel_edit_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -136,9 +138,7 @@ function EditAlertChannels({ description: t('channel_edit_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { showError(response.error || t('channel_edit_failed')); } @@ -183,9 +183,7 @@ function EditAlertChannels({ description: t('channel_edit_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -195,6 +193,48 @@ function EditAlertChannels({ setSavingState(false); }, [preparePagerRequest, notifications, selectedConfig, t]); + const prepareMsTeamsRequest = useCallback( + () => ({ + webhook_url: selectedConfig?.webhook_url || '', + name: selectedConfig?.name || '', + send_resolved: true, + text: selectedConfig?.text || '', + title: selectedConfig?.title || '', + id, + }), + [id, selectedConfig], + ); + + const onMsTeamsEditHandler = useCallback(async () => { + setSavingState(true); + + if (selectedConfig?.webhook_url === '') { + notifications.error({ + message: 'Error', + description: t('webhook_url_required'), + }); + setSavingState(false); + return; + } + + const response = await editMsTeamsApi(prepareMsTeamsRequest()); + + if (response.statusCode === 200) { + notifications.success({ + message: 'Success', + description: t('channel_edit_done'), + }); + + history.replace(ROUTES.ALL_CHANNELS); + } else { + notifications.error({ + message: 'Error', + description: response.error || t('channel_edit_failed'), + }); + } + setSavingState(false); + }, [prepareMsTeamsRequest, t, notifications, selectedConfig]); + const onSaveHandler = useCallback( (value: ChannelType) => { if (value === SlackType) { @@ -203,9 +243,16 @@ function EditAlertChannels({ onWebhookEditHandler(); } else if (value === PagerType) { onPagerEditHandler(); + } else if (value === MsTeamsType) { + onMsTeamsEditHandler(); } }, - [onSlackEditHandler, onWebhookEditHandler, onPagerEditHandler], + [ + onSlackEditHandler, + onWebhookEditHandler, + onPagerEditHandler, + onMsTeamsEditHandler, + ], ); const performChannelTest = useCallback( @@ -227,6 +274,10 @@ function EditAlertChannels({ request = preparePagerRequest(); if (request) response = await testPagerApi(request); break; + case MsTeamsType: + request = prepareMsTeamsRequest(); + if (request) response = await testMsTeamsApi(request); + break; default: notifications.error({ message: 'Error', @@ -260,6 +311,7 @@ function EditAlertChannels({ prepareWebhookRequest, preparePagerRequest, prepareSlackRequest, + prepareMsTeamsRequest, notifications, ], ); diff --git a/frontend/src/container/FormAlertChannels/Settings/MsTeams.tsx b/frontend/src/container/FormAlertChannels/Settings/MsTeams.tsx new file mode 100644 index 0000000000..48751f4acc --- /dev/null +++ b/frontend/src/container/FormAlertChannels/Settings/MsTeams.tsx @@ -0,0 +1,57 @@ +import { Form, Input } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { MsTeamsChannel } from '../../CreateAlertChannels/config'; + +function MsTeams({ setSelectedConfig }: MsTeamsProps): JSX.Element { + const { t } = useTranslation('channels'); + + return ( + <> + + { + setSelectedConfig((value) => ({ + ...value, + webhook_url: event.target.value, + })); + }} + /> + + + + + setSelectedConfig((value) => ({ + ...value, + title: event.target.value, + })) + } + /> + + + + + setSelectedConfig((value) => ({ + ...value, + text: event.target.value, + })) + } + placeholder={t('placeholder_slack_description')} + /> + + + ); +} + +interface MsTeamsProps { + setSelectedConfig: React.Dispatch< + React.SetStateAction> + >; +} + +export default MsTeams; diff --git a/frontend/src/container/FormAlertChannels/index.tsx b/frontend/src/container/FormAlertChannels/index.tsx index 14f6d32413..1aa4de4748 100644 --- a/frontend/src/container/FormAlertChannels/index.tsx +++ b/frontend/src/container/FormAlertChannels/index.tsx @@ -1,8 +1,11 @@ import { Form, FormInstance, Input, Select, Typography } from 'antd'; import { Store } from 'antd/lib/form/interface'; +import UpgradePrompt from 'components/Upgrade/UpgradePrompt'; +import { FeatureKeys } from 'constants/features'; import ROUTES from 'constants/routes'; import { ChannelType, + MsTeamsType, PagerChannel, PagerType, SlackChannel, @@ -10,18 +13,18 @@ import { WebhookChannel, WebhookType, } from 'container/CreateAlertChannels/config'; +import useFeatureFlags from 'hooks/useFeatureFlag'; +import { isFeatureKeys } from 'hooks/useFeatureFlag/utils'; import history from 'lib/history'; import { Dispatch, ReactElement, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; +import MsTeamsSettings from './Settings/MsTeams'; import PagerSettings from './Settings/Pager'; import SlackSettings from './Settings/Slack'; import WebhookSettings from './Settings/Webhook'; import { Button } from './styles'; -const { Option } = Select; -const { Title } = Typography; - function FormAlertChannels({ formInstance, type, @@ -36,8 +39,27 @@ function FormAlertChannels({ editing = false, }: FormAlertChannelsProps): JSX.Element { const { t } = useTranslation('channels'); + const isUserOnEEPlan = useFeatureFlags(FeatureKeys.ENTERPRISE_PLAN); + + const feature = `ALERT_CHANNEL_${type.toUpperCase()}`; + + const hasFeature = useFeatureFlags( + isFeatureKeys(feature) ? feature : FeatureKeys.ALERT_CHANNEL_SLACK, + ); + + const isOssFeature = useFeatureFlags(FeatureKeys.OSS); const renderSettings = (): ReactElement | null => { + if ( + // for ee plan + !isOssFeature?.active && + (!hasFeature || !hasFeature.active) && + type === 'msteams' + ) { + // channel type is not available for users plan + return ; + } + switch (type) { case SlackType: return ; @@ -45,14 +67,16 @@ function FormAlertChannels({ return ; case PagerType: return ; - + case MsTeamsType: + return ; default: return null; } }; + return ( <> - {title} + {title}
@@ -69,15 +93,22 @@ function FormAlertChannels({ @@ -85,7 +116,7 @@ function FormAlertChannels({