From 8aa9f38861601c5fc53b6eaa2b3b645199a53d84 Mon Sep 17 00:00:00 2001 From: Mia Hsu Date: Wed, 16 Oct 2024 09:32:03 -0700 Subject: [PATCH 1/6] first draft --- .../analytics/workflowAnalyticsEvents.tsx | 2 +- .../projectInstall/createProject.spec.tsx | 19 +++ .../views/projectInstall/createProject.tsx | 123 ++++++++++++-- .../issueAlertNotificationContext.tsx | 32 ++++ .../issueAlertNotificationOptions.spec.tsx | 107 +++++++++++++ .../issueAlertNotificationOptions.tsx | 150 ++++++++++++++++++ .../projectInstall/issueAlertOptions.spec.tsx | 56 +++++-- .../projectInstall/issueAlertOptions.tsx | 10 ++ .../messagingIntegrationAlertRule.spec.tsx | 90 +++++++++++ .../messagingIntegrationAlertRule.tsx | 120 ++++++++++++++ 10 files changed, 683 insertions(+), 26 deletions(-) create mode 100644 static/app/views/projectInstall/issueAlertNotificationContext.tsx create mode 100644 static/app/views/projectInstall/issueAlertNotificationOptions.spec.tsx create mode 100644 static/app/views/projectInstall/issueAlertNotificationOptions.tsx create mode 100644 static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx create mode 100644 static/app/views/projectInstall/messagingIntegrationAlertRule.tsx diff --git a/static/app/utils/analytics/workflowAnalyticsEvents.tsx b/static/app/utils/analytics/workflowAnalyticsEvents.tsx index a59046f069fb6b..0545e7be22ac45 100644 --- a/static/app/utils/analytics/workflowAnalyticsEvents.tsx +++ b/static/app/utils/analytics/workflowAnalyticsEvents.tsx @@ -147,7 +147,7 @@ export type TeamInsightsEventParameters = { issue_alert: 'Default' | 'Custom' | 'No Rule'; platform: string; project_id: string; - rule_id: string; + rule_ids: string[]; }; 'project_detail.change_chart': {chart_index: number; metric: string}; 'project_detail.open_anr_issues': {}; diff --git a/static/app/views/projectInstall/createProject.spec.tsx b/static/app/views/projectInstall/createProject.spec.tsx index 6bddf4efe8a0dd..4d326126c0a621 100644 --- a/static/app/views/projectInstall/createProject.spec.tsx +++ b/static/app/views/projectInstall/createProject.spec.tsx @@ -1,4 +1,5 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; +import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegrations'; import {MOCK_RESP_VERBOSE} from 'sentry-fixture/ruleConditions'; import {TeamFixture} from 'sentry-fixture/team'; @@ -47,6 +48,15 @@ function renderFrameworkModalMockRequests({ body: [], }); + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/integrations/?integrationType=messaging`, + body: [ + OrganizationIntegrationsFixture({ + name: "Moo Deng's Workspace", + }), + ], + }); + const projectCreationMockRequest = MockApiClient.addMockResponse({ url: `/teams/${organization.slug}/${teamSlug}/projects/`, method: 'POST', @@ -375,6 +385,15 @@ describe('CreateProject', function () { url: `/projects/${organization.slug}/rule-conditions/`, body: MOCK_RESP_VERBOSE, }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/integrations/?integrationType=messaging`, + body: [ + OrganizationIntegrationsFixture({ + name: "Moo Deng's Workspace", + }), + ], + }); }); afterEach(() => { diff --git a/static/app/views/projectInstall/createProject.tsx b/static/app/views/projectInstall/createProject.tsx index e2b307b3f72fea..d9a50dc49b1dce 100644 --- a/static/app/views/projectInstall/createProject.tsx +++ b/static/app/views/projectInstall/createProject.tsx @@ -24,6 +24,8 @@ import {Tooltip} from 'sentry/components/tooltip'; import {t, tct} from 'sentry/locale'; import ProjectsStore from 'sentry/stores/projectsStore'; import {space} from 'sentry/styles/space'; +import {IssueAlertActionType} from 'sentry/types/alerts'; +import type {OrganizationIntegration} from 'sentry/types/integrations'; import type {OnboardingSelectedSDK} from 'sentry/types/onboarding'; import type {Team} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -35,6 +37,7 @@ import useApi from 'sentry/utils/useApi'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {useTeams} from 'sentry/utils/useTeams'; +import {IssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; import IssueAlertOptions, { MetricValues, RuleAction, @@ -81,6 +84,52 @@ function CreateProject() { const [alertRuleConfig, setAlertRuleConfig] = useState( undefined ); + const [alertNotificationAction, setAlertNotificationAction] = useState< + IssueAlertActionType[] + >([IssueAlertActionType.NOTIFY_EMAIL]); + const [alertNotificationProvider, setAlertNotificationProvider] = useState< + string | undefined + >(undefined); + const [alertNotificationIntegration, setAlertNotificationIntegration] = useState< + OrganizationIntegration | undefined + >(undefined); + const [alertNotificationChannel, setAlertNotificationChannel] = useState< + string | undefined + >(undefined); + + const integrationAction = useMemo(() => { + const id = alertNotificationAction.find( + action => action !== IssueAlertActionType.NOTIFY_EMAIL + ); + if (id === IssueAlertActionType.SLACK) { + return [ + { + id: id, + workspace: alertNotificationIntegration?.id, + channel: alertNotificationChannel, + }, + ]; + } + if (id === IssueAlertActionType.DISCORD) { + return [ + { + id: id, + server: alertNotificationIntegration?.id, + channel_id: alertNotificationChannel, + }, + ]; + } + if (id === IssueAlertActionType.MS_TEAMS) { + return [ + { + id: id, + team: alertNotificationIntegration?.id, + channel: alertNotificationChannel, + }, + ]; + } + return undefined; + }, [alertNotificationAction, alertNotificationIntegration, alertNotificationChannel]); const frameworkSelectionEnabled = !!organization?.features.includes( 'onboarding-sdk-selection' @@ -121,7 +170,7 @@ function CreateProject() { }, }); - let ruleId: string | undefined; + const ruleIds: string[] = []; if (shouldCreateCustomRule) { const ruleData = await api.requestPromise( `/projects/${organization.slug}/${projectData.slug}/rules/`, @@ -136,7 +185,28 @@ function CreateProject() { }, } ); - ruleId = ruleData.id; + ruleIds?.push(ruleData.id); + } + if ( + organization.features.includes( + 'messaging-integration-onboarding-project-creation' + ) && + integrationAction + ) { + const ruleData = await api.requestPromise( + `/projects/${organization.slug}/${projectData.slug}/rules/`, + { + method: 'POST', + data: { + name, + conditions, + actions: integrationAction, + actionMatch, + frequency, + }, + } + ); + ruleIds?.push(ruleData.id); } trackAnalytics('project_creation_page.created', { organization, @@ -147,7 +217,7 @@ function CreateProject() { : 'No Rule', project_id: projectData.id, platform: selectedPlatform.key, - rule_id: ruleId || '', + rule_ids: ruleIds, }); ProjectsStore.onCreateSuccess(projectData, organization.slug); @@ -192,7 +262,7 @@ function CreateProject() { } } }, - [api, alertRuleConfig, organization, platform, projectName, team] + [api, alertRuleConfig, organization, platform, projectName, team, integrationAction] ); const handleProjectCreation = useCallback(async () => { @@ -269,11 +339,15 @@ function CreateProject() { const isMissingProjectName = projectName === ''; const isMissingAlertThreshold = shouldCreateCustomRule && !conditions?.every?.(condition => condition.value); + const isMissingMessagingIntegrationChannel = + integrationAction?.some(action => action !== IssueAlertActionType.NOTIFY_EMAIL) && + !alertNotificationChannel; const formErrorCount = [ isMissingTeam, isMissingProjectName, isMissingAlertThreshold, + isMissingMessagingIntegrationChannel, ].filter(value => value).length; const canSubmitForm = !inFlight && canUserCreateProject && formErrorCount === 0; @@ -285,6 +359,10 @@ function CreateProject() { submitTooltipText = t('Please provide a project name'); } else if (isMissingAlertThreshold) { submitTooltipText = t('Please provide an alert threshold'); + } else if (isMissingMessagingIntegrationChannel) { + submitTooltipText = t( + 'Please provide an integration channel for alert notifications' + ); } const alertFrequencyDefaultValues = useMemo(() => { @@ -344,11 +422,24 @@ function CreateProject() { noAutoFilter /> {t('Set your alert frequency')} - setAlertRuleConfig(updatedData)} - /> + + setAlertRuleConfig(updatedData)} + /> + {t('Name your project and assign it a team')} ) => { @@ -405,11 +496,15 @@ function CreateProject() { {errors && ( - {Object.keys(errors).map(key => ( -
- {startCase(key)}: {errors[key]} -
- ))} + {Object.keys(errors).map(key => { + const label = + key === 'actions' ? 'Notify via integration' : startCase(key); + return ( +
+ {label}: {errors[key]} +
+ ); + })}
)} diff --git a/static/app/views/projectInstall/issueAlertNotificationContext.tsx b/static/app/views/projectInstall/issueAlertNotificationContext.tsx new file mode 100644 index 00000000000000..09fc007728e454 --- /dev/null +++ b/static/app/views/projectInstall/issueAlertNotificationContext.tsx @@ -0,0 +1,32 @@ +import {createContext, useContext} from 'react'; + +import type {IssueAlertActionType} from 'sentry/types/alerts'; +import type {OrganizationIntegration} from 'sentry/types/integrations'; + +export type IssueAlertNotificationContextValue = { + alertNotificationAction: IssueAlertActionType[]; + alertNotificationChannel: string | undefined; + alertNotificationIntegration: OrganizationIntegration | undefined; + alertNotificationProvider: string | undefined; + setAlertNotificationAction: (method: IssueAlertActionType[]) => void; + setAlertNotificationChannel: (channel: string | undefined) => void; + setAlertNotificationIntegration: ( + integration: OrganizationIntegration | undefined + ) => void; + setAlertNotificationProvider: (provider: string | undefined) => void; +}; + +export const IssueAlertNotificationContext = + createContext(null); + +export function useIssueAlertNotificationContext(): IssueAlertNotificationContextValue { + const context = useContext(IssueAlertNotificationContext); + + if (!context) { + throw new Error( + 'useIssueAlertNotificationContext must be used within a IssueAlertNotificationContext.Provider' + ); + } + + return context; +} diff --git a/static/app/views/projectInstall/issueAlertNotificationOptions.spec.tsx b/static/app/views/projectInstall/issueAlertNotificationOptions.spec.tsx new file mode 100644 index 00000000000000..b991e3f405dce5 --- /dev/null +++ b/static/app/views/projectInstall/issueAlertNotificationOptions.spec.tsx @@ -0,0 +1,107 @@ +import {GitHubIntegrationProviderFixture} from 'sentry-fixture/githubIntegrationProvider'; +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegrations'; + +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import type {OrganizationIntegration} from 'sentry/types/integrations'; +import {IssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; +import IssueAlertNotificationOptions from 'sentry/views/projectInstall/issueAlertNotificationOptions'; + +describe('MessagingIntegrationAlertRule', function () { + const organization = OrganizationFixture({ + features: ['messaging-integration-onboarding-project-creation'], + }); + let mockResponse: jest.Mock; + let integrations: OrganizationIntegration[] = []; + const mockSetAlertNotificationAction = jest.fn(); + + const issueAlertNotificationContextValue = { + alertNotificationAction: [], + alertNotificationChannel: 'channel', + alertNotificationIntegration: undefined, + alertNotificationProvider: 'slack', + setAlertNotificationAction: mockSetAlertNotificationAction, + setAlertNotificationChannel: jest.fn(), + setAlertNotificationIntegration: jest.fn(), + setAlertNotificationProvider: jest.fn(), + }; + + const getComponent = () => ( + + + + ); + + beforeEach(function () { + integrations = [ + OrganizationIntegrationsFixture({ + name: "Moo Deng's Workspace", + status: 'disabled', + }), + OrganizationIntegrationsFixture({ + name: "Moo Waan's Workspace", + status: 'disabled', + }), + ]; + mockResponse = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/integrations/?integrationType=messaging`, + body: integrations, + }); + }); + + afterEach(function () { + MockApiClient.clearMockResponses(); + }); + + it('renders setup button if no integrations are active', async function () { + const providers = (providerKey: string) => [ + GitHubIntegrationProviderFixture({key: providerKey}), + ]; + const providerKeys = ['slack', 'discord', 'msteams']; + const mockResponses: jest.Mock[] = []; + providerKeys.forEach(providerKey => { + mockResponses.push( + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/config/integrations/?provider_key=${providerKey}`, + body: {providers: providers(providerKey)}, + }) + ); + }); + render(getComponent(), {organization: organization}); + await screen.findByText(/notify via email/i); + expect(screen.queryByText(/notify via integration/i)).not.toBeInTheDocument(); + await screen.findByRole('button', {name: /connect to messaging/i}); + expect(mockResponse).toHaveBeenCalled(); + mockResponses.forEach(mock => { + expect(mock).toHaveBeenCalled(); + }); + }); + + it('renders alert configuration if integration is installed', async function () { + integrations.push( + OrganizationIntegrationsFixture({ + name: "Moo Toon's Workspace", + status: 'active', + }) + ); + render(getComponent(), {organization: organization}); + await screen.findByText(/notify via email/i); + await screen.findByText(/notify via integration/i); + expect(mockResponse).toHaveBeenCalled(); + }); + + it('calls setter when new integration option is selected', async function () { + integrations.push( + OrganizationIntegrationsFixture({ + name: "Moo Toon's Workspace", + status: 'active', + }) + ); + render(getComponent(), {organization: organization}); + await screen.findByText(/notify via email/i); + await screen.findByText(/notify via integration/i); + await userEvent.click(screen.getByText(/notify via integration/i)); + expect(mockSetAlertNotificationAction).toHaveBeenCalled(); + }); +}); diff --git a/static/app/views/projectInstall/issueAlertNotificationOptions.tsx b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx new file mode 100644 index 00000000000000..2b962845213d86 --- /dev/null +++ b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx @@ -0,0 +1,150 @@ +import {useEffect, useMemo, useState} from 'react'; +import styled from '@emotion/styled'; + +import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox'; +import {space} from 'sentry/styles/space'; +import {IssueAlertActionType} from 'sentry/types/alerts'; +import type {OrganizationIntegration} from 'sentry/types/integrations'; +import {useApiQuery} from 'sentry/utils/queryClient'; +import useOrganization from 'sentry/utils/useOrganization'; +import SetupMessagingIntegrationButton, { + MessagingIntegrationAnalyticsView, +} from 'sentry/views/alerts/rules/issue/setupMessagingIntegrationButton'; +import {useIssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; +import MessagingIntegrationAlertRule from 'sentry/views/projectInstall/messagingIntegrationAlertRule'; + +export const providerDetails = { + slack: { + name: 'Slack', + action: IssueAlertActionType.SLACK, + label: 'workspace to', + placeholder: 'channel, e.g. #critical', + }, + discord: { + name: 'Discord', + action: IssueAlertActionType.DISCORD, + label: 'server in the channel', + placeholder: 'channel ID or URL', + }, + msteams: { + name: 'MSTeams', + action: IssueAlertActionType.MS_TEAMS, + label: 'team to', + placeholder: 'channel ID', + }, +}; + +enum MultipleCheckboxOptions { + EMAIL = 'email', + INTEGRATION = 'integration', +} + +export default function IssueAlertNotificationOptions() { + const organization = useOrganization(); + const { + alertNotificationAction: action, + alertNotificationProvider: provider, + setAlertNotificationAction: setAction, + setAlertNotificationIntegration: setIntegration, + setAlertNotificationProvider: setProvider, + } = useIssueAlertNotificationContext(); + const [selectedValues, setSelectedValues] = useState( + action.map(a => + a === IssueAlertActionType.NOTIFY_EMAIL + ? MultipleCheckboxOptions.EMAIL + : MultipleCheckboxOptions.INTEGRATION + ) + ); + + const messagingIntegrationsQuery = useApiQuery( + [`/organizations/${organization.slug}/integrations/?integrationType=messaging`], + {staleTime: Infinity} + ); + + const refetchConfigs = () => messagingIntegrationsQuery.refetch(); + + const providersToIntegrations = useMemo(() => { + const map: {[key: string]: OrganizationIntegration[]} = {}; + if (!messagingIntegrationsQuery.data) { + return {}; + } + for (const i of messagingIntegrationsQuery.data) { + const providerSlug = i.provider.slug; + map[providerSlug] = map[providerSlug] ?? []; + map[providerSlug].push(i); + } + return map; + }, [messagingIntegrationsQuery.data]); + + useEffect(() => { + const providerKeys = Object.keys(providersToIntegrations); + if (providerKeys.length > 0) { + const firstProvider = providerKeys[0]; + setProvider(firstProvider); + + const firstIntegration = providersToIntegrations[firstProvider][0]; + setIntegration(firstIntegration); + } + }, [providersToIntegrations, setProvider, setIntegration]); + + const shouldRenderSetupButton = useMemo(() => { + return messagingIntegrationsQuery.data?.every(i => i.status !== 'active'); + }, [messagingIntegrationsQuery.data]); + + const shouldRenderNotificationConfigs = useMemo(() => { + return selectedValues.some(v => v !== MultipleCheckboxOptions.EMAIL); + }, [selectedValues]); + + const onChange = values => { + setSelectedValues(values); + setAction( + values.map(v => + v === MultipleCheckboxOptions.INTEGRATION && provider + ? providerDetails[provider].action + : IssueAlertActionType.NOTIFY_EMAIL + ) + ); + }; + + if (messagingIntegrationsQuery.isLoading || messagingIntegrationsQuery.isError) { + return null; + } + + return ( +
+ + + + Notify via email + + {!shouldRenderSetupButton && provider && ( +
+ + Notify via integration (Slack, Discord, MS Teams, etc.) + + {shouldRenderNotificationConfigs && ( + + )} +
+ )} +
+
+ {shouldRenderSetupButton && ( + + )} +
+ ); +} + +const Wrapper = styled('div')` + display: flex; + flex-direction: column; + gap: ${space(1)}; +`; diff --git a/static/app/views/projectInstall/issueAlertOptions.spec.tsx b/static/app/views/projectInstall/issueAlertOptions.spec.tsx index c7c8f3a8f779f3..6833480387ace1 100644 --- a/static/app/views/projectInstall/issueAlertOptions.spec.tsx +++ b/static/app/views/projectInstall/issueAlertOptions.spec.tsx @@ -1,3 +1,5 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegrations'; import { MOCK_RESP_INCONSISTENT_INTERVALS, MOCK_RESP_INCONSISTENT_PLACEHOLDERS, @@ -5,23 +7,55 @@ import { MOCK_RESP_VERBOSE, } from 'sentry-fixture/ruleConditions'; -import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import selectEvent from 'sentry-test/selectEvent'; +import {IssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; import IssueAlertOptions from 'sentry/views/projectInstall/issueAlertOptions'; describe('IssueAlertOptions', function () { - const {organization} = initializeOrg(); + const organization = OrganizationFixture({ + features: ['messaging-integration-onboarding-project-creation'], + }); const URL = `/projects/${organization.slug}/rule-conditions/`; const props = { onChange: jest.fn(), }; + + const issueAlertNotificationContextValue = { + alertNotificationAction: [], + alertNotificationChannel: 'channel', + alertNotificationIntegration: OrganizationIntegrationsFixture({ + name: "Moo Deng's Workspace", + status: 'disabled', + }), + alertNotificationProvider: 'slack', + setAlertNotificationAction: jest.fn(), + setAlertNotificationChannel: jest.fn(), + setAlertNotificationIntegration: jest.fn(), + setAlertNotificationProvider: jest.fn(), + }; + + const getComponent = () => ( + + + + ); + beforeEach(() => { MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/rule-conditions/`, body: MOCK_RESP_VERBOSE, }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/integrations/?integrationType=messaging`, + body: [ + OrganizationIntegrationsFixture({ + name: "Moo Deng's Workspace", + }), + ], + }); }); afterEach(() => { MockApiClient.clearMockResponses(); @@ -34,7 +68,7 @@ describe('IssueAlertOptions', function () { body: [], }); - render(, {organization}); + render(getComponent(), {organization}); expect(screen.getAllByRole('radio')).toHaveLength(2); }); @@ -44,7 +78,7 @@ describe('IssueAlertOptions', function () { body: {}, }); - render(, {organization}); + render(getComponent(), {organization}); expect(screen.getAllByRole('radio')).toHaveLength(2); }); @@ -54,7 +88,7 @@ describe('IssueAlertOptions', function () { body: MOCK_RESP_INCONSISTENT_INTERVALS, }); - render(, {organization}); + render(getComponent(), {organization}); expect(screen.getAllByRole('radio')).toHaveLength(2); }); @@ -63,7 +97,7 @@ describe('IssueAlertOptions', function () { url: URL, body: MOCK_RESP_INCONSISTENT_PLACEHOLDERS, }); - render(, {organization}); + render(getComponent(), {organization}); expect(screen.getAllByRole('radio')).toHaveLength(3); }); @@ -73,7 +107,7 @@ describe('IssueAlertOptions', function () { body: MOCK_RESP_ONLY_IGNORED_CONDITIONS_INVALID, }); - render(, {organization}); + render(getComponent(), {organization}); expect(screen.getAllByRole('radio')).toHaveLength(3); await selectEvent.select(screen.getByText('Select...'), 'users affected by'); expect(props.onChange).toHaveBeenCalledWith( @@ -90,7 +124,7 @@ describe('IssueAlertOptions', function () { body: MOCK_RESP_VERBOSE, }); - render(); + render(getComponent()); expect(screen.getAllByRole('radio')).toHaveLength(3); }); @@ -100,7 +134,7 @@ describe('IssueAlertOptions', function () { body: MOCK_RESP_VERBOSE, }); - render(); + render(getComponent()); await selectEvent.select(screen.getByText('occurrences of'), 'users affected by'); await selectEvent.select(screen.getByText('one minute'), '30 days'); expect(props.onChange).toHaveBeenCalledWith( @@ -117,7 +151,7 @@ describe('IssueAlertOptions', function () { body: MOCK_RESP_VERBOSE, }); - render(); + render(getComponent()); expect(screen.getByTestId('range-input')).toHaveValue(10); }); @@ -127,7 +161,7 @@ describe('IssueAlertOptions', function () { body: MOCK_RESP_VERBOSE, }); - render(); + render(getComponent()); await userEvent.click(screen.getByLabelText(/When there are more than/i)); expect(props.onChange).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/static/app/views/projectInstall/issueAlertOptions.tsx b/static/app/views/projectInstall/issueAlertOptions.tsx index f016865bc00e82..020da444b46c45 100644 --- a/static/app/views/projectInstall/issueAlertOptions.tsx +++ b/static/app/views/projectInstall/issueAlertOptions.tsx @@ -13,6 +13,7 @@ import type {IssueAlertRuleAction} from 'sentry/types/alerts'; import {IssueAlertActionType, IssueAlertConditionType} from 'sentry/types/alerts'; import type {Organization} from 'sentry/types/organization'; import withOrganization from 'sentry/utils/withOrganization'; +import IssueAlertNotificationOptions from 'sentry/views/projectInstall/issueAlertNotificationOptions'; export enum MetricValues { ERRORS = 0, @@ -303,6 +304,12 @@ class IssueAlertOptions extends DeprecatedAsyncComponent { onChange={alertSetting => this.setStateAndUpdateParents({alertSetting})} value={this.state.alertSetting} /> + {!this.props.organization.features.includes( + 'messaging-integration-onboarding-project-creation' + ) && + parseInt(this.state.alertSetting, 10) !== RuleAction.CREATE_ALERT_LATER && ( + + )} ); } @@ -313,6 +320,9 @@ export default withOrganization(IssueAlertOptions); const Content = styled('div')` padding-top: ${space(2)}; padding-bottom: ${space(4)}; + display: flex; + flex-direction: column; + gap: ${space(2)}; `; const CustomizeAlert = styled('div')` diff --git a/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx b/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx new file mode 100644 index 00000000000000..b2561a06176cf3 --- /dev/null +++ b/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx @@ -0,0 +1,90 @@ +import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegrations'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; +import selectEvent from 'sentry-test/selectEvent'; + +import {IssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; +import MessagingIntegrationAlertRule from 'sentry/views/projectInstall/messagingIntegrationAlertRule'; + +describe('MessagingIntegrationAlertRule', function () { + const slackIntegrations = [ + OrganizationIntegrationsFixture({ + name: "Moo Deng's Workspace", + }), + OrganizationIntegrationsFixture({ + name: "Moo Waan's Workspace", + }), + ]; + const discordIntegrations = [ + OrganizationIntegrationsFixture({ + name: "Moo Deng's Server", + }), + ]; + const msteamsIntegrations = [ + OrganizationIntegrationsFixture({ + name: "Moo Deng's Team", + }), + ]; + + const mockSetAlertNotificationChannel = jest.fn(); + const mockSetAlertNotificationIntegration = jest.fn(); + const mockSetAlertNotificationProvider = jest.fn(); + + const issueAlertNotificationContextValue = { + alertNotificationAction: [], + alertNotificationChannel: 'channel', + alertNotificationIntegration: slackIntegrations[0], + alertNotificationProvider: 'slack', + setAlertNotificationAction: jest.fn(), + setAlertNotificationChannel: mockSetAlertNotificationChannel, + setAlertNotificationIntegration: mockSetAlertNotificationIntegration, + setAlertNotificationProvider: mockSetAlertNotificationProvider, + }; + + const providersToIntegrations = { + slack: slackIntegrations, + discord: discordIntegrations, + msteams: msteamsIntegrations, + }; + + const getComponent = props => ( + + + + ); + + it('renders', function () { + render(getComponent({})); + expect(screen.getAllByRole('textbox')).toHaveLength(3); + }); + + it('calls setter when new integration is selected', async function () { + render(getComponent({})); + await selectEvent.select( + screen.getByText("Moo Deng's Workspace"), + "Moo Waan's Workspace" + ); + expect(mockSetAlertNotificationIntegration).toHaveBeenCalled(); + }); + + it('calls setters when new provider is selected', async function () { + render(getComponent({})); + await selectEvent.select(screen.getByText('Slack'), 'Discord'); + expect(mockSetAlertNotificationProvider).toHaveBeenCalled(); + expect(mockSetAlertNotificationIntegration).toHaveBeenCalled(); + expect(mockSetAlertNotificationChannel).toHaveBeenCalled(); + }); + + // it('disables integration select when there is only one option', function () { + // render( + // getComponent({ + // alertNotificationIntegration: discordIntegrations[0], + // alertNotificationProvider: 'discord', + // }) + // ); + // screen.getByRole('text').click(); + // expect(screen.getByRole('textbox')).toBeDisabled(); + // }); +}); diff --git a/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx b/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx new file mode 100644 index 00000000000000..dfa74dd076a680 --- /dev/null +++ b/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx @@ -0,0 +1,120 @@ +import {useEffect, useMemo} from 'react'; +import styled from '@emotion/styled'; + +import SelectControl from 'sentry/components/forms/controls/selectControl'; +import Input from 'sentry/components/input'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import type {OrganizationIntegration} from 'sentry/types/integrations'; +import {useIssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; +import {providerDetails} from 'sentry/views/projectInstall/issueAlertNotificationOptions'; + +type Props = { + providersToIntegrations: Record; +}; + +export default function MessagingIntegrationAlertRule({providersToIntegrations}: Props) { + const { + alertNotificationChannel: channel, + alertNotificationIntegration: integration, + alertNotificationProvider: provider, + setAlertNotificationChannel: setChannel, + setAlertNotificationIntegration: setIntegration, + setAlertNotificationProvider: setProvider, + } = useIssueAlertNotificationContext(); + + const providerOptions = useMemo( + () => + Object.keys(providersToIntegrations).map(p => ({ + value: p, + label: providerDetails[p].name, + })), + [providersToIntegrations] + ); + const integrationOptions = useMemo( + () => + provider && providersToIntegrations[provider] + ? providersToIntegrations[provider]?.map(i => ({ + value: i, + label: i.name, + })) + : [], + [providersToIntegrations, provider] + ); + + useEffect(() => { + const providerKeys = Object.keys(providersToIntegrations); + if (providerKeys.length > 0) { + const firstProvider = providerKeys[0]; + setProvider(firstProvider); + + const firstIntegration = providersToIntegrations[firstProvider][0]; + setIntegration(firstIntegration); + } + }, [providersToIntegrations, setProvider, setIntegration]); + + if (!provider) { + return null; + } + + return ( +
+ + + {t('Send')} + { + setProvider(p.value); + setIntegration(providersToIntegrations[p.value][0]); + setChannel(''); + }} + /> + {t('notification to the')} + setIntegration(i.value)} + /> + {providerDetails[provider]?.label} + ) => + setChannel(e.target.value) + } + /> + + +
+ ); +} + +const RuleWrapper = styled('div')` + padding: ${space(1)}; + background-color: ${p => p.theme.backgroundSecondary}; + border-radius: ${p => p.theme.borderRadius}; +`; + +const Rule = styled('div')` + display: flex; + align-items: center; + gap: ${space(1)}; +`; + +const InlineSelectControl = styled(SelectControl)` + width: 180px; +`; + +const InlineInput = styled(Input)` + width: auto; + min-height: 28px; +`; From 43e4ea731c7ec52517b52f02bbc7fd09023cc2ff Mon Sep 17 00:00:00 2001 From: Mia Hsu Date: Wed, 16 Oct 2024 15:29:42 -0700 Subject: [PATCH 2/6] use hook and no context --- .../modals/projectCreationModal.spec.tsx | 12 ++ .../views/projectInstall/createProject.tsx | 142 ++++++--------- .../issueAlertNotificationContext.tsx | 32 ---- .../issueAlertNotificationOptions.spec.tsx | 29 ++- .../issueAlertNotificationOptions.tsx | 172 +++++++++++++----- .../projectInstall/issueAlertOptions.tsx | 9 +- .../messagingIntegrationAlertRule.spec.tsx | 50 +++-- .../messagingIntegrationAlertRule.tsx | 22 +-- .../views/projectInstall/newProject.spec.tsx | 14 ++ 9 files changed, 265 insertions(+), 217 deletions(-) delete mode 100644 static/app/views/projectInstall/issueAlertNotificationContext.tsx diff --git a/static/app/components/modals/projectCreationModal.spec.tsx b/static/app/components/modals/projectCreationModal.spec.tsx index 8a330464c47b2b..11ea0fc5544c4c 100644 --- a/static/app/components/modals/projectCreationModal.spec.tsx +++ b/static/app/components/modals/projectCreationModal.spec.tsx @@ -1,4 +1,5 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; +import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegrations'; import {MOCK_RESP_VERBOSE} from 'sentry-fixture/ruleConditions'; import {TeamFixture} from 'sentry-fixture/team'; @@ -53,6 +54,12 @@ describe('Project Creation Modal', function () { const team = TeamFixture({ access: ['team:admin', 'team:write', 'team:read'], }); + const integrations = [ + OrganizationIntegrationsFixture({ + name: "Moo Deng's Workspace", + status: 'active', + }), + ]; MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/rule-conditions/`, @@ -80,6 +87,11 @@ describe('Project Creation Modal', function () { body: [], }); + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/integrations/?integrationType=messaging`, + body: integrations, + }); + OrganizationStore.onUpdate(organization); TeamStore.loadUserTeams([team]); diff --git a/static/app/views/projectInstall/createProject.tsx b/static/app/views/projectInstall/createProject.tsx index d9a50dc49b1dce..3e737cd3800f51 100644 --- a/static/app/views/projectInstall/createProject.tsx +++ b/static/app/views/projectInstall/createProject.tsx @@ -24,8 +24,6 @@ import {Tooltip} from 'sentry/components/tooltip'; import {t, tct} from 'sentry/locale'; import ProjectsStore from 'sentry/stores/projectsStore'; import {space} from 'sentry/styles/space'; -import {IssueAlertActionType} from 'sentry/types/alerts'; -import type {OrganizationIntegration} from 'sentry/types/integrations'; import type {OnboardingSelectedSDK} from 'sentry/types/onboarding'; import type {Team} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -37,7 +35,10 @@ import useApi from 'sentry/utils/useApi'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {useTeams} from 'sentry/utils/useTeams'; -import {IssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; +import { + MultipleCheckboxOptions, + useCreateNotificationAction, +} from 'sentry/views/projectInstall/issueAlertNotificationOptions'; import IssueAlertOptions, { MetricValues, RuleAction, @@ -84,52 +85,18 @@ function CreateProject() { const [alertRuleConfig, setAlertRuleConfig] = useState( undefined ); - const [alertNotificationAction, setAlertNotificationAction] = useState< - IssueAlertActionType[] - >([IssueAlertActionType.NOTIFY_EMAIL]); - const [alertNotificationProvider, setAlertNotificationProvider] = useState< - string | undefined - >(undefined); - const [alertNotificationIntegration, setAlertNotificationIntegration] = useState< - OrganizationIntegration | undefined - >(undefined); - const [alertNotificationChannel, setAlertNotificationChannel] = useState< - string | undefined - >(undefined); - - const integrationAction = useMemo(() => { - const id = alertNotificationAction.find( - action => action !== IssueAlertActionType.NOTIFY_EMAIL - ); - if (id === IssueAlertActionType.SLACK) { - return [ - { - id: id, - workspace: alertNotificationIntegration?.id, - channel: alertNotificationChannel, - }, - ]; - } - if (id === IssueAlertActionType.DISCORD) { - return [ - { - id: id, - server: alertNotificationIntegration?.id, - channel_id: alertNotificationChannel, - }, - ]; - } - if (id === IssueAlertActionType.MS_TEAMS) { - return [ - { - id: id, - team: alertNotificationIntegration?.id, - channel: alertNotificationChannel, - }, - ]; - } - return undefined; - }, [alertNotificationAction, alertNotificationIntegration, alertNotificationChannel]); + + const { + createNotificationAction, + actions: alertNotificationActions, + provider, + integration, + channel, + setActions: setAlertNotificationActions, + setProvider, + setIntegration, + setChannel, + } = useCreateNotificationAction(); const frameworkSelectionEnabled = !!organization?.features.includes( 'onboarding-sdk-selection' @@ -185,28 +152,25 @@ function CreateProject() { }, } ); - ruleIds?.push(ruleData.id); + ruleIds.push(ruleData.id); } if ( organization.features.includes( 'messaging-integration-onboarding-project-creation' - ) && - integrationAction + ) ) { - const ruleData = await api.requestPromise( - `/projects/${organization.slug}/${projectData.slug}/rules/`, - { - method: 'POST', - data: { - name, - conditions, - actions: integrationAction, - actionMatch, - frequency, - }, - } - ); - ruleIds?.push(ruleData.id); + const ruleData = await createNotificationAction({ + api, + name, + organizationSlug: organization.slug, + projectSlug: projectData.slug, + conditions, + actionMatch, + frequency, + }); + if (ruleData) { + ruleIds.push(ruleData.id); + } } trackAnalytics('project_creation_page.created', { organization, @@ -262,7 +226,15 @@ function CreateProject() { } } }, - [api, alertRuleConfig, organization, platform, projectName, team, integrationAction] + [ + api, + alertRuleConfig, + organization, + platform, + projectName, + team, + createNotificationAction, + ] ); const handleProjectCreation = useCallback(async () => { @@ -340,8 +312,9 @@ function CreateProject() { const isMissingAlertThreshold = shouldCreateCustomRule && !conditions?.every?.(condition => condition.value); const isMissingMessagingIntegrationChannel = - integrationAction?.some(action => action !== IssueAlertActionType.NOTIFY_EMAIL) && - !alertNotificationChannel; + alertNotificationActions?.some( + action => action === MultipleCheckboxOptions.INTEGRATION + ) && !channel; const formErrorCount = [ isMissingTeam, @@ -422,24 +395,21 @@ function CreateProject() { noAutoFilter /> {t('Set your alert frequency')} - setAlertRuleConfig(updatedData)} + notificationProps={{ + actions: alertNotificationActions, + channel, + integration, + provider, + setActions: setAlertNotificationActions, + setChannel, + setIntegration, + setProvider, }} - > - setAlertRuleConfig(updatedData)} - /> - + /> {t('Name your project and assign it a team')} ) => { diff --git a/static/app/views/projectInstall/issueAlertNotificationContext.tsx b/static/app/views/projectInstall/issueAlertNotificationContext.tsx deleted file mode 100644 index 09fc007728e454..00000000000000 --- a/static/app/views/projectInstall/issueAlertNotificationContext.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import {createContext, useContext} from 'react'; - -import type {IssueAlertActionType} from 'sentry/types/alerts'; -import type {OrganizationIntegration} from 'sentry/types/integrations'; - -export type IssueAlertNotificationContextValue = { - alertNotificationAction: IssueAlertActionType[]; - alertNotificationChannel: string | undefined; - alertNotificationIntegration: OrganizationIntegration | undefined; - alertNotificationProvider: string | undefined; - setAlertNotificationAction: (method: IssueAlertActionType[]) => void; - setAlertNotificationChannel: (channel: string | undefined) => void; - setAlertNotificationIntegration: ( - integration: OrganizationIntegration | undefined - ) => void; - setAlertNotificationProvider: (provider: string | undefined) => void; -}; - -export const IssueAlertNotificationContext = - createContext(null); - -export function useIssueAlertNotificationContext(): IssueAlertNotificationContextValue { - const context = useContext(IssueAlertNotificationContext); - - if (!context) { - throw new Error( - 'useIssueAlertNotificationContext must be used within a IssueAlertNotificationContext.Provider' - ); - } - - return context; -} diff --git a/static/app/views/projectInstall/issueAlertNotificationOptions.spec.tsx b/static/app/views/projectInstall/issueAlertNotificationOptions.spec.tsx index b991e3f405dce5..28c6256aae3e87 100644 --- a/static/app/views/projectInstall/issueAlertNotificationOptions.spec.tsx +++ b/static/app/views/projectInstall/issueAlertNotificationOptions.spec.tsx @@ -5,7 +5,6 @@ import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegr import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import type {OrganizationIntegration} from 'sentry/types/integrations'; -import {IssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; import IssueAlertNotificationOptions from 'sentry/views/projectInstall/issueAlertNotificationOptions'; describe('MessagingIntegrationAlertRule', function () { @@ -14,24 +13,20 @@ describe('MessagingIntegrationAlertRule', function () { }); let mockResponse: jest.Mock; let integrations: OrganizationIntegration[] = []; - const mockSetAlertNotificationAction = jest.fn(); + const mockSetAction = jest.fn(); - const issueAlertNotificationContextValue = { - alertNotificationAction: [], - alertNotificationChannel: 'channel', - alertNotificationIntegration: undefined, - alertNotificationProvider: 'slack', - setAlertNotificationAction: mockSetAlertNotificationAction, - setAlertNotificationChannel: jest.fn(), - setAlertNotificationIntegration: jest.fn(), - setAlertNotificationProvider: jest.fn(), + const notificationProps = { + actions: [], + channel: 'channel', + integration: undefined, + provider: 'slack', + setActions: mockSetAction, + setChannel: jest.fn(), + setIntegration: jest.fn(), + setProvider: jest.fn(), }; - const getComponent = () => ( - - - - ); + const getComponent = () => ; beforeEach(function () { integrations = [ @@ -102,6 +97,6 @@ describe('MessagingIntegrationAlertRule', function () { await screen.findByText(/notify via email/i); await screen.findByText(/notify via integration/i); await userEvent.click(screen.getByText(/notify via integration/i)); - expect(mockSetAlertNotificationAction).toHaveBeenCalled(); + expect(mockSetAction).toHaveBeenCalled(); }); }); diff --git a/static/app/views/projectInstall/issueAlertNotificationOptions.tsx b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx index 2b962845213d86..6ff2cb9d11226a 100644 --- a/static/app/views/projectInstall/issueAlertNotificationOptions.tsx +++ b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx @@ -1,6 +1,7 @@ -import {useEffect, useMemo, useState} from 'react'; +import {useCallback, useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; +import type {Client} from 'sentry/api'; import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox'; import {space} from 'sentry/styles/space'; import {IssueAlertActionType} from 'sentry/types/alerts'; @@ -10,7 +11,6 @@ import useOrganization from 'sentry/utils/useOrganization'; import SetupMessagingIntegrationButton, { MessagingIntegrationAnalyticsView, } from 'sentry/views/alerts/rules/issue/setupMessagingIntegrationButton'; -import {useIssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; import MessagingIntegrationAlertRule from 'sentry/views/projectInstall/messagingIntegrationAlertRule'; export const providerDetails = { @@ -34,44 +34,138 @@ export const providerDetails = { }, }; -enum MultipleCheckboxOptions { +export const enum MultipleCheckboxOptions { EMAIL = 'email', INTEGRATION = 'integration', } -export default function IssueAlertNotificationOptions() { - const organization = useOrganization(); - const { - alertNotificationAction: action, - alertNotificationProvider: provider, - setAlertNotificationAction: setAction, - setAlertNotificationIntegration: setIntegration, - setAlertNotificationProvider: setProvider, - } = useIssueAlertNotificationContext(); - const [selectedValues, setSelectedValues] = useState( - action.map(a => - a === IssueAlertActionType.NOTIFY_EMAIL - ? MultipleCheckboxOptions.EMAIL - : MultipleCheckboxOptions.INTEGRATION - ) +export type IssueAlertNotificationProps = { + actions: MultipleCheckboxOptions[]; + channel: string | undefined; + integration: OrganizationIntegration | undefined; + provider: string | undefined; + setActions: (action: MultipleCheckboxOptions[]) => void; + setChannel: (channel: string | undefined) => void; + setIntegration: (integration: OrganizationIntegration | undefined) => void; + setProvider: (provider: string | undefined) => void; +}; + +export function useCreateNotificationAction() { + const [actions, setActions] = useState([ + MultipleCheckboxOptions.EMAIL, + ]); + const [provider, setProvider] = useState(undefined); + const [integration, setIntegration] = useState( + undefined + ); + const [channel, setChannel] = useState(undefined); + + const integrationAction = useMemo(() => { + const isCreatingIntegrationNotification = actions.find( + action => action === MultipleCheckboxOptions.INTEGRATION + ); + if (!isCreatingIntegrationNotification) { + return undefined; + } + if (provider === 'slack') { + return [ + { + id: IssueAlertActionType.SLACK, + workspace: integration?.id, + channel: channel, + }, + ]; + } + if (provider === 'discord') { + return [ + { + id: IssueAlertActionType.DISCORD, + server: integration?.id, + channel_id: channel, + }, + ]; + } + if (provider === 'msteams') { + return [ + { + id: IssueAlertActionType.MS_TEAMS, + team: integration?.id, + channel: channel, + }, + ]; + } + return undefined; + }, [actions, integration, channel, provider]); + + type Props = { + actionMatch: string | undefined; + api: Client; + conditions: {id: string; interval: string; value: string}[] | undefined; + frequency: number | undefined; + name: string | undefined; + organizationSlug: string; + projectSlug: string; + }; + + const createNotificationAction = useCallback( + ({ + api, + organizationSlug, + projectSlug, + name, + conditions, + actionMatch, + frequency, + }: Props) => { + if (!integrationAction) { + return null; + } + return api.requestPromise(`/projects/${organizationSlug}/${projectSlug}/rules/`, { + method: 'POST', + data: { + name, + conditions, + actions: integrationAction, + actionMatch, + frequency, + }, + }); + }, + [integrationAction] ); + return { + createNotificationAction, + actions, + provider, + integration, + channel, + setActions, + setProvider, + setIntegration, + setChannel, + }; +} + +export default function IssueAlertNotificationOptions( + notificationProps: IssueAlertNotificationProps +) { + const organization = useOrganization(); + const {actions, provider, setActions, setIntegration, setProvider} = notificationProps; + const messagingIntegrationsQuery = useApiQuery( [`/organizations/${organization.slug}/integrations/?integrationType=messaging`], {staleTime: Infinity} ); - const refetchConfigs = () => messagingIntegrationsQuery.refetch(); - const providersToIntegrations = useMemo(() => { const map: {[key: string]: OrganizationIntegration[]} = {}; - if (!messagingIntegrationsQuery.data) { - return {}; - } - for (const i of messagingIntegrationsQuery.data) { - const providerSlug = i.provider.slug; - map[providerSlug] = map[providerSlug] ?? []; - map[providerSlug].push(i); + if (messagingIntegrationsQuery.data) { + for (const i of messagingIntegrationsQuery.data) { + const providerSlug = i.provider.slug; + map[providerSlug] = map[providerSlug] ?? []; + map[providerSlug].push(i); + } } return map; }, [messagingIntegrationsQuery.data]); @@ -92,19 +186,8 @@ export default function IssueAlertNotificationOptions() { }, [messagingIntegrationsQuery.data]); const shouldRenderNotificationConfigs = useMemo(() => { - return selectedValues.some(v => v !== MultipleCheckboxOptions.EMAIL); - }, [selectedValues]); - - const onChange = values => { - setSelectedValues(values); - setAction( - values.map(v => - v === MultipleCheckboxOptions.INTEGRATION && provider - ? providerDetails[provider].action - : IssueAlertActionType.NOTIFY_EMAIL - ) - ); - }; + return actions.some(v => v !== MultipleCheckboxOptions.EMAIL); + }, [actions]); if (messagingIntegrationsQuery.isLoading || messagingIntegrationsQuery.isError) { return null; @@ -112,7 +195,11 @@ export default function IssueAlertNotificationOptions() { return (
- + setActions(values)} + > Notify via email @@ -124,6 +211,7 @@ export default function IssueAlertNotificationOptions() { {shouldRenderNotificationConfigs && ( )} @@ -133,7 +221,7 @@ export default function IssueAlertNotificationOptions() { {shouldRenderSetupButton && ( void; type Props = DeprecatedAsyncComponent['props'] & { + notificationProps: IssueAlertNotificationProps; onChange: StateUpdater; organization: Organization; alertSetting?: string; @@ -304,11 +307,11 @@ class IssueAlertOptions extends DeprecatedAsyncComponent { onChange={alertSetting => this.setStateAndUpdateParents({alertSetting})} value={this.state.alertSetting} /> - {!this.props.organization.features.includes( + {this.props.organization.features.includes( 'messaging-integration-onboarding-project-creation' ) && parseInt(this.state.alertSetting, 10) !== RuleAction.CREATE_ALERT_LATER && ( - + )} ); diff --git a/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx b/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx index b2561a06176cf3..0841261f93be16 100644 --- a/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx +++ b/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx @@ -3,7 +3,6 @@ import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegr import {render, screen} from 'sentry-test/reactTestingLibrary'; import selectEvent from 'sentry-test/selectEvent'; -import {IssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; import MessagingIntegrationAlertRule from 'sentry/views/projectInstall/messagingIntegrationAlertRule'; describe('MessagingIntegrationAlertRule', function () { @@ -26,19 +25,19 @@ describe('MessagingIntegrationAlertRule', function () { }), ]; - const mockSetAlertNotificationChannel = jest.fn(); - const mockSetAlertNotificationIntegration = jest.fn(); - const mockSetAlertNotificationProvider = jest.fn(); + const mockSetChannel = jest.fn(); + const mockSetIntegration = jest.fn(); + const mockSetProvider = jest.fn(); - const issueAlertNotificationContextValue = { - alertNotificationAction: [], - alertNotificationChannel: 'channel', - alertNotificationIntegration: slackIntegrations[0], - alertNotificationProvider: 'slack', - setAlertNotificationAction: jest.fn(), - setAlertNotificationChannel: mockSetAlertNotificationChannel, - setAlertNotificationIntegration: mockSetAlertNotificationIntegration, - setAlertNotificationProvider: mockSetAlertNotificationProvider, + const notificationProps = { + actions: [], + channel: 'channel', + integration: slackIntegrations[0], + provider: 'slack', + setActions: jest.fn(), + setChannel: mockSetChannel, + setIntegration: mockSetIntegration, + setProvider: mockSetProvider, }; const providersToIntegrations = { @@ -47,34 +46,33 @@ describe('MessagingIntegrationAlertRule', function () { msteams: msteamsIntegrations, }; - const getComponent = props => ( - - - + const getComponent = () => ( + ); it('renders', function () { - render(getComponent({})); + render(getComponent()); expect(screen.getAllByRole('textbox')).toHaveLength(3); }); it('calls setter when new integration is selected', async function () { - render(getComponent({})); + render(getComponent()); await selectEvent.select( screen.getByText("Moo Deng's Workspace"), "Moo Waan's Workspace" ); - expect(mockSetAlertNotificationIntegration).toHaveBeenCalled(); + expect(mockSetIntegration).toHaveBeenCalled(); }); it('calls setters when new provider is selected', async function () { - render(getComponent({})); + render(getComponent()); await selectEvent.select(screen.getByText('Slack'), 'Discord'); - expect(mockSetAlertNotificationProvider).toHaveBeenCalled(); - expect(mockSetAlertNotificationIntegration).toHaveBeenCalled(); - expect(mockSetAlertNotificationChannel).toHaveBeenCalled(); + expect(mockSetProvider).toHaveBeenCalled(); + expect(mockSetIntegration).toHaveBeenCalled(); + expect(mockSetChannel).toHaveBeenCalled(); }); // it('disables integration select when there is only one option', function () { diff --git a/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx b/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx index dfa74dd076a680..5cbd412227120e 100644 --- a/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx +++ b/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx @@ -6,22 +6,22 @@ import Input from 'sentry/components/input'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {OrganizationIntegration} from 'sentry/types/integrations'; -import {useIssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; -import {providerDetails} from 'sentry/views/projectInstall/issueAlertNotificationOptions'; +import { + type IssueAlertNotificationProps, + providerDetails, +} from 'sentry/views/projectInstall/issueAlertNotificationOptions'; type Props = { + notificationProps: IssueAlertNotificationProps; providersToIntegrations: Record; }; -export default function MessagingIntegrationAlertRule({providersToIntegrations}: Props) { - const { - alertNotificationChannel: channel, - alertNotificationIntegration: integration, - alertNotificationProvider: provider, - setAlertNotificationChannel: setChannel, - setAlertNotificationIntegration: setIntegration, - setAlertNotificationProvider: setProvider, - } = useIssueAlertNotificationContext(); +export default function MessagingIntegrationAlertRule({ + notificationProps, + providersToIntegrations, +}: Props) { + const {channel, integration, provider, setChannel, setIntegration, setProvider} = + notificationProps; const providerOptions = useMemo( () => diff --git a/static/app/views/projectInstall/newProject.spec.tsx b/static/app/views/projectInstall/newProject.spec.tsx index be3c676089cc82..312c8b83939612 100644 --- a/static/app/views/projectInstall/newProject.spec.tsx +++ b/static/app/views/projectInstall/newProject.spec.tsx @@ -1,3 +1,5 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegrations'; import {MOCK_RESP_VERBOSE} from 'sentry-fixture/ruleConditions'; import {render} from 'sentry-test/reactTestingLibrary'; @@ -5,11 +7,23 @@ import {render} from 'sentry-test/reactTestingLibrary'; import NewProject from 'sentry/views/projectInstall/newProject'; describe('NewProjectPlatform', function () { + const organization = OrganizationFixture(); + const integrations = [ + OrganizationIntegrationsFixture({ + name: "Moo Deng's Workspace", + status: 'active', + }), + ]; + beforeEach(() => { MockApiClient.addMockResponse({ url: `/projects/org-slug/rule-conditions/`, body: MOCK_RESP_VERBOSE, }); + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/integrations/?integrationType=messaging`, + body: integrations, + }); }); afterEach(() => { From 565c186d1cebec0d0852a03c864745aefe233a21 Mon Sep 17 00:00:00 2001 From: Mia Hsu Date: Wed, 16 Oct 2024 17:30:21 -0700 Subject: [PATCH 3/6] dont create alert rule if set up later option is chosen --- .../views/projectInstall/createProject.spec.tsx | 11 ++++++++++- .../app/views/projectInstall/createProject.tsx | 12 ++++++++---- .../projectInstall/issueAlertOptions.spec.tsx | 9 ++------- .../views/projectInstall/issueAlertOptions.tsx | 17 ++++++++++++----- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/static/app/views/projectInstall/createProject.spec.tsx b/static/app/views/projectInstall/createProject.spec.tsx index 4d326126c0a621..47a2c896b17653 100644 --- a/static/app/views/projectInstall/createProject.spec.tsx +++ b/static/app/views/projectInstall/createProject.spec.tsx @@ -377,7 +377,9 @@ describe('CreateProject', function () { }); describe('Issue Alerts Options', function () { - const organization = OrganizationFixture(); + const organization = OrganizationFixture({ + features: ['messaging-integration-onboarding-project-creation'], + }); beforeEach(() => { TeamStore.loadUserTeams([teamWithAccess]); @@ -425,6 +427,13 @@ describe('CreateProject', function () { await userEvent.clear(screen.getByTestId('range-input')); expect(getSubmitButton()).toBeDisabled(); + await userEvent.click( + screen.getByRole('checkbox', { + name: /notify via integration \(slack, discord, ms teams, etc\.\)/i, + }) + ); + expect(getSubmitButton()).toBeDisabled(); + await userEvent.click(screen.getByText("I'll create my own alerts later")); expect(getSubmitButton()).toBeEnabled(); }); diff --git a/static/app/views/projectInstall/createProject.tsx b/static/app/views/projectInstall/createProject.tsx index 3e737cd3800f51..5f02bccf107a3b 100644 --- a/static/app/views/projectInstall/createProject.tsx +++ b/static/app/views/projectInstall/createProject.tsx @@ -106,6 +106,7 @@ function CreateProject() { async (selectedFramework?: OnboardingSelectedSDK) => { const {slug} = organization; const { + shouldCreateRule, shouldCreateCustomRule, name, conditions, @@ -157,7 +158,8 @@ function CreateProject() { if ( organization.features.includes( 'messaging-integration-onboarding-project-creation' - ) + ) && + shouldCreateRule ) { const ruleData = await createNotificationAction({ api, @@ -301,7 +303,7 @@ function CreateProject() { setProjectName(newName); } - const {shouldCreateCustomRule, conditions} = alertRuleConfig || {}; + const {shouldCreateRule, shouldCreateCustomRule, conditions} = alertRuleConfig || {}; const canUserCreateProject = canCreateProject(organization); const canCreateTeam = organization.access.includes('project:admin'); @@ -312,9 +314,11 @@ function CreateProject() { const isMissingAlertThreshold = shouldCreateCustomRule && !conditions?.every?.(condition => condition.value); const isMissingMessagingIntegrationChannel = + shouldCreateRule && alertNotificationActions?.some( action => action === MultipleCheckboxOptions.INTEGRATION - ) && !channel; + ) && + !channel; const formErrorCount = [ isMissingTeam, @@ -468,7 +472,7 @@ function CreateProject() { {Object.keys(errors).map(key => { const label = - key === 'actions' ? 'Notify via integration' : startCase(key); + key === 'actions' ? t('Notify via integration') : startCase(key); return (
{label}: {errors[key]} diff --git a/static/app/views/projectInstall/issueAlertOptions.spec.tsx b/static/app/views/projectInstall/issueAlertOptions.spec.tsx index 6833480387ace1..ae1076776ca687 100644 --- a/static/app/views/projectInstall/issueAlertOptions.spec.tsx +++ b/static/app/views/projectInstall/issueAlertOptions.spec.tsx @@ -10,7 +10,6 @@ import { import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import selectEvent from 'sentry-test/selectEvent'; -import {IssueAlertNotificationContext} from 'sentry/views/projectInstall/issueAlertNotificationContext'; import IssueAlertOptions from 'sentry/views/projectInstall/issueAlertOptions'; describe('IssueAlertOptions', function () { @@ -22,7 +21,7 @@ describe('IssueAlertOptions', function () { onChange: jest.fn(), }; - const issueAlertNotificationContextValue = { + const notificationProps = { alertNotificationAction: [], alertNotificationChannel: 'channel', alertNotificationIntegration: OrganizationIntegrationsFixture({ @@ -36,11 +35,7 @@ describe('IssueAlertOptions', function () { setAlertNotificationProvider: jest.fn(), }; - const getComponent = () => ( - - - - ); + const getComponent = () => ; beforeEach(() => { MockApiClient.addMockResponse({ diff --git a/static/app/views/projectInstall/issueAlertOptions.tsx b/static/app/views/projectInstall/issueAlertOptions.tsx index 7c35614b13c8ec..24ec10362b2933 100644 --- a/static/app/views/projectInstall/issueAlertOptions.tsx +++ b/static/app/views/projectInstall/issueAlertOptions.tsx @@ -44,12 +44,12 @@ const METRIC_CONDITION_MAP = { type StateUpdater = (updatedData: RequestDataFragment) => void; type Props = DeprecatedAsyncComponent['props'] & { - notificationProps: IssueAlertNotificationProps; onChange: StateUpdater; organization: Organization; alertSetting?: string; interval?: string; metric?: MetricValues; + notificationProps?: IssueAlertNotificationProps; platformLanguage?: SupportedLanguages; threshold?: string; }; @@ -73,6 +73,7 @@ type RequestDataFragment = { frequency: number; name: string; shouldCreateCustomRule: boolean; + shouldCreateRule: boolean; }; function getConditionFrom( @@ -196,27 +197,32 @@ class IssueAlertOptions extends DeprecatedAsyncComponent { getUpdatedData(): RequestDataFragment { let defaultRules: boolean; + let shouldCreateRule: boolean; let shouldCreateCustomRule: boolean; const alertSetting: RuleAction = parseInt(this.state.alertSetting, 10); switch (alertSetting) { case RuleAction.DEFAULT_ALERT: defaultRules = true; - shouldCreateCustomRule = false; - break; - case RuleAction.CREATE_ALERT_LATER: - defaultRules = false; + shouldCreateRule = true; shouldCreateCustomRule = false; break; case RuleAction.CUSTOMIZED_ALERTS: defaultRules = false; + shouldCreateRule = true; shouldCreateCustomRule = true; break; + case RuleAction.CREATE_ALERT_LATER: + defaultRules = false; + shouldCreateRule = false; + shouldCreateCustomRule = false; + break; default: throw new RangeError('Supplied alert creation action is not handled'); } return { defaultRules, + shouldCreateRule, shouldCreateCustomRule, name: 'Send a notification for new issues', conditions: @@ -310,6 +316,7 @@ class IssueAlertOptions extends DeprecatedAsyncComponent { {this.props.organization.features.includes( 'messaging-integration-onboarding-project-creation' ) && + this.props.notificationProps && parseInt(this.state.alertSetting, 10) !== RuleAction.CREATE_ALERT_LATER && ( )} From 35032f2f87c1941a53b442974ddbd00e1720f88e Mon Sep 17 00:00:00 2001 From: Mia Hsu Date: Wed, 16 Oct 2024 17:40:26 -0700 Subject: [PATCH 4/6] SWITCH STATEMENT YAY --- .../issueAlertNotificationOptions.tsx | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/static/app/views/projectInstall/issueAlertNotificationOptions.tsx b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx index 6ff2cb9d11226a..d18a0c54d00c0f 100644 --- a/static/app/views/projectInstall/issueAlertNotificationOptions.tsx +++ b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx @@ -67,34 +67,34 @@ export function useCreateNotificationAction() { if (!isCreatingIntegrationNotification) { return undefined; } - if (provider === 'slack') { - return [ - { - id: IssueAlertActionType.SLACK, - workspace: integration?.id, - channel: channel, - }, - ]; - } - if (provider === 'discord') { - return [ - { - id: IssueAlertActionType.DISCORD, - server: integration?.id, - channel_id: channel, - }, - ]; - } - if (provider === 'msteams') { - return [ - { - id: IssueAlertActionType.MS_TEAMS, - team: integration?.id, - channel: channel, - }, - ]; + switch (provider) { + case 'slack': + return [ + { + id: IssueAlertActionType.SLACK, + workspace: integration?.id, + channel: channel, + }, + ]; + case 'discord': + return [ + { + id: IssueAlertActionType.DISCORD, + server: integration?.id, + channel_id: channel, + }, + ]; + case 'msteams': + return [ + { + id: IssueAlertActionType.MS_TEAMS, + team: integration?.id, + channel: channel, + }, + ]; + default: + return undefined; } - return undefined; }, [actions, integration, channel, provider]); type Props = { From a9db89b6061d48c30be7d5428bc629663a614bee Mon Sep 17 00:00:00 2001 From: Mia Hsu Date: Thu, 17 Oct 2024 00:37:53 -0700 Subject: [PATCH 5/6] one less useMemo --- .../issueAlertNotificationOptions.tsx | 78 +++++++++---------- 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/static/app/views/projectInstall/issueAlertNotificationOptions.tsx b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx index d18a0c54d00c0f..dbdf74e936c518 100644 --- a/static/app/views/projectInstall/issueAlertNotificationOptions.tsx +++ b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx @@ -60,43 +60,6 @@ export function useCreateNotificationAction() { ); const [channel, setChannel] = useState(undefined); - const integrationAction = useMemo(() => { - const isCreatingIntegrationNotification = actions.find( - action => action === MultipleCheckboxOptions.INTEGRATION - ); - if (!isCreatingIntegrationNotification) { - return undefined; - } - switch (provider) { - case 'slack': - return [ - { - id: IssueAlertActionType.SLACK, - workspace: integration?.id, - channel: channel, - }, - ]; - case 'discord': - return [ - { - id: IssueAlertActionType.DISCORD, - server: integration?.id, - channel_id: channel, - }, - ]; - case 'msteams': - return [ - { - id: IssueAlertActionType.MS_TEAMS, - team: integration?.id, - channel: channel, - }, - ]; - default: - return undefined; - } - }, [actions, integration, channel, provider]); - type Props = { actionMatch: string | undefined; api: Client; @@ -117,8 +80,43 @@ export function useCreateNotificationAction() { actionMatch, frequency, }: Props) => { - if (!integrationAction) { - return null; + let integrationAction; + const isCreatingIntegrationNotification = actions.find( + action => action === MultipleCheckboxOptions.INTEGRATION + ); + if (!isCreatingIntegrationNotification) { + return undefined; + } + switch (provider) { + case 'slack': + integrationAction = [ + { + id: IssueAlertActionType.SLACK, + workspace: integration?.id, + channel: channel, + }, + ]; + break; + case 'discord': + integrationAction = [ + { + id: IssueAlertActionType.DISCORD, + server: integration?.id, + channel_id: channel, + }, + ]; + break; + case 'msteams': + integrationAction = [ + { + id: IssueAlertActionType.MS_TEAMS, + team: integration?.id, + channel: channel, + }, + ]; + break; + default: + return undefined; } return api.requestPromise(`/projects/${organizationSlug}/${projectSlug}/rules/`, { method: 'POST', @@ -131,7 +129,7 @@ export function useCreateNotificationAction() { }, }); }, - [integrationAction] + [actions, provider, integration, channel] ); return { From 68d0db28160ff8d27372dac12954b68f212d6e49 Mon Sep 17 00:00:00 2001 From: Mia Hsu Date: Thu, 17 Oct 2024 11:44:47 -0700 Subject: [PATCH 6/6] translation + other fixes --- .../projectInstall/createProject.spec.tsx | 2 +- .../views/projectInstall/createProject.tsx | 76 ++++++----------- .../issueAlertNotificationOptions.tsx | 85 ++++++++++++++----- .../messagingIntegrationAlertRule.spec.tsx | 33 ++++--- .../messagingIntegrationAlertRule.tsx | 78 ++++++++--------- 5 files changed, 153 insertions(+), 121 deletions(-) diff --git a/static/app/views/projectInstall/createProject.spec.tsx b/static/app/views/projectInstall/createProject.spec.tsx index 47a2c896b17653..a51d0f28d6fea4 100644 --- a/static/app/views/projectInstall/createProject.spec.tsx +++ b/static/app/views/projectInstall/createProject.spec.tsx @@ -429,7 +429,7 @@ describe('CreateProject', function () { await userEvent.click( screen.getByRole('checkbox', { - name: /notify via integration \(slack, discord, ms teams, etc\.\)/i, + name: 'Notify via integration (Slack, Discord, MS Teams, etc.)', }) ); expect(getSubmitButton()).toBeDisabled(); diff --git a/static/app/views/projectInstall/createProject.tsx b/static/app/views/projectInstall/createProject.tsx index 5f02bccf107a3b..3fe0324d287890 100644 --- a/static/app/views/projectInstall/createProject.tsx +++ b/static/app/views/projectInstall/createProject.tsx @@ -86,17 +86,7 @@ function CreateProject() { undefined ); - const { - createNotificationAction, - actions: alertNotificationActions, - provider, - integration, - channel, - setActions: setAlertNotificationActions, - setProvider, - setIntegration, - setChannel, - } = useCreateNotificationAction(); + const {createNotificationAction, notificationProps} = useCreateNotificationAction(); const frameworkSelectionEnabled = !!organization?.features.includes( 'onboarding-sdk-selection' @@ -155,24 +145,16 @@ function CreateProject() { ); ruleIds.push(ruleData.id); } - if ( - organization.features.includes( - 'messaging-integration-onboarding-project-creation' - ) && - shouldCreateRule - ) { - const ruleData = await createNotificationAction({ - api, - name, - organizationSlug: organization.slug, - projectSlug: projectData.slug, - conditions, - actionMatch, - frequency, - }); - if (ruleData) { - ruleIds.push(ruleData.id); - } + const ruleData = await createNotificationAction({ + shouldCreateRule, + name, + projectSlug: projectData.slug, + conditions, + actionMatch, + frequency, + }); + if (ruleData) { + ruleIds.push(ruleData.id); } trackAnalytics('project_creation_page.created', { organization, @@ -315,10 +297,10 @@ function CreateProject() { shouldCreateCustomRule && !conditions?.every?.(condition => condition.value); const isMissingMessagingIntegrationChannel = shouldCreateRule && - alertNotificationActions?.some( + notificationProps.actions?.some( action => action === MultipleCheckboxOptions.INTEGRATION ) && - !channel; + !notificationProps.channel; const formErrorCount = [ isMissingTeam, @@ -342,6 +324,10 @@ function CreateProject() { ); } + const keyToErrorText = { + actions: t('Notify via integration'), + }; + const alertFrequencyDefaultValues = useMemo(() => { if (!autoFill) { return {}; @@ -403,16 +389,7 @@ function CreateProject() { {...alertFrequencyDefaultValues} platformLanguage={platform?.language as SupportedLanguages} onChange={updatedData => setAlertRuleConfig(updatedData)} - notificationProps={{ - actions: alertNotificationActions, - channel, - integration, - provider, - setActions: setAlertNotificationActions, - setChannel, - setIntegration, - setProvider, - }} + notificationProps={notificationProps} /> {t('Name your project and assign it a team')} - {Object.keys(errors).map(key => { - const label = - key === 'actions' ? t('Notify via integration') : startCase(key); - return ( -
- {label}: {errors[key]} -
- ); - })} + {Object.keys(errors).map(key => ( +
+ + {keyToErrorText[key] ? keyToErrorText[key] : startCase(key)} + + : {errors[key]} +
+ ))} )} diff --git a/static/app/views/projectInstall/issueAlertNotificationOptions.tsx b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx index dbdf74e936c518..11a0940ffb4cac 100644 --- a/static/app/views/projectInstall/issueAlertNotificationOptions.tsx +++ b/static/app/views/projectInstall/issueAlertNotificationOptions.tsx @@ -1,12 +1,13 @@ import {useCallback, useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; -import type {Client} from 'sentry/api'; import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox'; +import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {IssueAlertActionType} from 'sentry/types/alerts'; import type {OrganizationIntegration} from 'sentry/types/integrations'; import {useApiQuery} from 'sentry/utils/queryClient'; +import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; import SetupMessagingIntegrationButton, { MessagingIntegrationAnalyticsView, @@ -15,22 +16,43 @@ import MessagingIntegrationAlertRule from 'sentry/views/projectInstall/messaging export const providerDetails = { slack: { - name: 'Slack', + name: t('Slack'), action: IssueAlertActionType.SLACK, - label: 'workspace to', - placeholder: 'channel, e.g. #critical', + placeholder: t('channel, e.g. #critical'), + makeSentence: ({providerName, integrationName, target}) => + tct( + 'Send [providerName] notification to the [integrationName] workspace to [target]', + { + providerName, + integrationName, + target, + } + ), }, discord: { name: 'Discord', action: IssueAlertActionType.DISCORD, - label: 'server in the channel', placeholder: 'channel ID or URL', + makeSentence: ({providerName, integrationName, target}) => + tct( + 'Send [providerName] notification to the [integrationName] server in the channel [target]', + { + providerName, + integrationName, + target, + } + ), }, msteams: { name: 'MSTeams', action: IssueAlertActionType.MS_TEAMS, - label: 'team to', placeholder: 'channel ID', + makeSentence: ({providerName, integrationName, target}) => + tct('Send [providerName] notification to the [integrationName] team to [target]', { + providerName, + integrationName, + target, + }), }, }; @@ -60,33 +82,41 @@ export function useCreateNotificationAction() { ); const [channel, setChannel] = useState(undefined); + const api = useApi(); + const organization = useOrganization(); + type Props = { actionMatch: string | undefined; - api: Client; conditions: {id: string; interval: string; value: string}[] | undefined; frequency: number | undefined; name: string | undefined; - organizationSlug: string; projectSlug: string; + shouldCreateRule: boolean | undefined; }; const createNotificationAction = useCallback( ({ - api, - organizationSlug, + shouldCreateRule, projectSlug, name, conditions, actionMatch, frequency, }: Props) => { - let integrationAction; const isCreatingIntegrationNotification = actions.find( action => action === MultipleCheckboxOptions.INTEGRATION ); - if (!isCreatingIntegrationNotification) { + if ( + !organization.features.includes( + 'messaging-integration-onboarding-project-creation' + ) || + !shouldCreateRule || + !isCreatingIntegrationNotification + ) { return undefined; } + + let integrationAction; switch (provider) { case 'slack': integrationAction = [ @@ -118,7 +148,8 @@ export function useCreateNotificationAction() { default: return undefined; } - return api.requestPromise(`/projects/${organizationSlug}/${projectSlug}/rules/`, { + + return api.requestPromise(`/projects/${organization.slug}/${projectSlug}/rules/`, { method: 'POST', data: { name, @@ -129,19 +160,29 @@ export function useCreateNotificationAction() { }, }); }, - [actions, provider, integration, channel] + [ + actions, + api, + provider, + integration, + channel, + organization.features, + organization.slug, + ] ); return { createNotificationAction, - actions, - provider, - integration, - channel, - setActions, - setProvider, - setIntegration, - setChannel, + notificationProps: { + actions, + provider, + integration, + channel, + setActions, + setProvider, + setIntegration, + setChannel, + }, }; } diff --git a/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx b/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx index 0841261f93be16..a22375a5184563 100644 --- a/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx +++ b/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx @@ -75,14 +75,27 @@ describe('MessagingIntegrationAlertRule', function () { expect(mockSetChannel).toHaveBeenCalled(); }); - // it('disables integration select when there is only one option', function () { - // render( - // getComponent({ - // alertNotificationIntegration: discordIntegrations[0], - // alertNotificationProvider: 'discord', - // }) - // ); - // screen.getByRole('text').click(); - // expect(screen.getByRole('textbox')).toBeDisabled(); - // }); + it('disables provider select when there is only one provider option', function () { + render( + + ); + expect(screen.getByLabelText('provider')).toBeDisabled(); + }); + + it('disables integration select when there is only one integration option', function () { + render( + + ); + expect(screen.getByLabelText('integration')).toBeDisabled(); + }); }); diff --git a/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx b/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx index 5cbd412227120e..fc132e9e9ec16d 100644 --- a/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx +++ b/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import SelectControl from 'sentry/components/forms/controls/selectControl'; import Input from 'sentry/components/input'; -import {t} from 'sentry/locale'; +// import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {OrganizationIntegration} from 'sentry/types/integrations'; import { @@ -58,43 +58,45 @@ export default function MessagingIntegrationAlertRule({ } return ( -
- - - {t('Send')} - { - setProvider(p.value); - setIntegration(providersToIntegrations[p.value][0]); - setChannel(''); - }} - /> - {t('notification to the')} - setIntegration(i.value)} - /> - {providerDetails[provider]?.label} - ) => - setChannel(e.target.value) - } - /> - - -
+ + + {providerDetails[provider]?.makeSentence({ + providerName: ( + { + setProvider(p.value); + setIntegration(providersToIntegrations[p.value][0]); + setChannel(''); + }} + /> + ), + integrationName: ( + setIntegration(i.value)} + /> + ), + target: ( + ) => + setChannel(e.target.value) + } + /> + ), + })} + + ); }