-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat(aci): Require at least one connected monitor in the alert builder #113246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import {Fragment, useCallback, useContext, useEffect, useRef, useState} from 'react'; | ||
| import styled from '@emotion/styled'; | ||
|
|
||
| import {Alert} from '@sentry/scraps/alert'; | ||
| import {Button, LinkButton} from '@sentry/scraps/button'; | ||
| import {Container, Flex, Stack} from '@sentry/scraps/layout'; | ||
|
|
||
|
|
@@ -22,6 +23,7 @@ import type {Detector} from 'sentry/types/workflowEngine/detectors'; | |
| import {useQueryClient} from 'sentry/utils/queryClient'; | ||
| import {useOrganization} from 'sentry/utils/useOrganization'; | ||
| import {useProjects} from 'sentry/utils/useProjects'; | ||
| import {AutomationBuilderErrorContext} from 'sentry/views/automations/components/automationBuilderErrorContext'; | ||
| import {ConnectedMonitorsList} from 'sentry/views/automations/components/connectedMonitorsList'; | ||
| import {useConnectedDetectors} from 'sentry/views/automations/hooks/useConnectedDetectors'; | ||
| import {DetectorSearch} from 'sentry/views/detectors/components/detectorSearch'; | ||
|
|
@@ -284,13 +286,16 @@ function SpecificMonitorsSection({ | |
| ); | ||
| } | ||
|
|
||
| export const CONNECTED_MONITORS_ERROR_ID = 'connectedMonitors'; | ||
|
|
||
| function EditConnectedMonitorsContent({ | ||
| initialMode, | ||
| connectedIds, | ||
| setConnectedIds, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not related to this pr and a bit of a nit.. but might be nice to get this method from context or something so it's clearer where the connected id's are being stored. |
||
| }: ContentProps) { | ||
| const [monitorMode, setMonitorMode] = useState<MonitorMode>(initialMode); | ||
| const {form} = useContext(FormContext); | ||
| const errorContext = useContext(AutomationBuilderErrorContext); | ||
|
|
||
| const handleModeChange = useCallback( | ||
| (newMode: MonitorMode) => { | ||
|
|
@@ -302,11 +307,23 @@ function EditConnectedMonitorsContent({ | |
| ); | ||
| const handleProjectChange = useCallback( | ||
| (projectIds: string[]) => { | ||
| if (!projectIds.length) { | ||
| if (projectIds.length) { | ||
| errorContext?.removeError(CONNECTED_MONITORS_ERROR_ID); | ||
| } else { | ||
| setConnectedIds([]); | ||
| } | ||
| }, | ||
| [setConnectedIds] | ||
| [setConnectedIds, errorContext] | ||
| ); | ||
|
|
||
| const handleSetConnectedIds = useCallback( | ||
| (ids: Automation['detectorIds']) => { | ||
| setConnectedIds(ids); | ||
| if (ids.length) { | ||
| errorContext?.removeError(CONNECTED_MONITORS_ERROR_ID); | ||
| } | ||
| }, | ||
| [setConnectedIds, errorContext] | ||
| ); | ||
|
|
||
| return ( | ||
|
|
@@ -332,9 +349,14 @@ function EditConnectedMonitorsContent({ | |
| ) : ( | ||
| <SpecificMonitorsSection | ||
| connectedIds={connectedIds} | ||
| setConnectedIds={setConnectedIds} | ||
| setConnectedIds={handleSetConnectedIds} | ||
| /> | ||
| )} | ||
| {errorContext?.errors[CONNECTED_MONITORS_ERROR_ID] && ( | ||
| <Alert variant="danger"> | ||
| {errorContext.errors[CONNECTED_MONITORS_ERROR_ID]} | ||
| </Alert> | ||
| )} | ||
| </Stack> | ||
| </FormSection> | ||
| </WorkflowEngineContainer> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import {AutomationFixture} from 'sentry-fixture/automations'; | |
| import {IssueStreamDetectorFixture} from 'sentry-fixture/detectors'; | ||
| import {MemberFixture} from 'sentry-fixture/member'; | ||
| import {OrganizationFixture} from 'sentry-fixture/organization'; | ||
| import {PageFiltersFixture} from 'sentry-fixture/pageFilters'; | ||
| import {ProjectFixture} from 'sentry-fixture/project'; | ||
| import {UserFixture} from 'sentry-fixture/user'; | ||
| import { | ||
|
|
@@ -13,6 +14,7 @@ import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrar | |
| import {selectEvent} from 'sentry-test/selectEvent'; | ||
|
|
||
| import * as indicators from 'sentry/actionCreators/indicator'; | ||
| import {PageFiltersStore} from 'sentry/components/pageFilters/store'; | ||
| import {ProjectsStore} from 'sentry/stores/projectsStore'; | ||
| import type {Action} from 'sentry/types/workflowEngine/actions'; | ||
| import {ActionGroup, ActionType} from 'sentry/types/workflowEngine/actions'; | ||
|
|
@@ -209,7 +211,12 @@ describe('AutomationNewSettings', () => { | |
| body: created, | ||
| }); | ||
|
|
||
| const {router} = render(<AutomationNewSettings />, {organization}); | ||
| const {router} = render(<AutomationNewSettings />, { | ||
| organization, | ||
| initialRouterConfig: { | ||
| location: {pathname: '/', query: {connectedIds: '123'}}, | ||
| }, | ||
| }); | ||
|
|
||
| // Add an action filter (tagged event) | ||
| await selectEvent.select( | ||
|
|
@@ -291,7 +298,7 @@ describe('AutomationNewSettings', () => { | |
| }, | ||
| ], | ||
| config: {frequency: 0}, | ||
| detectorIds: [], | ||
| detectorIds: ['123'], | ||
| enabled: true, | ||
| }, | ||
| }) | ||
|
|
@@ -329,7 +336,12 @@ describe('AutomationNewSettings', () => { | |
| body: created, | ||
| }); | ||
|
|
||
| render(<AutomationNewSettings />, {organization}); | ||
| render(<AutomationNewSettings />, { | ||
| organization, | ||
| initialRouterConfig: { | ||
| location: {pathname: '/', query: {connectedIds: '123'}}, | ||
| }, | ||
| }); | ||
|
|
||
| const addAction = async (label: string) => { | ||
| await selectEvent.select(screen.getByRole('textbox', {name: 'Add action'}), label); | ||
|
|
@@ -593,6 +605,68 @@ describe('AutomationNewSettings', () => { | |
| }); | ||
| }); | ||
|
|
||
| it('shows validation error when submitting with no projects or monitors selected', async () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we also add that it's cleared if something has been added?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes good catch! |
||
| ProjectsStore.loadInitialData([]); | ||
|
|
||
| const post = MockApiClient.addMockResponse({ | ||
| url: `/organizations/${organization.slug}/workflows/`, | ||
| method: 'POST', | ||
| body: AutomationFixture(), | ||
| }); | ||
|
|
||
| render(<AutomationNewSettings />, {organization}); | ||
|
|
||
| // Add an action so the builder validation passes | ||
| await selectEvent.select(screen.getByRole('textbox', {name: 'Add action'}), 'Slack'); | ||
| await userEvent.type(screen.getByRole('textbox', {name: 'Target'}), '#alerts'); | ||
|
|
||
| // Submit with no projects or monitors selected | ||
| await userEvent.click(screen.getByRole('button', {name: 'Create Alert'})); | ||
|
|
||
| // Should show validation error | ||
| expect( | ||
| await screen.findByText( | ||
| 'Select at least one project or monitor to create an alert.' | ||
| ) | ||
| ).toBeInTheDocument(); | ||
|
|
||
| // Should not have submitted | ||
| expect(post).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('pre-selects a project from page filters on first load', async () => { | ||
| const project = ProjectFixture({id: '2', slug: 'my-project', isMember: true}); | ||
| ProjectsStore.loadInitialData([project]); | ||
| PageFiltersStore.onInitializeUrlState( | ||
| PageFiltersFixture({projects: [Number(project.id)]}) | ||
| ); | ||
|
|
||
| render(<AutomationNewSettings />, {organization}); | ||
|
|
||
| // The project selector should show the pre-selected project | ||
| expect(await screen.findByText('my-project')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('pre-selects the first member project when "my projects" is selected', async () => { | ||
| const memberProject = ProjectFixture({ | ||
| id: '3', | ||
| slug: 'member-project', | ||
| isMember: true, | ||
| }); | ||
| const otherProject = ProjectFixture({ | ||
| id: '4', | ||
| slug: 'other-project', | ||
| isMember: false, | ||
| }); | ||
| ProjectsStore.loadInitialData([otherProject, memberProject]); | ||
| PageFiltersStore.onInitializeUrlState(PageFiltersFixture({projects: []})); | ||
|
|
||
| render(<AutomationNewSettings />, {organization}); | ||
|
|
||
| // Should pre-select the member project | ||
| expect(await screen.findByText('member-project')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('surfaces error details when test notification fails', async () => { | ||
| jest.spyOn(indicators, 'addErrorMessage'); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might be nice to have a comment as to why we don't validate in the drawer; (seems like because we're enforcing on to be there, but it's a little unclear why we don't want to validate that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, will add. We don't validate it in the drawer because we actually do want to create it without any connections (because when creating inline for a new monitor, the monitor hasn't been created yet)