Skip to content

Commit 0809bad

Browse files
natemoo-reTkDodo
authored andcommitted
ref(notifications): Migrate account notification settings to new form system (#108393)
## Summary - Migrate the notification settings hub page (`notificationSettings.tsx`) from the legacy `Form`/`JsonForm` system to `AutoSaveField` components using the TanStack-based form system - Migrate the fine-tuning page (`notificationSettingsByType.tsx`) from legacy `Form`/`JsonForm`/`FormModel` + MobX `Observer` to `AutoSaveField` components with `FieldGroup` layout - Add `onValueChange` prop to `AutoSaveField` to enable external state observation (replaces MobX `Observer` pattern for tracking selected providers) - Consolidate `fields.tsx` and `fields2.tsx` into a single `fields.tsx` containing both `ACCOUNT_NOTIFICATION_FIELDS` (page titles/descriptions) and `NOTIFICATION_SETTING_FIELDS` (field configs) - Replace gear icon `LinkButton` with text "Update Settings" `LinkButton` on the hub page - Delete redundant `accountNotificationSettings.tsx` form data file and `SELF_NOTIFICATION_SETTINGS_TYPES` constant Closes DE-932 --------- Co-authored-by: Dominik Dorfmeister <dominik.dorfmeister@sentry.io>
1 parent dafea47 commit 0809bad

File tree

9 files changed

+524
-506
lines changed

9 files changed

+524
-506
lines changed

static/app/components/core/form/generatedFieldRegistry.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ export const FORM_FIELD_REGISTRY: Record<string, FormFieldDefinition> = {
8181
label: t('Additional Email'),
8282
hintText: t('Designate an alternative email for this account'),
8383
},
84+
'notification-settings.personalActivityNotifications': {
85+
name: 'personalActivityNotifications',
86+
formId: 'notification-settings',
87+
route: '/settings/account/notifications/',
88+
label: t('My Own Activity'),
89+
hintText: t('Notifications about your own actions on Sentry.'),
90+
},
91+
'notification-settings.selfAssignOnResolve': {
92+
name: 'selfAssignOnResolve',
93+
formId: 'notification-settings',
94+
route: '/settings/account/notifications/',
95+
label: t('Resolve and Auto-Assign'),
96+
hintText: t("When you resolve an unassigned issue, we'll auto-assign it to you."),
97+
},
8498
'password-form.password': {
8599
name: 'password',
86100
formId: 'password-form',

static/app/data/forms/accountNotificationSettings.tsx

Lines changed: 0 additions & 20 deletions
This file was deleted.

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,6 @@ export const NOTIFICATION_SETTINGS_TYPES = [
3636
'brokenMonitors',
3737
] as const;
3838

39-
export const SELF_NOTIFICATION_SETTINGS_TYPES = [
40-
'personalActivityNotifications',
41-
'selfAssignOnResolve',
42-
] as const;
43-
4439
// 'alerts' | 'workflow' ...
4540
export type NotificationSettingsType = (typeof NOTIFICATION_SETTINGS_TYPES)[number];
4641

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

Lines changed: 240 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import {t} from 'sentry/locale';
1+
import {Fragment} from 'react';
2+
import upperFirst from 'lodash/upperFirst';
3+
4+
import {ExternalLink} from '@sentry/scraps/link';
5+
6+
import type {Field} from 'sentry/components/forms/types';
7+
import QuestionTooltip from 'sentry/components/questionTooltip';
8+
import {DATA_CATEGORY_INFO} from 'sentry/constants';
9+
import {t, tct} from 'sentry/locale';
210
import type {SelectValue} from 'sentry/types/core';
11+
import {DataCategoryExact} from 'sentry/types/core';
12+
import {getPricingDocsLinkForEventType} from 'sentry/views/settings/account/notifications/utils';
313

414
export type FineTuneField = {
515
description: string;
@@ -108,3 +118,232 @@ export const ACCOUNT_NOTIFICATION_FIELDS: Record<string, FineTuneField> = {
108118
// Component will create choices
109119
},
110120
};
121+
122+
export const NOTIFICATION_SETTING_FIELDS = {
123+
alerts: {
124+
name: 'alerts',
125+
type: 'select',
126+
label: t('Issue Alerts'),
127+
choices: [
128+
['always', t('On')],
129+
['never', t('Off')],
130+
],
131+
help: t('Notifications sent from Alert rules that your team has set up.'),
132+
},
133+
workflow: {
134+
name: 'workflow',
135+
type: 'select',
136+
label: t('Issue Workflow'),
137+
choices: [
138+
['always', t('On')],
139+
['subscribe_only', t('Only Subscribed Issues')],
140+
['never', t('Off')],
141+
],
142+
help: t('Changes in issue assignment, resolution status, and comments.'),
143+
},
144+
deploy: {
145+
name: 'deploy',
146+
type: 'select',
147+
label: t('Deploys'),
148+
choices: [
149+
['always', t('On')],
150+
['committed_only', t('Releases with My Commits')],
151+
['never', t('Off')],
152+
],
153+
help: t('Release, environment, and commit overviews.'),
154+
},
155+
provider: {
156+
name: 'provider',
157+
type: 'select',
158+
label: t('Delivery Method'),
159+
choices: [
160+
['email', t('Email')],
161+
['slack', t('Slack')],
162+
['msteams', t('Microsoft Teams')],
163+
],
164+
help: t('Where personal notifications will be sent.'),
165+
multiple: true,
166+
onChange: val => {
167+
// This is a little hack to prevent this field from being empty.
168+
// TODO(nisanthan): need to prevent showing the clearable on. the multi-select when its only 1 value.
169+
if (!val || val.length === 0) {
170+
throw new Error('Invalid selection. Field cannot be empty.');
171+
}
172+
},
173+
},
174+
approval: {
175+
name: 'approval',
176+
type: 'select',
177+
label: t('Nudges'),
178+
choices: [
179+
['always', t('On')],
180+
['never', t('Off')],
181+
],
182+
help: t('Notifications that require review or approval.'),
183+
},
184+
quota: {
185+
name: 'quota',
186+
type: 'select',
187+
label: t('Quota'),
188+
choices: [
189+
['always', t('On')],
190+
['never', t('Off')],
191+
],
192+
help: t('Error, transaction, replay, attachment, and cron monitor quota limits.'),
193+
},
194+
reports: {
195+
name: 'reports',
196+
type: 'select',
197+
label: t('Weekly Reports'),
198+
help: t('A summary of the past week for an organization.'),
199+
choices: [
200+
['always', t('On')],
201+
['never', t('Off')],
202+
],
203+
},
204+
email: {
205+
name: 'email routing',
206+
type: 'blank',
207+
choices: undefined,
208+
label: t('Email Routing'),
209+
help: t('Change the email address that receives notifications.'),
210+
},
211+
spikeProtection: {
212+
name: 'spikeProtection',
213+
type: 'select',
214+
label: t('Spike Protection'),
215+
choices: [
216+
['always', t('On')],
217+
['never', t('Off')],
218+
],
219+
help: t('Notifications about spikes on a per project basis.'),
220+
},
221+
brokenMonitors: {
222+
name: 'brokenMonitors',
223+
type: 'select',
224+
label: t('Broken Cron Monitors'),
225+
choices: [
226+
['always', t('On')],
227+
['never', t('Off')],
228+
],
229+
help: t(
230+
'Notifications for Cron Monitors that have been in a failing state for a prolonged period of time'
231+
),
232+
},
233+
// legacy options
234+
personalActivityNotifications: {
235+
name: 'personalActivityNotifications',
236+
type: 'select',
237+
label: t('My Own Activity'),
238+
choices: [
239+
[true as any, t('On')],
240+
[false as any, t('Off')],
241+
],
242+
help: t('Notifications about your own actions on Sentry.'),
243+
},
244+
selfAssignOnResolve: {
245+
name: 'selfAssignOnResolve',
246+
type: 'select',
247+
label: t('Resolve and Auto-Assign'),
248+
choices: [
249+
[true as any, t('On')],
250+
[false as any, t('Off')],
251+
],
252+
help: t("When you resolve an unassigned issue, we'll auto-assign it to you."),
253+
},
254+
} satisfies Record<string, Field>;
255+
256+
const CATEGORY_QUOTA_FIELDS = Object.values(DATA_CATEGORY_INFO)
257+
.filter(
258+
categoryInfo =>
259+
categoryInfo.isBilledCategory &&
260+
// Exclude Seer categories as they will be handled by a combined quotaSeerBudget field
261+
categoryInfo.name !== DataCategoryExact.SEER_AUTOFIX &&
262+
categoryInfo.name !== DataCategoryExact.SEER_SCANNER
263+
)
264+
.map(categoryInfo => {
265+
return {
266+
name: 'quota' + upperFirst(categoryInfo.plural),
267+
label: categoryInfo.titleName,
268+
help: tct(
269+
`Receive notifications about your [displayName] quotas. [learnMore:Learn more]`,
270+
{
271+
displayName: categoryInfo.displayName,
272+
learnMore: (
273+
<ExternalLink href={getPricingDocsLinkForEventType(categoryInfo.name)} />
274+
),
275+
}
276+
),
277+
choices: [
278+
['always', t('On')],
279+
['never', t('Off')],
280+
] as const,
281+
};
282+
});
283+
284+
// Define the combined Seer budget field
285+
const quotaSeerBudgetField = {
286+
// This maps to NotificationSettingEnum.QUOTA_SEER_BUDGET
287+
name: 'quotaSeerBudget',
288+
label: t('Seer Budget'),
289+
help: tct(`Receive notifications for your Seer budget. [learnMore:Learn more]`, {
290+
learnMore: (
291+
<ExternalLink
292+
href={getPricingDocsLinkForEventType(DataCategoryExact.SEER_AUTOFIX)}
293+
/>
294+
),
295+
}),
296+
choices: [
297+
['always', t('On')],
298+
['never', t('Off')],
299+
] as const,
300+
};
301+
302+
// partial field definition for quota sub-categories
303+
export const QUOTA_FIELDS = [
304+
{
305+
name: 'quotaWarnings',
306+
label: t('Set Quota Limit'),
307+
help: t('Receive notifications when your organization exceeds the following limits.'),
308+
choices: [
309+
['always', t('100% and 80%')],
310+
['never', t('100%')],
311+
] as const,
312+
},
313+
...CATEGORY_QUOTA_FIELDS,
314+
quotaSeerBudgetField,
315+
{
316+
name: 'quotaSpendAllocations',
317+
label: (
318+
<Fragment>
319+
{t('Spend Allocations')}{' '}
320+
<QuestionTooltip position="top" title="Business plan only" size="xs" />
321+
</Fragment>
322+
),
323+
help: t('Receive notifications about your spend allocations.'),
324+
choices: [
325+
['always', t('On')],
326+
['never', t('Off')],
327+
] as const,
328+
},
329+
];
330+
331+
export const SPEND_FIELDS = [
332+
{
333+
name: 'quota',
334+
label: t('Spend Notifications'),
335+
help: tct(
336+
'Receive notifications when your spend crosses predefined or custom thresholds. [learnMore:Learn more]',
337+
{
338+
learnMore: (
339+
<ExternalLink href="https://docs.sentry.io/product/alerts/notifications/#spend-notifications" />
340+
),
341+
}
342+
),
343+
choices: [
344+
['always', t('On')],
345+
['never', t('Off')],
346+
] as const,
347+
},
348+
...QUOTA_FIELDS.slice(1),
349+
];

0 commit comments

Comments
 (0)