(
- key: K,
- value: AlertRuleOptions[K]
- ) => void;
- notificationProps?: IssueAlertNotificationProps;
-}
-
-export function IssueAlertOptions({
- alertSetting = DEFAULT_ISSUE_ALERT_OPTIONS_VALUES.alertSetting,
- interval = DEFAULT_ISSUE_ALERT_OPTIONS_VALUES.interval,
- metric = DEFAULT_ISSUE_ALERT_OPTIONS_VALUES.metric,
- threshold = DEFAULT_ISSUE_ALERT_OPTIONS_VALUES.threshold,
- notificationProps,
- onFieldChange,
-}: IssueAlertOptionsProps) {
- const issueAlertOptionsChoices: Array<[RuleAction, React.ReactNode]> = [
- [RuleAction.DEFAULT_ALERT, t('Alert me on high priority issues')],
- [
- RuleAction.CUSTOMIZED_ALERTS,
- tct('When there are more than [threshold][metric] a unique error [interval]', {
- threshold: (
- // 80px is just enough to see 6 digits at a time
-
- {
- onFieldChange('threshold', e.target.value);
- }}
- data-test-id="range-input"
- />
-
- ),
- metric: (
- e.preventDefault()}>
-
- ),
- interval: (
- e.preventDefault()}>
-
- ),
- }),
- ],
- [RuleAction.CREATE_ALERT_LATER, t("I'll create my own alerts later")],
- ];
-
- return (
-
- [
- choiceValue.toString(),
-
- {node}
- ,
- ])}
- label={t('Options for creating an alert')}
- onChange={val => {
- const selectedAlertSetting = parseRuleAction(val);
- onFieldChange('alertSetting', selectedAlertSetting);
- }}
- value={alertSetting.toString()}
- />
- {notificationProps && alertSetting !== RuleAction.CREATE_ALERT_LATER && (
-
- )}
-
- );
-}
-
-const Content = styled('div')`
- padding-top: ${p => p.theme.space.xl};
- padding-bottom: ${p => p.theme.space['3xl']};
- display: flex;
- flex-direction: column;
- gap: ${p => p.theme.space['3xl']};
-`;
diff --git a/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx b/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx
index 0bd94a7ada31..33ec5cf0676b 100644
--- a/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx
+++ b/static/app/views/projectInstall/messagingIntegrationAlertRule.spec.tsx
@@ -1,361 +1,10 @@
import {OrganizationFixture} from 'sentry-fixture/organization';
import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegrations';
-import {
- render,
- renderHookWithProviders,
- screen,
- userEvent,
- waitFor,
-} from 'sentry-test/reactTestingLibrary';
-import {selectEvent} from 'sentry-test/selectEvent';
+import {renderHookWithProviders} from 'sentry-test/reactTestingLibrary';
import * as analytics from 'sentry/utils/analytics';
-import type {IssueAlertNotificationProps} from 'sentry/views/projectInstall/issueAlertNotificationOptions';
-import {
- MessagingIntegrationAlertRule,
- useMessagingIntegrationAlertRule,
-} from 'sentry/views/projectInstall/messagingIntegrationAlertRule';
-describe('MessagingIntegrationAlertRule', () => {
- const organization = OrganizationFixture();
- 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 providersToIntegrations = {
- slack: slackIntegrations,
- discord: discordIntegrations,
- msteams: msteamsIntegrations,
- };
-
- const mockSetChannel = jest.fn();
- const mockSetIntegration = jest.fn();
- const mockSetProvider = jest.fn();
-
- const notificationProps: IssueAlertNotificationProps = {
- actions: [],
- channel: {
- label: 'channel',
- value: 'channel',
- },
- integration: slackIntegrations[0],
- provider: 'slack',
- providersToIntegrations,
- queryError: false,
- querySuccess: true,
- shouldRenderSetupButton: false,
- setActions: jest.fn(),
- setChannel: mockSetChannel,
- setIntegration: mockSetIntegration,
- setProvider: mockSetProvider,
- };
-
- beforeEach(() => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${slackIntegrations[0]!.id}/channels/`,
- body: {
- results: [],
- },
- });
- });
-
- const getComponent = () => ;
- const getChannelSelect = () => screen.getByRole('textbox', {name: 'channel'});
-
- it('renders', () => {
- render(getComponent());
- expect(screen.getAllByRole('textbox')).toHaveLength(3);
- });
-
- it('clears the channel select when channel prop becomes undefined', () => {
- const {rerender} = render(getComponent(), {organization});
-
- // The initial channel value label is visible.
- expect(screen.getByText('channel')).toBeInTheDocument();
-
- // Parent state clears channel (e.g. after provider or integration change).
- rerender(
-
- );
-
- // The stale channel label must no longer be shown; the select is empty.
- expect(screen.queryByText('channel')).not.toBeInTheDocument();
- });
-
- it('calls setter when new integration is selected', async () => {
- render(getComponent());
- await selectEvent.select(
- screen.getByText("Moo Deng's Workspace"),
- "Moo Waan's Workspace"
- );
- expect(mockSetIntegration).toHaveBeenCalled();
- });
-
- it('calls setters when new provider is selected', async () => {
- render(getComponent());
- await selectEvent.select(screen.getByText('Slack'), 'Discord');
- expect(mockSetProvider).toHaveBeenCalled();
- expect(mockSetIntegration).toHaveBeenCalled();
- expect(mockSetChannel).toHaveBeenCalled();
- });
-
- it('disables provider select when there is only one provider option', () => {
- render(
-
- );
- expect(screen.getByLabelText('provider')).toBeDisabled();
- });
-
- it('disables integration select when there is only one integration option', () => {
- render(
-
- );
- expect(screen.getByLabelText('integration')).toBeDisabled();
- });
-
- it('loads channels', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${discordIntegrations[0]!.id}/channels/`,
- body: {
- nextCursor: null,
- results: [
- {id: '1', name: 'general', display: '#general', type: 'text'},
- {id: '2', name: 'alerts', display: '#alerts', type: 'text'},
- ],
- },
- });
- render(
-
- );
- await selectEvent.openMenu(getChannelSelect());
- expect(await screen.findByText('#general (1)')).toBeInTheDocument();
- expect(screen.getByText('#alerts (2)')).toBeInTheDocument();
- await selectEvent.select(getChannelSelect(), /#alerts/);
- expect(mockSetChannel).toHaveBeenCalledWith({
- label: '#alerts (2)',
- value: '2',
- new: false,
- channelId: '2',
- channelName: '#alerts',
- });
- });
-
- it('shows the selected channel when no channels are returned', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${discordIntegrations[0]!.id}/channels/`,
- body: {
- nextCursor: null,
- results: [],
- },
- });
-
- render(
-
- );
-
- await selectEvent.openMenu(getChannelSelect());
- expect(
- await screen.findByRole('menuitemradio', {name: 'channel'})
- ).toBeInTheDocument();
- expect(mockSetChannel).not.toHaveBeenCalled();
- });
-
- it('set custom channel as "new" when created', async () => {
- render(getComponent());
-
- await selectEvent.create(getChannelSelect(), '#custom-channel', {
- waitForElement: false,
- createOptionText: '#custom-channel',
- });
-
- expect(mockSetChannel).toHaveBeenCalledWith({
- label: '#custom-channel',
- value: '#custom-channel',
- new: true,
- });
- });
-
- it('validates custom channel when created', async () => {
- const validationRequest = MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${slackIntegrations[0]!.id}/channel-validate/`,
- body: {valid: true},
- });
-
- render(
-
- );
-
- await waitFor(() => {
- expect(validationRequest).toHaveBeenCalledWith(
- expect.anything(),
- expect.objectContaining({
- query: {channel: '#custom-channel'},
- })
- );
- });
- });
-
- it('displays validation error when channel is invalid', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${slackIntegrations[0]!.id}/channel-validate/`,
- body: {valid: false, detail: 'Channel not found'},
- });
-
- render(
-
- );
-
- expect(await screen.findByText('Channel not found')).toBeInTheDocument();
- });
-
- it('displays default error message when validation fails without detail', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${slackIntegrations[0]!.id}/channel-validate/`,
- body: {valid: false},
- });
-
- render(
-
- );
-
- expect(
- await screen.findByText('Channel not found or restricted')
- ).toBeInTheDocument();
- });
-
- it('displays error when validation request fails', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${slackIntegrations[0]!.id}/channel-validate/`,
- statusCode: 500,
- body: {detail: 'Internal Error'},
- });
-
- render(
-
- );
-
- expect(
- await screen.findByText('Unexpected integration channel validation error')
- ).toBeInTheDocument();
- });
-
- it('clears the selected channel', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${slackIntegrations[0]!.id}/channel-validate/`,
- body: {valid: false, detail: 'Channel not found'},
- });
-
- render(
-
- );
-
- expect(await screen.findByText('Channel not found')).toBeInTheDocument();
- await userEvent.click(screen.getByLabelText('Clear choices'));
- expect(mockSetChannel).toHaveBeenCalledWith(undefined);
- });
-
- it('changes provider after a validation error', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${slackIntegrations[0]!.id}/channel-validate/`,
- body: {valid: false, detail: 'Channel not found'},
- });
-
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${discordIntegrations[0]!.id}/channels/`,
- body: {results: []},
- });
-
- render(
-
- );
-
- expect(await screen.findByText('Channel not found')).toBeInTheDocument();
-
- await selectEvent.select(screen.getByText('Slack'), 'Discord');
- expect(mockSetProvider).toHaveBeenCalledWith('discord');
- });
-
- it('changes integration after a validation error', async () => {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${slackIntegrations[0]!.id}/channel-validate/`,
- body: {valid: false, detail: 'Channel not found'},
- });
-
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/integrations/${slackIntegrations[1]!.id}/channels/`,
- body: {results: []},
- });
-
- render(
-
- );
-
- expect(await screen.findByText('Channel not found')).toBeInTheDocument();
-
- await selectEvent.select(
- screen.getByText("Moo Deng's Workspace"),
- "Moo Waan's Workspace"
- );
- expect(mockSetIntegration).toHaveBeenCalledWith(slackIntegrations[1]);
- });
-});
+import {useMessagingIntegrationAlertRule} from 'sentry/views/projectInstall/messagingIntegrationAlertRule';
describe('useMessagingIntegrationAlertRule change analytics', () => {
const organization = OrganizationFixture();
diff --git a/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx b/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx
index d0e8d9aebb43..cf9bc2c7137d 100644
--- a/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx
+++ b/static/app/views/projectInstall/messagingIntegrationAlertRule.tsx
@@ -197,89 +197,6 @@ export function ChannelSelect({
);
}
-export function MessagingIntegrationAlertRule(props: IssueAlertNotificationProps) {
- const {
- provider,
- integration,
- channel,
- providerOptions,
- integrationOptions,
- channelOptions,
- isChannelLoading,
- channelError,
- providerDisabled,
- integrationDisabled,
- onProviderChange,
- onIntegrationChange,
- onChannelChange,
- onCreateChannel,
- } = useMessagingIntegrationAlertRule(props, 'legacy');
-
- if (!provider) {
- return null;
- }
-
- return (
-
- {providerDetails[provider as keyof typeof providerDetails]?.makeSentence({
- providerName: (
-
- ),
- integrationName: (
-
- ),
- target: (
-
- {() => (
-
- )}
-
- ),
- })}
-
- );
-}
-
-const Rule = styled('div')`
- padding: ${p => p.theme.space.md};
- background-color: ${p => p.theme.tokens.background.secondary};
- border-radius: ${p => p.theme.radius.md};
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: ${p => p.theme.space.md};
-`;
-
-const InlineSelectControl = styled(Select)`
- width: 180px;
-`;
-
-// Preserves the classic inline channel-select width.
-const InlineChannelSelect = styled(ChannelSelect)`
- min-width: 220px;
-`;
-
-/** @public Consumed by the SCM layout in a downstream PR. */
export const ChannelField = styled(FormField)`
padding: 0;
`;