Skip to content

Commit 324537d

Browse files
author
David Wang
authored
feat(crons): Disallow teams not part of project to be selected for notifications (#69073)
Adds a new `memberOfProjectSlug` prop to the sentryMemberTeamSelectorField to restrict selectable teams to only be the ones that are associated with the provided project slug <img width="582" alt="image" src="https://github.com/getsentry/sentry/assets/9372512/432ba671-91e9-4d35-8975-dec29709646c"> related to: #63814
1 parent b89c554 commit 324537d

File tree

3 files changed

+84
-13
lines changed

3 files changed

+84
-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';
@@ -125,4 +126,36 @@ describe('SentryMemberTeamSelectorField', () => {
125126
await userEvent.click(screen.getByLabelText('Clear choices'));
126127
expect(mock).toHaveBeenCalledWith([], expect.anything());
127128
});
129+
130+
it('disables teams not associated with project', async () => {
131+
const project = ProjectFixture();
132+
const teamWithProject = TeamFixture({projects: [project], slug: 'my-team'});
133+
const teamWithoutProject = TeamFixture({id: '2', slug: 'disabled-team'});
134+
TeamStore.init();
135+
TeamStore.loadInitialData([teamWithProject, teamWithoutProject]);
136+
137+
const mock = jest.fn();
138+
render(
139+
<SentryMemberTeamSelectorField
140+
label="Select Owner"
141+
onChange={mock}
142+
memberOfProjectSlug={project.slug}
143+
name="team-or-member"
144+
multiple
145+
/>
146+
);
147+
148+
await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Select Owner'}));
149+
expect(
150+
within(
151+
(await screen.findByText('My Teams')).parentElement as HTMLElement
152+
).getByText('#my-team')
153+
).toBeInTheDocument();
154+
155+
expect(
156+
within(
157+
(await screen.findByText('Disabled Teams')).parentElement as HTMLElement
158+
).getByText('#disabled-team')
159+
).toBeInTheDocument();
160+
});
128161
});

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

Lines changed: 38 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,33 @@ 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+
memberOfProjectSlug && !team.projects.some(({slug}) => memberOfProjectSlug === slug)
112+
? 'disabledTeams'
113+
: team.isMember
114+
? 'memberTeams'
115+
: 'otherTeams'
116+
);
117+
118+
const myTeamOptions = memberTeams?.map(makeTeamOption) ?? [];
119+
const otherTeamOptions = otherTeams?.map(makeTeamOption) ?? [];
120+
const disabledTeamOptions = disabledTeams?.map(makeDisabledTeamOption) ?? [];
92121

93122
// TODO(epurkhiser): This is an unfortunate hack right now since we don't
94123
// actually load members anywhere and the useMembers and useTeams hook don't
@@ -128,6 +157,10 @@ function SentryMemberTeamSelectorField({
128157
label: t('Other Teams'),
129158
options: otherTeamOptions,
130159
},
160+
{
161+
label: t('Disabled Teams'),
162+
options: disabledTeamOptions,
163+
},
131164
]}
132165
{...props}
133166
/>

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)