Skip to content

Commit aa870e3

Browse files
mikejihbevbrogetsantry[bot]corpsmarkstory
authored
ref(hybrid-cloud): Restrict notification settings UX to a single organization (#50279)
Refactor the notification settings page to restrict viewing notification settings to a single organization. This is required to simplify some user notification setting APIs and make them compatible with hybrid cloud. Currently the user notifications APIs return "virtual" notification settings for every project. This page doesn't really use those settings, it instead renders projects from the list of projects and merges in what gets returned. I think it should work fine if we only return actual notification settings records, which will allow us to simplify the user notification settings endpoints by removing these "virtual" entries -- assuming this was the only use case. <img width="1231" alt="Issue_Alert_Notifications_—_Sentry" src="https://github.com/getsentry/sentry/assets/52534/3c59964c-faf2-41d5-8677-fb290063017f"> --------- Co-authored-by: Valery Brobbey <[email protected]> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com> Co-authored-by: Zachary Collins <[email protected]> Co-authored-by: Zach Collins <[email protected]> Co-authored-by: Mark Story <[email protected]>
1 parent d2f6b40 commit aa870e3

8 files changed

+221
-76
lines changed

static/app/views/settings/account/accountNotificationFineTuning.tsx

+44-17
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
ACCOUNT_NOTIFICATION_FIELDS,
2020
FineTuneField,
2121
} from 'sentry/views/settings/account/notifications/fields';
22-
import NotificationSettingsByType from 'sentry/views/settings/account/notifications/notificationSettingsByType';
22+
import NotificationSettingsByType, {
23+
OrganizationSelectHeader,
24+
} from 'sentry/views/settings/account/notifications/notificationSettingsByType';
2325
import {
2426
getNotificationTypeFromPathname,
2527
groupByOrganization,
@@ -71,7 +73,6 @@ function AccountNotificationsByProject({projects, field}: ANBPProps) {
7173
<Fragment>
7274
{data.map(({name, projects: projectFields}) => (
7375
<div key={name}>
74-
<PanelHeader>{name}</PanelHeader>
7576
{projectFields.map(f => (
7677
<PanelBodyLineItem key={f.name}>
7778
<SelectField
@@ -135,20 +136,33 @@ type State = DeprecatedAsyncView['state'] & {
135136
emails: UserEmail[] | null;
136137
fineTuneData: Record<string, any> | null;
137138
notifications: Record<string, any> | null;
139+
organizationId: string;
138140
projects: Project[] | null;
139141
};
140142

141143
class AccountNotificationFineTuning extends DeprecatedAsyncView<Props, State> {
144+
getDefaultState() {
145+
return {
146+
...super.getDefaultState(),
147+
emails: [],
148+
fineTuneData: null,
149+
notifications: [],
150+
projects: [],
151+
organizationId: this.props.organizations[0].id,
152+
};
153+
}
154+
142155
getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
143156
const {fineTuneType: pathnameType} = this.props.params;
157+
const orgId = this.state?.organizationId || this.props.organizations[0].id;
144158
const fineTuneType = getNotificationTypeFromPathname(pathnameType);
145159
const endpoints = [
146160
['notifications', '/users/me/notifications/'],
147161
['fineTuneData', `/users/me/notifications/${fineTuneType}/`],
148162
];
149163

150164
if (isGroupedByProject(fineTuneType)) {
151-
endpoints.push(['projects', '/projects/']);
165+
endpoints.push(['projects', `/projects/?organization_id=${orgId}`]);
152166
}
153167

154168
endpoints.push(['emails', '/users/me/emails/']);
@@ -178,6 +192,14 @@ class AccountNotificationFineTuning extends DeprecatedAsyncView<Props, State> {
178192
);
179193
}
180194

195+
handleOrgChange = (option: {label: string; value: string}) => {
196+
this.setState({organizationId: option.value});
197+
const self = this;
198+
setTimeout(() => {
199+
self.reloadData();
200+
}, 0);
201+
};
202+
181203
renderBody() {
182204
const {params} = this.props;
183205
const {fineTuneType: pathnameType} = params;
@@ -204,7 +226,6 @@ class AccountNotificationFineTuning extends DeprecatedAsyncView<Props, State> {
204226
if (!notifications || !fineTuneData) {
205227
return null;
206228
}
207-
208229
return (
209230
<div>
210231
<SettingsPageHeader title={title} />
@@ -227,19 +248,25 @@ class AccountNotificationFineTuning extends DeprecatedAsyncView<Props, State> {
227248
</Form>
228249
)}
229250
<Panel>
251+
<PanelHeader hasButtons={isProject}>
252+
{isProject ? (
253+
<Fragment>
254+
<OrganizationSelectHeader
255+
organizations={this.props.organizations}
256+
organizationId={this.state.organizationId}
257+
handleOrgChange={this.handleOrgChange}
258+
/>
259+
{this.renderSearchInput({
260+
placeholder: t('Search Projects'),
261+
url,
262+
stateKey,
263+
})}
264+
</Fragment>
265+
) : (
266+
<Heading>{t('Organizations')}</Heading>
267+
)}
268+
</PanelHeader>
230269
<PanelBody>
231-
<PanelHeader hasButtons={isProject}>
232-
<Heading>{isProject ? t('Projects') : t('Organizations')}</Heading>
233-
<div>
234-
{isProject &&
235-
this.renderSearchInput({
236-
placeholder: t('Search Projects'),
237-
url,
238-
stateKey,
239-
})}
240-
</div>
241-
</PanelHeader>
242-
243270
<Form
244271
saveOnBlur
245272
apiMethod="PUT"
@@ -271,4 +298,4 @@ const Heading = styled('div')`
271298
flex: 1;
272299
`;
273300

274-
export default AccountNotificationFineTuning;
301+
export default withOrganizations(AccountNotificationFineTuning);

static/app/views/settings/account/notifications/notificationSettings.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class NotificationSettings extends DeprecatedAsyncComponent<Props, State> {
5454

5555
getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
5656
return [
57-
['notificationSettings', `/users/me/notification-settings/`],
57+
['notificationSettings', `/users/me/notification-settings/`, {v2: 'serializer'}],
5858
['legacyData', '/users/me/notifications/'],
5959
];
6060
}

static/app/views/settings/account/notifications/notificationSettingsByOrganization.tsx

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import Form from 'sentry/components/forms/form';
2-
import JsonForm from 'sentry/components/forms/jsonForm';
32
import {t} from 'sentry/locale';
43
import {OrganizationSummary} from 'sentry/types';
54
import withOrganizations from 'sentry/utils/withOrganizations';
65
import {
76
NotificationSettingsByProviderObject,
87
NotificationSettingsObject,
98
} from 'sentry/views/settings/account/notifications/constants';
9+
import {StyledJsonForm} from 'sentry/views/settings/account/notifications/notificationSettingsByProjects';
1010
import {
1111
getParentData,
1212
getParentField,
@@ -38,11 +38,16 @@ function NotificationSettingsByOrganization({
3838
initialData={getParentData(notificationType, notificationSettings, organizations)}
3939
onSubmitSuccess={onSubmitSuccess}
4040
>
41-
<JsonForm
41+
<StyledJsonForm
4242
title={t('Organizations')}
43-
fields={organizations.map(organization =>
44-
getParentField(notificationType, notificationSettings, organization, onChange)
45-
)}
43+
fields={organizations.map(organization => {
44+
return getParentField(
45+
notificationType,
46+
notificationSettings,
47+
organization,
48+
onChange
49+
);
50+
})}
4651
/>
4752
</Form>
4853
);

static/app/views/settings/account/notifications/notificationSettingsByProjects.spec.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import {Project} from 'sentry/types';
55
import NotificationSettingsByProjects from 'sentry/views/settings/account/notifications/notificationSettingsByProjects';
66

77
const renderComponent = (projects: Project[]) => {
8-
const {routerContext} = initializeOrg();
8+
const {routerContext, organization} = initializeOrg();
99

1010
MockApiClient.addMockResponse({
11-
url: '/projects/',
11+
url: `/projects/`,
1212
method: 'GET',
1313
body: projects,
1414
});
@@ -28,6 +28,9 @@ const renderComponent = (projects: Project[]) => {
2828
notificationSettings={notificationSettings}
2929
onChange={jest.fn()}
3030
onSubmitSuccess={jest.fn()}
31+
organizationId={organization.id}
32+
organizations={[organization]}
33+
handleOrgChange={jest.fn()}
3134
/>,
3235
{context: routerContext}
3336
);

static/app/views/settings/account/notifications/notificationSettingsByProjects.tsx

+80-33
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ import EmptyMessage from 'sentry/components/emptyMessage';
66
import Form from 'sentry/components/forms/form';
77
import JsonForm from 'sentry/components/forms/jsonForm';
88
import Pagination from 'sentry/components/pagination';
9+
import Panel from 'sentry/components/panels/panel';
10+
import PanelBody from 'sentry/components/panels/panelBody';
11+
import PanelHeader from 'sentry/components/panels/panelHeader';
912
import {t} from 'sentry/locale';
10-
import {Project} from 'sentry/types';
13+
import {Organization, Project} from 'sentry/types';
1114
import {sortProjects} from 'sentry/utils';
1215
import {
1316
MIN_PROJECTS_FOR_PAGINATION,
1417
MIN_PROJECTS_FOR_SEARCH,
1518
NotificationSettingsByProviderObject,
1619
NotificationSettingsObject,
1720
} from 'sentry/views/settings/account/notifications/constants';
21+
import {OrganizationSelectHeader} from 'sentry/views/settings/account/notifications/notificationSettingsByType';
1822
import {
1923
getParentData,
2024
getParentField,
@@ -25,15 +29,22 @@ import {
2529
SearchWrapper,
2630
} from 'sentry/views/settings/components/defaultSearchBar';
2731

28-
type Props = {
32+
export type NotificationSettingsByProjectsBaseProps = {
2933
notificationSettings: NotificationSettingsObject;
3034
notificationType: string;
3135
onChange: (
3236
changedData: NotificationSettingsByProviderObject,
3337
parentId: string
3438
) => NotificationSettingsObject;
3539
onSubmitSuccess: () => void;
36-
} & DeprecatedAsyncComponent['props'];
40+
};
41+
42+
export type Props = {
43+
handleOrgChange: Function;
44+
organizationId: string;
45+
organizations: Organization[];
46+
} & NotificationSettingsByProjectsBaseProps &
47+
DeprecatedAsyncComponent['props'];
3748

3849
type State = {
3950
projects: Project[];
@@ -48,7 +59,15 @@ class NotificationSettingsByProjects extends DeprecatedAsyncComponent<Props, Sta
4859
}
4960

5061
getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
51-
return [['projects', '/projects/']];
62+
return [
63+
[
64+
'projects',
65+
`/projects/`,
66+
{
67+
query: {organizationId: this.props.organizationId},
68+
},
69+
],
70+
];
5271
}
5372

5473
/**
@@ -74,6 +93,12 @@ class NotificationSettingsByProjects extends DeprecatedAsyncComponent<Props, Sta
7493
);
7594
};
7695

96+
handleOrgChange = (option: {label: string; value: string}) => {
97+
// handleOrgChange(option: {label: string; value: string}) {
98+
this.props.handleOrgChange(option);
99+
setTimeout(() => this.reloadData(), 0);
100+
};
101+
77102
renderBody() {
78103
const {notificationType, notificationSettings, onChange, onSubmitSuccess} =
79104
this.props;
@@ -88,35 +113,50 @@ class NotificationSettingsByProjects extends DeprecatedAsyncComponent<Props, Sta
88113

89114
return (
90115
<Fragment>
91-
{canSearch &&
92-
this.renderSearchInput({
93-
stateKey: 'projects',
94-
url: '/projects/',
95-
placeholder: t('Search Projects'),
96-
children: renderSearch,
97-
})}
98-
<Form
99-
saveOnBlur
100-
apiMethod="PUT"
101-
apiEndpoint="/users/me/notification-settings/"
102-
initialData={getParentData(notificationType, notificationSettings, projects)}
103-
onSubmitSuccess={onSubmitSuccess}
104-
>
105-
{projects.length === 0 ? (
106-
<EmptyMessage>{t('No projects found')}</EmptyMessage>
107-
) : (
108-
Object.entries(this.getGroupedProjects()).map(([groupTitle, parents]) => (
109-
<JsonForm
110-
collapsible
111-
key={groupTitle}
112-
title={groupTitle}
113-
fields={parents.map(parent =>
114-
getParentField(notificationType, notificationSettings, parent, onChange)
115-
)}
116-
/>
117-
))
118-
)}
119-
</Form>
116+
<PanelHeader>
117+
<OrganizationSelectHeader
118+
organizations={this.props.organizations}
119+
organizationId={this.props.organizationId}
120+
handleOrgChange={this.handleOrgChange}
121+
/>
122+
123+
{canSearch &&
124+
this.renderSearchInput({
125+
stateKey: 'projects',
126+
url: `/projects/?organizationId=${this.props.organizationId}`,
127+
placeholder: t('Search Projects'),
128+
children: renderSearch,
129+
})}
130+
</PanelHeader>
131+
<PanelBody>
132+
<Form
133+
saveOnBlur
134+
apiMethod="PUT"
135+
apiEndpoint="/users/me/notification-settings/"
136+
initialData={getParentData(notificationType, notificationSettings, projects)}
137+
onSubmitSuccess={onSubmitSuccess}
138+
>
139+
{projects.length === 0 ? (
140+
<EmptyMessage>{t('No projects found')}</EmptyMessage>
141+
) : (
142+
Object.entries(this.getGroupedProjects()).map(([groupTitle, parents]) => (
143+
<StyledJsonForm
144+
collapsible
145+
key={groupTitle}
146+
// title={groupTitle}
147+
fields={parents.map(parent =>
148+
getParentField(
149+
notificationType,
150+
notificationSettings,
151+
parent,
152+
onChange
153+
)
154+
)}
155+
/>
156+
))
157+
)}
158+
</Form>
159+
</PanelBody>
120160
{canSearch && shouldPaginate && (
121161
<Pagination pageLinks={projectsPageLinks} {...this.props} />
122162
)}
@@ -132,3 +172,10 @@ const StyledSearchWrapper = styled(SearchWrapper)`
132172
width: 100%;
133173
}
134174
`;
175+
176+
export const StyledJsonForm = styled(JsonForm)`
177+
${Panel} {
178+
border: 0;
179+
margin-bottom: 0;
180+
}
181+
`;

static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function renderMockRequests(
3030
});
3131

3232
MockApiClient.addMockResponse({
33-
url: '/projects/',
33+
url: `/projects/`,
3434
method: 'GET',
3535
body: [],
3636
});

0 commit comments

Comments
 (0)