Skip to content

Commit b73b802

Browse files
author
davidenwang
committed
feat(crons): Disallow teams not part of project to be selected for alerts
commit-id:3863ee1e
1 parent e849a2a commit b73b802

File tree

3 files changed

+87
-13
lines changed

3 files changed

+87
-13
lines changed

static/app/components/forms/fields/sentryMemberTeamSelectorField.spec.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {OrganizationFixture} from 'sentry-fixture/organization';
2+
import {ProjectFixture} from 'sentry-fixture/project';
23
import {TeamFixture} from 'sentry-fixture/team';
34
import {UserFixture} from 'sentry-fixture/user';
45

5-
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
6+
import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
67
import selectEvent from 'sentry-test/selectEvent';
78

89
import MemberListStore from 'sentry/stores/memberListStore';
@@ -104,4 +105,36 @@ describe('SentryMemberTeamSelectorField', () => {
104105
await userEvent.click(screen.getByLabelText('Clear choices'));
105106
expect(mock).toHaveBeenCalledWith([], expect.anything());
106107
});
108+
109+
it('disables teams not associated with project', async () => {
110+
const project = ProjectFixture();
111+
const teamWithProject = TeamFixture({projects: [project], slug: 'my-team'});
112+
const teamWithoutProject = TeamFixture({id: '2', slug: 'disabled-team'});
113+
TeamStore.init();
114+
TeamStore.loadInitialData([teamWithProject, teamWithoutProject]);
115+
116+
const mock = jest.fn();
117+
render(
118+
<SentryMemberTeamSelectorField
119+
label="Select Owner"
120+
onChange={mock}
121+
memberOfProjectSlug={project.slug}
122+
name="team-or-member"
123+
multiple
124+
/>
125+
);
126+
127+
await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Select Owner'}));
128+
expect(
129+
within(
130+
(await screen.findByText('My Teams')).parentElement as HTMLElement
131+
).getByText('#my-team')
132+
).toBeInTheDocument();
133+
134+
expect(
135+
within(
136+
(await screen.findByText('Disabled Teams')).parentElement as HTMLElement
137+
).getByText('#disabled-team')
138+
).toBeInTheDocument();
139+
});
107140
});

static/app/components/forms/fields/sentryMemberTeamSelectorField.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {useContext, useEffect, useMemo} from 'react';
2-
import partition from 'lodash/partition';
2+
import groupBy from 'lodash/groupBy';
33

44
import Avatar from 'sentry/components/avatar';
5+
import {Tooltip} from 'sentry/components/tooltip';
56
import {t} from 'sentry/locale';
6-
import type {Team} from 'sentry/types';
7+
import type {DetailedTeam, Team} from 'sentry/types';
78
import {useMembers} from 'sentry/utils/useMembers';
89
import {useTeams} from 'sentry/utils/useTeams';
910
import {useTeamsById} from 'sentry/utils/useTeamsById';
@@ -17,6 +18,10 @@ import SelectField from './selectField';
1718
// projects can be passed as a direct prop as well
1819
export interface RenderFieldProps extends SelectFieldProps<any> {
1920
avatarSize?: number;
21+
/**
22+
* Ensures the only selectable teams and members are members of the given project
23+
*/
24+
memberOfProjectSlug?: string;
2025
/**
2126
* Use the slug as the select field value. Without setting this the numeric id
2227
* of the project will be used.
@@ -27,6 +32,7 @@ export interface RenderFieldProps extends SelectFieldProps<any> {
2732
function SentryMemberTeamSelectorField({
2833
avatarSize = 20,
2934
placeholder = t('Choose Teams and Members'),
35+
memberOfProjectSlug,
3036
...props
3137
}: RenderFieldProps) {
3238
const {form} = useContext(FormContext);
@@ -85,10 +91,36 @@ function SentryMemberTeamSelectorField({
8591
leadingItems: <Avatar team={team} size={avatarSize} />,
8692
});
8793

88-
const [myTeams, otherTeams] = partition(teams, team => team.isMember);
94+
const makeDisabledTeamOption = (team: Team) => ({
95+
...makeTeamOption(team),
96+
disabled: true,
97+
label: (
98+
<Tooltip
99+
position="left"
100+
title={t('%s is not a member of the selected project', `#${team.slug}`)}
101+
>
102+
#{team.slug}
103+
</Tooltip>
104+
),
105+
});
89106

90-
const myTeamOptions = myTeams.map(makeTeamOption);
91-
const otherTeamOptions = otherTeams.map(makeTeamOption);
107+
// TODO(davidenwang): Fix the team type here to avoid this type cast: `as DetailedTeam[]`
108+
const {disabledTeams, memberTeams, otherTeams} = groupBy(
109+
teams as DetailedTeam[],
110+
team => {
111+
const isDisabled =
112+
memberOfProjectSlug &&
113+
!team.projects.some(({slug}) => memberOfProjectSlug === slug);
114+
if (isDisabled) {
115+
return 'disabledTeams';
116+
}
117+
return team.isMember ? 'memberTeams' : 'otherTeams';
118+
}
119+
);
120+
121+
const myTeamOptions = memberTeams?.map(makeTeamOption) ?? [];
122+
const otherTeamOptions = otherTeams?.map(makeTeamOption) ?? [];
123+
const disabledTeamOptions = disabledTeams?.map(makeDisabledTeamOption) ?? [];
92124

93125
// TODO(epurkhiser): This is an unfortunate hack right now since we don't
94126
// actually load members anywhere and the useMembers and useTeams hook don't
@@ -128,6 +160,10 @@ function SentryMemberTeamSelectorField({
128160
label: t('Other Teams'),
129161
options: otherTeamOptions,
130162
},
163+
{
164+
label: t('Disabled Teams'),
165+
options: disabledTeamOptions,
166+
},
131167
]}
132168
{...props}
133169
/>

static/app/views/monitors/components/monitorForm.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -504,13 +504,18 @@ function MonitorForm({
504504
{t('Customize this monitors notification configuration in Alerts')}
505505
</AlertLink>
506506
)}
507-
<SentryMemberTeamSelectorField
508-
label={t('Notify')}
509-
help={t('Send notifications to a member or team.')}
510-
name="alertRule.targets"
511-
multiple
512-
menuPlacement="auto"
513-
/>
507+
<Observer>
508+
{() => (
509+
<SentryMemberTeamSelectorField
510+
label={t('Notify')}
511+
help={t('Send notifications to a member or team.')}
512+
name="alertRule.targets"
513+
memberOfProjectSlug={form.current.getValue('project')?.toString()}
514+
multiple
515+
menuPlacement="auto"
516+
/>
517+
)}
518+
</Observer>
514519
<Observer>
515520
{() => {
516521
const selectedAssignee = form.current.getValue('alertRule.targets');

0 commit comments

Comments
 (0)