From 58dced4b4ce048d644d1d7a548b9b32d025ca37b Mon Sep 17 00:00:00 2001 From: davidenwang Date: Fri, 5 Apr 2024 19:07:01 +0900 Subject: [PATCH] feat(crons): Disallow teams not part of project to be selected for alerts commit-id:3863ee1e --- .../sentryMemberTeamSelectorField.spec.tsx | 35 ++++++++++++++- .../fields/sentryMemberTeamSelectorField.tsx | 43 ++++++++++++++++--- .../views/monitors/components/monitorForm.tsx | 19 +++++--- 3 files changed, 84 insertions(+), 13 deletions(-) diff --git a/static/app/components/forms/fields/sentryMemberTeamSelectorField.spec.tsx b/static/app/components/forms/fields/sentryMemberTeamSelectorField.spec.tsx index 5849cff094eadd..9213028c71bd62 100644 --- a/static/app/components/forms/fields/sentryMemberTeamSelectorField.spec.tsx +++ b/static/app/components/forms/fields/sentryMemberTeamSelectorField.spec.tsx @@ -1,8 +1,9 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ProjectFixture} from 'sentry-fixture/project'; import {TeamFixture} from 'sentry-fixture/team'; import {UserFixture} from 'sentry-fixture/user'; -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; import selectEvent from 'sentry-test/selectEvent'; import MemberListStore from 'sentry/stores/memberListStore'; @@ -104,4 +105,36 @@ describe('SentryMemberTeamSelectorField', () => { await userEvent.click(screen.getByLabelText('Clear choices')); expect(mock).toHaveBeenCalledWith([], expect.anything()); }); + + it('disables teams not associated with project', async () => { + const project = ProjectFixture(); + const teamWithProject = TeamFixture({projects: [project], slug: 'my-team'}); + const teamWithoutProject = TeamFixture({id: '2', slug: 'disabled-team'}); + TeamStore.init(); + TeamStore.loadInitialData([teamWithProject, teamWithoutProject]); + + const mock = jest.fn(); + render( + + ); + + await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Select Owner'})); + expect( + within( + (await screen.findByText('My Teams')).parentElement as HTMLElement + ).getByText('#my-team') + ).toBeInTheDocument(); + + expect( + within( + (await screen.findByText('Disabled Teams')).parentElement as HTMLElement + ).getByText('#disabled-team') + ).toBeInTheDocument(); + }); }); diff --git a/static/app/components/forms/fields/sentryMemberTeamSelectorField.tsx b/static/app/components/forms/fields/sentryMemberTeamSelectorField.tsx index b686d9010da227..048c822abd6e84 100644 --- a/static/app/components/forms/fields/sentryMemberTeamSelectorField.tsx +++ b/static/app/components/forms/fields/sentryMemberTeamSelectorField.tsx @@ -1,9 +1,10 @@ import {useContext, useEffect, useMemo} from 'react'; -import partition from 'lodash/partition'; +import groupBy from 'lodash/groupBy'; import Avatar from 'sentry/components/avatar'; +import {Tooltip} from 'sentry/components/tooltip'; import {t} from 'sentry/locale'; -import type {Team} from 'sentry/types'; +import type {DetailedTeam, Team} from 'sentry/types'; import {useMembers} from 'sentry/utils/useMembers'; import {useTeams} from 'sentry/utils/useTeams'; import {useTeamsById} from 'sentry/utils/useTeamsById'; @@ -17,6 +18,10 @@ import SelectField from './selectField'; // projects can be passed as a direct prop as well export interface RenderFieldProps extends SelectFieldProps { avatarSize?: number; + /** + * Ensures the only selectable teams and members are members of the given project + */ + memberOfProjectSlug?: string; /** * Use the slug as the select field value. Without setting this the numeric id * of the project will be used. @@ -27,6 +32,7 @@ export interface RenderFieldProps extends SelectFieldProps { function SentryMemberTeamSelectorField({ avatarSize = 20, placeholder = t('Choose Teams and Members'), + memberOfProjectSlug, ...props }: RenderFieldProps) { const {form} = useContext(FormContext); @@ -85,10 +91,33 @@ function SentryMemberTeamSelectorField({ leadingItems: , }); - const [myTeams, otherTeams] = partition(teams, team => team.isMember); + const makeDisabledTeamOption = (team: Team) => ({ + ...makeTeamOption(team), + disabled: true, + label: ( + + #{team.slug} + + ), + }); - const myTeamOptions = myTeams.map(makeTeamOption); - const otherTeamOptions = otherTeams.map(makeTeamOption); + // TODO(davidenwang): Fix the team type here to avoid this type cast: `as DetailedTeam[]` + const {disabledTeams, memberTeams, otherTeams} = groupBy( + teams as DetailedTeam[], + team => + memberOfProjectSlug && !team.projects.some(({slug}) => memberOfProjectSlug === slug) + ? 'disabledTeams' + : team.isMember + ? 'memberTeams' + : 'otherTeams' + ); + + const myTeamOptions = memberTeams?.map(makeTeamOption) ?? []; + const otherTeamOptions = otherTeams?.map(makeTeamOption) ?? []; + const disabledTeamOptions = disabledTeams?.map(makeDisabledTeamOption) ?? []; // TODO(epurkhiser): This is an unfortunate hack right now since we don't // actually load members anywhere and the useMembers and useTeams hook don't @@ -128,6 +157,10 @@ function SentryMemberTeamSelectorField({ label: t('Other Teams'), options: otherTeamOptions, }, + { + label: t('Disabled Teams'), + options: disabledTeamOptions, + }, ]} {...props} /> diff --git a/static/app/views/monitors/components/monitorForm.tsx b/static/app/views/monitors/components/monitorForm.tsx index e907ca0726274a..4e48eedc5b1d59 100644 --- a/static/app/views/monitors/components/monitorForm.tsx +++ b/static/app/views/monitors/components/monitorForm.tsx @@ -504,13 +504,18 @@ function MonitorForm({ {t('Customize this monitors notification configuration in Alerts')} )} - + + {() => ( + + )} + {() => { const selectedAssignee = form.current.getValue('alertRule.targets');