Browse Source

feat(notifications): remove code related to old notification settings (#59744)

Now that we GAd the new notification settings, we can remove the code
for the old settings. I just copy-pasted code from the V2 files to the
non-v2 version.
Stephen Cefali 1 year ago
parent
commit
7570b84288

+ 24 - 17
static/app/views/settings/account/accountNotificationFineTuning.tsx

@@ -6,6 +6,7 @@ import EmptyMessage from 'sentry/components/emptyMessage';
 import SelectField from 'sentry/components/forms/fields/selectField';
 import Form from 'sentry/components/forms/form';
 import JsonForm from 'sentry/components/forms/jsonForm';
+import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import Pagination from 'sentry/components/pagination';
 import Panel from 'sentry/components/panels/panel';
 import PanelBody from 'sentry/components/panels/panelBody';
@@ -46,6 +47,7 @@ const accountNotifications = [
   'approval',
   'quota',
   'spikeProtection',
+  'reports',
 ];
 
 type ANBPProps = {
@@ -67,7 +69,15 @@ function AccountNotificationsByProject({projects, field}: ANBPProps) {
       // `name` key refers to field name
       // we use project.id because slugs are not unique across orgs
       name: project.id,
-      label: project.slug,
+      label: (
+        <ProjectBadge
+          project={project}
+          avatarSize={20}
+          displayName={project.slug}
+          avatarProps={{consistentWidth: true}}
+          disableLink
+        />
+      ),
     })),
   }));
 
@@ -235,22 +245,19 @@ class AccountNotificationFineTuning extends DeprecatedAsyncView<Props, State> {
         <SettingsPageHeader title={title} />
         {description && <TextBlock>{description}</TextBlock>}
 
-        {field &&
-          field.defaultFieldName &&
-          // not implemented yet
-          field.defaultFieldName !== 'weeklyReports' && (
-            <Form
-              saveOnBlur
-              apiMethod="PUT"
-              apiEndpoint="/users/me/notifications/"
-              initialData={notifications}
-            >
-              <JsonForm
-                title={`Default ${title}`}
-                fields={[fields[field.defaultFieldName]]}
-              />
-            </Form>
-          )}
+        {field && field.defaultFieldName && (
+          <Form
+            saveOnBlur
+            apiMethod="PUT"
+            apiEndpoint="/users/me/notifications/"
+            initialData={notifications}
+          >
+            <JsonForm
+              title={`Default ${title}`}
+              fields={[fields[field.defaultFieldName]]}
+            />
+          </Form>
+        )}
         <Panel>
           <StyledPanelHeader hasButtons={isProject}>
             {isProject ? (

+ 1 - 11
static/app/views/settings/account/accountNotificationFineTuningController.tsx

@@ -5,7 +5,6 @@ import type {Organization} from 'sentry/types';
 import withOrganizations from 'sentry/utils/withOrganizations';
 
 import AccountNotificationFineTuning from './accountNotificationFineTuning';
-import AccountNotificationFineTuningV2 from './accountNotificationFineTuningV2';
 
 interface AccountNotificationFineTuningControllerProps
   extends RouteComponentProps<{fineTuneType: string}, {}> {
@@ -14,7 +13,6 @@ interface AccountNotificationFineTuningControllerProps
 }
 
 export function AccountNotificationFineTuningController({
-  organizations,
   organizationsLoading,
   ...props
 }: AccountNotificationFineTuningControllerProps) {
@@ -22,15 +20,7 @@ export function AccountNotificationFineTuningController({
     return <LoadingIndicator />;
   }
 
-  // check if feature is enabled for any organization
-  const hasFeature = organizations.some(org =>
-    org.features.includes('notification-settings-v2')
-  );
-  return hasFeature ? (
-    <AccountNotificationFineTuningV2 {...props} />
-  ) : (
-    <AccountNotificationFineTuning {...props} />
-  );
+  return <AccountNotificationFineTuning {...props} />;
 }
 
 export default withOrganizations(AccountNotificationFineTuningController);

+ 0 - 322
static/app/views/settings/account/accountNotificationFineTuningV2.tsx

@@ -1,322 +0,0 @@
-import {Fragment} from 'react';
-import {RouteComponentProps} from 'react-router';
-import styled from '@emotion/styled';
-
-import EmptyMessage from 'sentry/components/emptyMessage';
-import SelectField from 'sentry/components/forms/fields/selectField';
-import Form from 'sentry/components/forms/form';
-import JsonForm from 'sentry/components/forms/jsonForm';
-import ProjectBadge from 'sentry/components/idBadge/projectBadge';
-import Pagination from 'sentry/components/pagination';
-import Panel from 'sentry/components/panels/panel';
-import PanelBody from 'sentry/components/panels/panelBody';
-import PanelHeader from 'sentry/components/panels/panelHeader';
-import {fields} from 'sentry/data/forms/accountNotificationSettings';
-import {t} from 'sentry/locale';
-import ConfigStore from 'sentry/stores/configStore';
-import {space} from 'sentry/styles/space';
-import {Organization, Project, UserEmail} from 'sentry/types';
-import parseLinkHeader from 'sentry/utils/parseLinkHeader';
-import withOrganizations from 'sentry/utils/withOrganizations';
-import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
-import {
-  ACCOUNT_NOTIFICATION_FIELDS,
-  FineTuneField,
-} from 'sentry/views/settings/account/notifications/fields';
-import NotificationSettingsByTypeV2 from 'sentry/views/settings/account/notifications/notificationSettingsByTypeV2';
-import {OrganizationSelectHeader} from 'sentry/views/settings/account/notifications/organizationSelectHeader';
-import {
-  getNotificationTypeFromPathname,
-  groupByOrganization,
-  isGroupedByProject,
-} from 'sentry/views/settings/account/notifications/utils';
-import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
-import TextBlock from 'sentry/views/settings/components/text/textBlock';
-
-const PanelBodyLineItem = styled(PanelBody)`
-  font-size: 1rem;
-  &:not(:last-child) {
-    border-bottom: 1px solid ${p => p.theme.innerBorder};
-  }
-`;
-
-const accountNotifications = [
-  'alerts',
-  'deploy',
-  'workflow',
-  'approval',
-  'quota',
-  'spikeProtection',
-  'reports',
-];
-
-type ANBPProps = {
-  field: FineTuneField;
-  projects: Project[];
-};
-
-function AccountNotificationsByProject({projects, field}: ANBPProps) {
-  const projectsByOrg = groupByOrganization(projects);
-
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  const {title, description, ...fieldConfig} = field;
-
-  // Display as select box in this view regardless of the type specified in the config
-  const data = Object.values(projectsByOrg).map(org => ({
-    name: org.organization.name,
-    projects: org.projects.map(project => ({
-      ...fieldConfig,
-      // `name` key refers to field name
-      // we use project.id because slugs are not unique across orgs
-      name: project.id,
-      label: (
-        <ProjectBadge
-          project={project}
-          avatarSize={20}
-          displayName={project.slug}
-          avatarProps={{consistentWidth: true}}
-          disableLink
-        />
-      ),
-    })),
-  }));
-
-  return (
-    <Fragment>
-      {data.map(({name, projects: projectFields}) => (
-        <div key={name}>
-          {projectFields.map(f => (
-            <PanelBodyLineItem key={f.name}>
-              <SelectField
-                defaultValue={f.defaultValue}
-                name={f.name}
-                options={f.options}
-                label={f.label}
-              />
-            </PanelBodyLineItem>
-          ))}
-        </div>
-      ))}
-    </Fragment>
-  );
-}
-
-type ANBOProps = {
-  field: FineTuneField;
-  organizations: Organization[];
-};
-
-function AccountNotificationsByOrganization({organizations, field}: ANBOProps) {
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  const {title, description, ...fieldConfig} = field;
-
-  // Display as select box in this view regardless of the type specified in the config
-  const data = organizations.map(org => ({
-    ...fieldConfig,
-    // `name` key refers to field name
-    // we use org.id to remain consistent project.id use (which is required because slugs are not unique across orgs)
-    name: org.id,
-    label: org.slug,
-  }));
-
-  return (
-    <Fragment>
-      {data.map(f => (
-        <PanelBodyLineItem key={f.name}>
-          <SelectField
-            defaultValue={f.defaultValue}
-            name={f.name}
-            options={f.options}
-            label={f.label}
-          />
-        </PanelBodyLineItem>
-      ))}
-    </Fragment>
-  );
-}
-
-const AccountNotificationsByOrganizationContainer = withOrganizations(
-  AccountNotificationsByOrganization
-);
-
-type Props = DeprecatedAsyncView['props'] &
-  RouteComponentProps<{fineTuneType: string}, {}> & {
-    organizations: Organization[];
-  };
-
-type State = DeprecatedAsyncView['state'] & {
-  emails: UserEmail[] | null;
-  fineTuneData: Record<string, any> | null;
-  notifications: Record<string, any> | null;
-  projects: Project[] | null;
-};
-
-class AccountNotificationFineTuningV2 extends DeprecatedAsyncView<Props, State> {
-  getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
-    const {fineTuneType: pathnameType} = this.props.params;
-    const fineTuneType = getNotificationTypeFromPathname(pathnameType);
-    const endpoints: ReturnType<DeprecatedAsyncView['getEndpoints']> = [
-      ['notifications', '/users/me/notifications/'],
-      ['fineTuneData', `/users/me/notifications/${fineTuneType}/`],
-    ];
-
-    if (isGroupedByProject(fineTuneType)) {
-      const organizationId = this.getOrganizationId();
-      endpoints.push(['projects', `/projects/`, {query: {organizationId}}]);
-    }
-
-    endpoints.push(['emails', '/users/me/emails/']);
-    if (fineTuneType === 'email') {
-      endpoints.push(['emails', '/users/me/emails/']);
-    }
-
-    return endpoints;
-  }
-
-  // Return a sorted list of user's verified emails
-  get emailChoices() {
-    return (
-      this.state.emails
-        ?.filter(({isVerified}) => isVerified)
-        ?.sort((a, b) => {
-          // Sort by primary -> email
-          if (a.isPrimary) {
-            return -1;
-          }
-          if (b.isPrimary) {
-            return 1;
-          }
-
-          return a.email < b.email ? -1 : 1;
-        }) ?? []
-    );
-  }
-
-  handleOrgChange = (organizationId: string) => {
-    this.props.router.replace({
-      ...this.props.location,
-      query: {organizationId},
-    });
-  };
-
-  getOrganizationId(): string | undefined {
-    const {location, organizations} = this.props;
-    const customerDomain = ConfigStore.get('customerDomain');
-    const orgFromSubdomain = organizations.find(
-      ({slug}) => slug === customerDomain?.subdomain
-    )?.id;
-    return location?.query?.organizationId ?? orgFromSubdomain ?? organizations[0]?.id;
-  }
-
-  renderBody() {
-    const {params, organizations} = this.props;
-    const {fineTuneType: pathnameType} = params;
-    const fineTuneType = getNotificationTypeFromPathname(pathnameType);
-
-    if (accountNotifications.includes(fineTuneType)) {
-      return <NotificationSettingsByTypeV2 notificationType={fineTuneType} />;
-    }
-
-    const {notifications, projects, fineTuneData, projectsPageLinks} = this.state;
-
-    const isProject = isGroupedByProject(fineTuneType) && organizations.length > 0;
-    const field = ACCOUNT_NOTIFICATION_FIELDS[fineTuneType];
-    const {title, description} = field;
-
-    const [stateKey] = isProject ? this.getEndpoints()[2] : [];
-    const hasProjects = !!projects?.length;
-
-    if (fineTuneType === 'email') {
-      // Fetch verified email addresses
-      field.options = this.emailChoices.map(({email}) => ({value: email, label: email}));
-    }
-
-    if (!notifications || !fineTuneData) {
-      return null;
-    }
-
-    const orgId = this.getOrganizationId();
-    const paginationObject = parseLinkHeader(projectsPageLinks ?? '');
-    const hasMore = paginationObject?.next?.results;
-    const hasPrevious = paginationObject?.previous?.results;
-
-    return (
-      <div>
-        <SettingsPageHeader title={title} />
-        {description && <TextBlock>{description}</TextBlock>}
-
-        {field && field.defaultFieldName && (
-          <Form
-            saveOnBlur
-            apiMethod="PUT"
-            apiEndpoint="/users/me/notifications/"
-            initialData={notifications}
-          >
-            <JsonForm
-              title={`Default ${title}`}
-              fields={[fields[field.defaultFieldName]]}
-            />
-          </Form>
-        )}
-        <Panel>
-          <StyledPanelHeader hasButtons={isProject}>
-            {isProject ? (
-              <Fragment>
-                <OrganizationSelectHeader
-                  organizations={organizations}
-                  organizationId={orgId}
-                  handleOrgChange={this.handleOrgChange}
-                />
-                {this.renderSearchInput({
-                  placeholder: t('Search Projects'),
-                  url: `/projects/?organizationId=${orgId}`,
-                  stateKey,
-                })}
-              </Fragment>
-            ) : (
-              <Heading>{t('Organizations')}</Heading>
-            )}
-          </StyledPanelHeader>
-          <PanelBody>
-            <Form
-              saveOnBlur
-              apiMethod="PUT"
-              apiEndpoint={`/users/me/notifications/${fineTuneType}/`}
-              initialData={fineTuneData}
-            >
-              {isProject && hasProjects && (
-                <AccountNotificationsByProject projects={projects!} field={field} />
-              )}
-
-              {isProject && !hasProjects && (
-                <EmptyMessage>{t('No projects found')}</EmptyMessage>
-              )}
-
-              {!isProject && (
-                <AccountNotificationsByOrganizationContainer field={field} />
-              )}
-            </Form>
-          </PanelBody>
-        </Panel>
-
-        {projects && (hasMore || hasPrevious) && (
-          <Pagination pageLinks={projectsPageLinks} />
-        )}
-      </div>
-    );
-  }
-}
-
-const Heading = styled('div')`
-  flex: 1;
-`;
-
-const StyledPanelHeader = styled(PanelHeader)`
-  flex-wrap: wrap;
-  gap: ${space(1)};
-  & > form:last-child {
-    flex-grow: 1;
-  }
-`;
-
-export default withOrganizations(AccountNotificationFineTuningV2);

+ 17 - 41
static/app/views/settings/account/notifications/notificationSettings.spec.tsx

@@ -1,24 +1,11 @@
 import {initializeOrg} from 'sentry-test/initializeOrg';
 import {render, screen} from 'sentry-test/reactTestingLibrary';
 
-import {
-  NotificationSettingsObject,
-  SELF_NOTIFICATION_SETTINGS_TYPES,
-} from 'sentry/views/settings/account/notifications/constants';
+import {SELF_NOTIFICATION_SETTINGS_TYPES} from 'sentry/views/settings/account/notifications/constants';
 import {NOTIFICATION_SETTING_FIELDS} from 'sentry/views/settings/account/notifications/fields2';
 import NotificationSettings from 'sentry/views/settings/account/notifications/notificationSettings';
 
-function renderMockRequests({
-  notificationSettings,
-}: {
-  notificationSettings: NotificationSettingsObject;
-}) {
-  MockApiClient.addMockResponse({
-    url: '/users/me/notification-settings/',
-    method: 'GET',
-    body: notificationSettings,
-  });
-
+function renderMockRequests({}: {}) {
   MockApiClient.addMockResponse({
     url: '/users/me/notifications/',
     method: 'GET',
@@ -31,23 +18,17 @@ function renderMockRequests({
 }
 
 describe('NotificationSettings', function () {
-  it('should render', function () {
+  it('should render', async function () {
     const {routerContext, organization} = initializeOrg();
 
-    renderMockRequests({
-      notificationSettings: {
-        alerts: {user: {me: {email: 'never', slack: 'never'}}},
-        deploy: {user: {me: {email: 'never', slack: 'never'}}},
-        workflow: {user: {me: {email: 'never', slack: 'never'}}},
-      },
-    });
+    renderMockRequests({});
 
     render(<NotificationSettings organizations={[organization]} />, {
       context: routerContext,
     });
 
     // There are 8 notification setting Selects/Toggles.
-    [
+    for (const field of [
       'alerts',
       'workflow',
       'deploy',
@@ -55,48 +36,43 @@ describe('NotificationSettings', function () {
       'reports',
       'email',
       ...SELF_NOTIFICATION_SETTINGS_TYPES,
-    ].forEach(field => {
+    ]) {
       expect(
-        screen.getByText(String(NOTIFICATION_SETTING_FIELDS[field].label))
+        await screen.findByText(String(NOTIFICATION_SETTING_FIELDS[field].label))
       ).toBeInTheDocument();
-    });
-
+    }
     expect(screen.getByText('Issue Alerts')).toBeInTheDocument();
   });
 
-  it('renders quota section with feature flag', function () {
+  it('renders quota section with feature flag', async function () {
     const {routerContext, organization} = initializeOrg({
       organization: {
         features: ['slack-overage-notifications'],
       },
     });
 
-    renderMockRequests({
-      notificationSettings: {
-        alerts: {user: {me: {email: 'never', slack: 'never'}}},
-        deploy: {user: {me: {email: 'never', slack: 'never'}}},
-        workflow: {user: {me: {email: 'never', slack: 'never'}}},
-      },
-    });
+    renderMockRequests({});
 
     render(<NotificationSettings organizations={[organization]} />, {
       context: routerContext,
     });
 
     // There are 9 notification setting Selects/Toggles.
-    [
+
+    for (const field of [
       'alerts',
       'workflow',
       'deploy',
       'approval',
-      'quota',
       'reports',
       'email',
+      'quota',
       ...SELF_NOTIFICATION_SETTINGS_TYPES,
-    ].forEach(field => {
+    ]) {
       expect(
-        screen.getByText(String(NOTIFICATION_SETTING_FIELDS[field].label))
+        await screen.findByText(String(NOTIFICATION_SETTING_FIELDS[field].label))
       ).toBeInTheDocument();
-    });
+    }
+    expect(screen.getByText('Issue Alerts')).toBeInTheDocument();
   });
 });

+ 131 - 185
static/app/views/settings/account/notifications/notificationSettings.tsx

@@ -1,214 +1,160 @@
 import {Fragment} from 'react';
+import styled from '@emotion/styled';
 
 import AlertLink from 'sentry/components/alertLink';
-import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
+import {LinkButton} from 'sentry/components/button';
 import Form from 'sentry/components/forms/form';
 import JsonForm from 'sentry/components/forms/jsonForm';
-import FormModel from 'sentry/components/forms/model';
 import {FieldObject} from 'sentry/components/forms/types';
-import Link from 'sentry/components/links/link';
+import LoadingError from 'sentry/components/loadingError';
+import Panel from 'sentry/components/panels/panel';
+import PanelBody from 'sentry/components/panels/panelBody';
+import PanelHeader from 'sentry/components/panels/panelHeader';
+import PanelItem from 'sentry/components/panels/panelItem';
+import Placeholder from 'sentry/components/placeholder';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
-import {IconMail} from 'sentry/icons';
+import {IconMail, IconSettings} from 'sentry/icons';
 import {t} from 'sentry/locale';
-import {Organization} from 'sentry/types';
-import {trackAnalytics} from 'sentry/utils/analytics';
+import type {Organization} from 'sentry/types';
+import {useApiQuery} from 'sentry/utils/queryClient';
 import withOrganizations from 'sentry/utils/withOrganizations';
 import {
-  CONFIRMATION_MESSAGE,
   NOTIFICATION_FEATURE_MAP,
   NOTIFICATION_SETTINGS_PATHNAMES,
   NOTIFICATION_SETTINGS_TYPES,
-  NotificationSettingsObject,
   SELF_NOTIFICATION_SETTINGS_TYPES,
 } from 'sentry/views/settings/account/notifications/constants';
 import {NOTIFICATION_SETTING_FIELDS} from 'sentry/views/settings/account/notifications/fields2';
-import {
-  decideDefault,
-  getParentIds,
-  getStateToPutForDefault,
-  isSufficientlyComplex,
-  mergeNotificationSettings,
-} from 'sentry/views/settings/account/notifications/utils';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 
-type Props = DeprecatedAsyncComponent['props'] & {
+interface NotificationSettingsProps {
   organizations: Organization[];
-};
-
-type State = {
-  legacyData: {[key: string]: string};
-  notificationSettings: NotificationSettingsObject;
-} & DeprecatedAsyncComponent['state'];
-
-class NotificationSettings extends DeprecatedAsyncComponent<Props, State> {
-  model = new FormModel();
-
-  getDefaultState(): State {
-    return {
-      ...super.getDefaultState(),
-      notificationSettings: {},
-      legacyData: {},
-    };
-  }
-
-  getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
-    return [
-      ['notificationSettings', `/users/me/notification-settings/`, {v2: 'serializer'}],
-      ['legacyData', '/users/me/notifications/'],
-    ];
-  }
-
-  componentDidMount() {
-    super.componentDidMount();
-    // only tied to a user
-    trackAnalytics('notification_settings.index_page_viewed', {
-      organization: null,
-    });
-  }
-
-  getStateToPutForDefault = (
-    changedData: {[key: string]: string},
-    notificationType: string
-  ) => {
-    /**
-     * Update the current providers' parent-independent notification settings
-     * with the new value. If the new value is "never", then also update all
-     * parent-specific notification settings to "default". If the previous value
-     * was "never", then assume providerList should be "email" only.
-     */
-
-    const {notificationSettings} = this.state;
-
-    const updatedNotificationSettings = getStateToPutForDefault(
-      notificationType,
-      notificationSettings,
-      changedData,
-      getParentIds(notificationType, notificationSettings)
-    );
-
-    this.setState({
-      notificationSettings: mergeNotificationSettings(
-        notificationSettings,
-        updatedNotificationSettings
-      ),
-    });
+}
 
-    return updatedNotificationSettings;
+function NotificationSettings({organizations}: NotificationSettingsProps) {
+  const checkFeatureFlag = (flag: string) => {
+    return organizations.some(org => org.features?.includes(flag));
   };
-
-  checkFeatureFlag(flag: string) {
-    return this.props.organizations.some(org => org.features?.includes(flag));
-  }
-
-  get notificationSettingsType() {
-    // filter out notification settings if the feature flag isn't set
-    return NOTIFICATION_SETTINGS_TYPES.filter(type => {
-      const notificationFlag = NOTIFICATION_FEATURE_MAP[type];
-      if (notificationFlag) {
-        return this.checkFeatureFlag(notificationFlag);
-      }
-      return true;
-    });
-  }
-
-  getInitialData(): {[key: string]: string} {
-    const {notificationSettings, legacyData} = this.state;
-
-    const notificationsInitialData = Object.fromEntries(
-      this.notificationSettingsType.map(notificationType => [
-        notificationType,
-        decideDefault(notificationType, notificationSettings),
-      ])
-    );
-
-    const allInitialData = {
-      ...notificationsInitialData,
-      ...legacyData,
-    };
-
-    return allInitialData;
-  }
-
-  getFields(): FieldObject[] {
-    const {notificationSettings} = this.state;
-
-    const fields: FieldObject[] = [];
-    const endOfFields: FieldObject[] = [];
-    for (const notificationType of this.notificationSettingsType) {
-      const field = Object.assign({}, NOTIFICATION_SETTING_FIELDS[notificationType], {
-        getData: data => this.getStateToPutForDefault(data, notificationType),
-        help: (
-          <Fragment>
-            <p>
-              {NOTIFICATION_SETTING_FIELDS[notificationType].help}
-              &nbsp;
-              <Link
-                data-test-id="fine-tuning"
-                to={`/settings/account/notifications/${NOTIFICATION_SETTINGS_PATHNAMES[notificationType]}`}
-              >
-                Fine tune
-              </Link>
-            </p>
-          </Fragment>
-        ),
-      }) as any;
-
-      if (
-        isSufficientlyComplex(notificationType, notificationSettings) &&
-        typeof field !== 'function'
-      ) {
-        field.confirm = {never: CONFIRMATION_MESSAGE};
-      }
-      if (field.type === 'blank') {
-        endOfFields.push(field);
-      } else {
-        fields.push(field);
-      }
+  const notificationFields = NOTIFICATION_SETTINGS_TYPES.filter(type => {
+    const notificationFlag = NOTIFICATION_FEATURE_MAP[type];
+    if (notificationFlag) {
+      return checkFeatureFlag(notificationFlag);
     }
+    return true;
+  });
 
-    const legacyField = SELF_NOTIFICATION_SETTINGS_TYPES.map(
-      type => NOTIFICATION_SETTING_FIELDS[type] as FieldObject
+  const renderOneSetting = (type: string) => {
+    const field = NOTIFICATION_SETTING_FIELDS[type];
+    return (
+      <FieldWrapper key={type}>
+        <div>
+          <FieldLabel>{field.label}</FieldLabel>
+          <FieldHelp>{field.help}</FieldHelp>
+        </div>
+        <IconWrapper>
+          <LinkButton
+            icon={<IconSettings size="sm" />}
+            size="sm"
+            borderless
+            aria-label={t('Notification Settings')}
+            data-test-id="fine-tuning"
+            to={`/settings/account/notifications/${NOTIFICATION_SETTINGS_PATHNAMES[type]}/`}
+          />
+        </IconWrapper>
+      </FieldWrapper>
     );
-
-    fields.push(...legacyField);
-
-    const allFields = [...fields, ...endOfFields];
-
-    return allFields;
-  }
-
-  onFieldChange = (fieldName: string) => {
-    if (SELF_NOTIFICATION_SETTINGS_TYPES.includes(fieldName)) {
-      this.model.setFormOptions({apiEndpoint: '/users/me/notifications/'});
-    } else {
-      this.model.setFormOptions({apiEndpoint: '/users/me/notification-settings/'});
-    }
   };
 
-  renderBody() {
-    return (
-      <Fragment>
-        <SentryDocumentTitle title={t('Notifications')} />
-        <SettingsPageHeader title={t('Notifications')} />
-        <TextBlock>
-          {t('Personal notifications sent by email or an integration.')}
-        </TextBlock>
-        <Form
-          model={this.model}
-          saveOnBlur
-          apiMethod="PUT"
-          onFieldChange={this.onFieldChange}
-          initialData={this.getInitialData()}
-        >
-          <JsonForm title={t('Notifications')} fields={this.getFields()} />
-        </Form>
-        <AlertLink to="/settings/account/emails" icon={<IconMail />}>
-          {t('Looking to add or remove an email address? Use the emails panel.')}
-        </AlertLink>
-      </Fragment>
-    );
-  }
+  const legacyFields = SELF_NOTIFICATION_SETTINGS_TYPES.map(
+    type => NOTIFICATION_SETTING_FIELDS[type] as FieldObject
+  );
+
+  // use 0 as stale time because we change the values elsewhere
+  const {
+    data: initialLegacyData,
+    isLoading,
+    isError,
+    isSuccess,
+    refetch,
+  } = useApiQuery<{[key: string]: string}>(['/users/me/notifications/'], {
+    staleTime: 0,
+  });
+
+  return (
+    <Fragment>
+      <SentryDocumentTitle title={t('Notifications')} />
+      <SettingsPageHeader title={t('Notifications')} />
+      <TextBlock>
+        {t('Personal notifications sent by email or an integration.')}
+      </TextBlock>
+      {isError && <LoadingError onRetry={refetch} />}
+      <PanelNoBottomMargin>
+        <PanelHeader>{t('Notification')}</PanelHeader>
+        <PanelBody>{notificationFields.map(renderOneSetting)}</PanelBody>
+      </PanelNoBottomMargin>
+      <BottomFormWrapper>
+        {isLoading && (
+          <Panel>
+            {new Array(2).fill(0).map((_, idx) => (
+              <PanelItem key={idx}>
+                <Placeholder height="38px" />
+              </PanelItem>
+            ))}
+          </Panel>
+        )}
+        {isSuccess && (
+          <Form
+            saveOnBlur
+            apiMethod="PUT"
+            apiEndpoint="/users/me/notifications/"
+            initialData={initialLegacyData}
+          >
+            <JsonForm fields={legacyFields} />
+          </Form>
+        )}
+      </BottomFormWrapper>
+      <AlertLink to="/settings/account/emails" icon={<IconMail />}>
+        {t('Looking to add or remove an email address? Use the emails panel.')}
+      </AlertLink>
+    </Fragment>
+  );
 }
-
 export default withOrganizations(NotificationSettings);
+
+const FieldLabel = styled('div')`
+  font-size: ${p => p.theme.fontSizeMedium};
+`;
+
+const FieldHelp = styled('div')`
+  font-size: ${p => p.theme.fontSizeSmall};
+  color: ${p => p.theme.gray300};
+`;
+
+const FieldWrapper = styled('div')`
+  display: grid;
+  grid-template-columns: 1fr min-content;
+  padding: ${p => p.theme.grid * 2}px;
+  border-bottom: 1px solid ${p => p.theme.border};
+`;
+
+const IconWrapper = styled('div')`
+  display: flex;
+  margin: auto;
+  cursor: pointer;
+`;
+
+const BottomFormWrapper = styled('div')`
+  ${Panel} {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+    border-top: 0;
+  }
+`;
+
+const PanelNoBottomMargin = styled(Panel)`
+  margin-bottom: 0;
+  border-bottom: 0;
+  border-bottom-left-radius: 0;
+  border-bottom-right-radius: 0;
+`;

+ 197 - 88
static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx

@@ -1,24 +1,38 @@
+import selectEvent from 'react-select-event';
+import {NotificationDefaults} from 'sentry-fixture/notificationDefaults';
 import {Organization} from 'sentry-fixture/organization';
-import {OrganizationIntegrations} from 'sentry-fixture/organizationIntegrations';
-import {UserIdentity} from 'sentry-fixture/userIdentity';
 
-import {render, screen} from 'sentry-test/reactTestingLibrary';
+import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
 
 import ConfigStore from 'sentry/stores/configStore';
+import {Organization as TOrganization} from 'sentry/types';
 import {OrganizationIntegration} from 'sentry/types/integrations';
-import {NotificationSettingsObject} from 'sentry/views/settings/account/notifications/constants';
-import NotificationSettingsByType from 'sentry/views/settings/account/notifications/notificationSettingsByType';
-import {Identity} from 'sentry/views/settings/account/notifications/types';
-
-function renderMockRequests(
-  notificationSettings: NotificationSettingsObject,
-  identities: Identity[] = [],
-  organizationIntegrations: OrganizationIntegration[] = []
-) {
+
+import {NotificationOptionsObject, NotificationProvidersObject} from './constants';
+import NotificationSettingsByType from './notificationSettingsByType';
+import {Identity} from './types';
+
+function renderMockRequests({
+  notificationOptions = [],
+  notificationProviders = [],
+  identities = [],
+  organizationIntegrations = [],
+}: {
+  identities?: Identity[];
+  notificationOptions?: NotificationOptionsObject[];
+  notificationProviders?: NotificationProvidersObject[];
+  organizationIntegrations?: OrganizationIntegration[];
+}) {
   MockApiClient.addMockResponse({
-    url: '/users/me/notification-settings/',
+    url: '/users/me/notification-options/',
     method: 'GET',
-    body: notificationSettings,
+    body: notificationOptions,
+  });
+
+  MockApiClient.addMockResponse({
+    url: '/users/me/notification-providers/',
+    method: 'GET',
+    body: notificationProviders,
   });
 
   MockApiClient.addMockResponse({
@@ -34,21 +48,48 @@ function renderMockRequests(
   });
 
   MockApiClient.addMockResponse({
-    url: `/projects/`,
+    url: `/organizations/org-slug/projects/`,
     method: 'GET',
-    body: [],
+    body: [
+      {
+        id: '4',
+        slug: 'foo',
+        name: 'foo',
+      },
+    ],
   });
 }
 
-function renderComponent(
-  notificationSettings: NotificationSettingsObject,
-  identities: Identity[] = [],
-  organizationIntegrations: OrganizationIntegration[] = []
-) {
+function renderComponent({
+  notificationOptions = [],
+  notificationProviders = [],
+  identities = [],
+  organizationIntegrations = [],
+  organizations = [],
+  notificationType = 'alerts',
+}: {
+  identities?: Identity[];
+  notificationOptions?: NotificationOptionsObject[];
+  notificationProviders?: NotificationProvidersObject[];
+  notificationType?: string;
+  organizationIntegrations?: OrganizationIntegration[];
+  organizations?: TOrganization[];
+}) {
   const org = Organization();
-  renderMockRequests(notificationSettings, identities, organizationIntegrations);
+  renderMockRequests({
+    notificationOptions,
+    notificationProviders,
+    identities,
+    organizationIntegrations,
+  });
+  organizations = organizations.length ? organizations : [org];
 
-  render(<NotificationSettingsByType notificationType="alerts" organizations={[org]} />);
+  return render(
+    <NotificationSettingsByType
+      notificationType={notificationType}
+      organizations={organizations}
+    />
+  );
 }
 
 describe('NotificationSettingsByType', function () {
@@ -56,10 +97,25 @@ describe('NotificationSettingsByType', function () {
     MockApiClient.clearMockResponses();
     jest.clearAllMocks();
   });
+  beforeEach(() => {
+    MockApiClient.addMockResponse({
+      url: '/notification-defaults/',
+      method: 'GET',
+      body: NotificationDefaults(),
+    });
+  });
 
-  it('should render when everything is disabled', function () {
+  it('should render when default is disabled', function () {
     renderComponent({
-      alerts: {user: {me: {email: 'never', slack: 'never'}}},
+      notificationOptions: [
+        {
+          id: '1',
+          scopeIdentifier: '2',
+          scopeType: 'user',
+          type: 'alerts',
+          value: 'never',
+        },
+      ],
     });
 
     // There is only one field and it is the default and it is set to "off".
@@ -67,87 +123,140 @@ describe('NotificationSettingsByType', function () {
     expect(screen.getByText('Off')).toBeInTheDocument();
   });
 
-  it('should render when notification settings are enabled', function () {
-    renderComponent({
-      alerts: {user: {me: {email: 'always', slack: 'always'}}},
+  it('should default to the subdomain org', async function () {
+    const organization = Organization();
+    const otherOrganization = Organization({
+      id: '2',
+      slug: 'other-org',
+      name: 'other org',
     });
-
-    expect(screen.getByRole('textbox', {name: 'Issue Alerts'})).toBeInTheDocument();
-    expect(screen.getByText('On')).toBeInTheDocument(); // Select Value
-
-    expect(screen.getByRole('textbox', {name: 'Delivery Method'})).toBeInTheDocument();
-    expect(screen.getByText('Email')).toBeInTheDocument(); // Select Value
-    expect(screen.getByText('Slack')).toBeInTheDocument(); // Select Value
+    ConfigStore.set('customerDomain', {
+      ...ConfigStore.get('customerDomain')!,
+      subdomain: otherOrganization.slug,
+    });
+    // renderMockRequests({
+    //   alerts: {user: {me: {email: 'always', slack: 'always'}}},
+    // });
+    const projectsMock = MockApiClient.addMockResponse({
+      url: `/organizations/${otherOrganization.slug}/projects/`,
+      method: 'GET',
+      body: [],
+    });
+    renderComponent({organizations: [organization, otherOrganization]});
+    expect(await screen.findByText(otherOrganization.name)).toBeInTheDocument();
+    expect(projectsMock).toHaveBeenCalledTimes(1);
   });
 
-  it('should render warning modal when identity not linked', function () {
-    const org = Organization();
-
-    renderComponent(
-      {
-        alerts: {user: {me: {email: 'always', slack: 'always'}}},
-      },
-      [],
-      [OrganizationIntegrations()]
-    );
+  it('renders all the quota subcatories', async function () {
+    renderComponent({notificationType: 'quota'});
 
+    // check for all the quota subcategories
     expect(
-      screen.getByText(
-        /You've selected Slack as your delivery method, but do not have a linked account for the following organizations/
+      await screen.findByText(
+        'Receive notifications when your organization exceeds the following limits.'
       )
     ).toBeInTheDocument();
-
-    expect(screen.getByRole('listitem')).toHaveTextContent(org.slug);
+    expect(
+      await screen.findByText('Receive notifications about your error quotas.')
+    ).toBeInTheDocument();
+    expect(screen.getByText('Errors')).toBeInTheDocument();
+    expect(screen.getByText('Transactions')).toBeInTheDocument();
+    expect(screen.getByText('Replays')).toBeInTheDocument();
+    expect(screen.getByText('Attachments')).toBeInTheDocument();
+    expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
   });
+  it('adds a project override and removes it', async function () {
+    renderComponent({});
 
-  it('should not render warning modal when identity is linked', function () {
-    const org = Organization();
+    await selectEvent.select(screen.getByText('Project\u2026'), 'foo');
+    await selectEvent.select(screen.getByText('Value\u2026'), 'On');
 
-    renderComponent(
-      {
-        alerts: {user: {me: {email: 'always', slack: 'always'}}},
+    const addSettingMock = MockApiClient.addMockResponse({
+      url: `/users/me/notification-options/`,
+      method: 'PUT',
+      body: {
+        id: '7',
+        scopeIdentifier: '4',
+        scopeType: 'project',
+        type: 'alerts',
+        value: 'always',
       },
-      [UserIdentity()],
-      [OrganizationIntegrations({organizationId: org.id})]
-    );
+    });
 
-    expect(
-      screen.queryByText(
-        /You've selected Slack as your delivery method, but do not have a linked account for the following organizations/
-      )
-    ).not.toBeInTheDocument();
-  });
+    // click the add button
+    await userEvent.click(screen.getByRole('button', {name: 'Add override'}));
+    expect(addSettingMock).toHaveBeenCalledTimes(1);
 
-  it('should default to the subdomain org', async function () {
-    const organization = Organization();
-    const otherOrganization = Organization({
-      id: '2',
-      slug: 'other-org',
-      name: 'other org',
+    // check it hits delete
+    const deleteSettingMock = MockApiClient.addMockResponse({
+      url: `/users/me/notification-options/7/`,
+      method: 'DELETE',
+      body: {},
     });
-    ConfigStore.set('customerDomain', {
-      ...ConfigStore.get('customerDomain')!,
-      subdomain: otherOrganization.slug,
-    });
-    renderMockRequests({
-      alerts: {user: {me: {email: 'always', slack: 'always'}}},
+    await userEvent.click(screen.getByRole('button', {name: 'Delete'}));
+    expect(deleteSettingMock).toHaveBeenCalledTimes(1);
+  });
+  it('edits a project override', async function () {
+    renderComponent({
+      notificationOptions: [
+        {
+          id: '7',
+          scopeIdentifier: '4',
+          scopeType: 'project',
+          type: 'alerts',
+          value: 'always',
+        },
+      ],
     });
-    const projectsMock = MockApiClient.addMockResponse({
-      url: '/projects/',
-      query: {
-        organizationId: otherOrganization.id,
+    const editSettingMock = MockApiClient.addMockResponse({
+      url: `/users/me/notification-options/`,
+      method: 'PUT',
+      body: {
+        id: '7',
+        scopeIdentifier: '4',
+        scopeType: 'project',
+        type: 'alerts',
+        value: 'never',
       },
-      method: 'GET',
-      body: [],
     });
 
-    render(
-      <NotificationSettingsByType
-        notificationType="alerts"
-        organizations={[organization, otherOrganization]}
-      />
+    expect(await screen.findByText('foo')).toBeInTheDocument();
+    await selectEvent.select(screen.getAllByText('On')[1], 'Off');
+
+    expect(editSettingMock).toHaveBeenCalledTimes(1);
+    expect(editSettingMock).toHaveBeenCalledWith(
+      expect.anything(),
+      expect.objectContaining({
+        data: {
+          id: '7',
+          scopeIdentifier: '4',
+          scopeType: 'project',
+          type: 'alerts',
+          value: 'never',
+        },
+      })
     );
-    expect(await screen.findByText(otherOrganization.name)).toBeInTheDocument();
-    expect(projectsMock).toHaveBeenCalledTimes(1);
+  });
+  it('renders and sets the provider options', async function () {
+    renderComponent({
+      notificationProviders: [
+        {
+          id: '1',
+          type: 'alerts',
+          scopeType: 'user',
+          scopeIdentifier: '1',
+          provider: 'email',
+          value: 'never',
+        },
+      ],
+    });
+    const changeProvidersMock = MockApiClient.addMockResponse({
+      url: `/users/me/notification-providers/`,
+      method: 'PUT',
+      body: [],
+    });
+    const multiSelect = screen.getByRole('textbox', {name: 'Delivery Method'});
+    await selectEvent.select(multiSelect, ['Email']);
+    expect(changeProvidersMock).toHaveBeenCalledTimes(1);
   });
 });

+ 235 - 212
static/app/views/settings/account/notifications/notificationSettingsByType.tsx

@@ -1,53 +1,44 @@
 import {Fragment} from 'react';
+import styled from '@emotion/styled';
 
+import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
 import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
 import Form from 'sentry/components/forms/form';
 import JsonForm from 'sentry/components/forms/jsonForm';
 import {Field} from 'sentry/components/forms/types';
+import Panel from 'sentry/components/panels/panel';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
+import ConfigStore from 'sentry/stores/configStore';
 import {Organization, OrganizationSummary} from 'sentry/types';
 import {OrganizationIntegration} from 'sentry/types/integrations';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import withOrganizations from 'sentry/utils/withOrganizations';
-import {
-  ALL_PROVIDER_NAMES,
-  CONFIRMATION_MESSAGE,
-  NotificationSettingsByProviderObject,
-  NotificationSettingsObject,
-} from 'sentry/views/settings/account/notifications/constants';
-import {ACCOUNT_NOTIFICATION_FIELDS} from 'sentry/views/settings/account/notifications/fields';
-import {
-  NOTIFICATION_SETTING_FIELDS,
-  QUOTA_FIELDS,
-} from 'sentry/views/settings/account/notifications/fields2';
-import NotificationSettingsByOrganization from 'sentry/views/settings/account/notifications/notificationSettingsByOrganization';
-import NotificationSettingsByProjects from 'sentry/views/settings/account/notifications/notificationSettingsByProjects';
-import {Identity} from 'sentry/views/settings/account/notifications/types';
-import UnlinkedAlert from 'sentry/views/settings/account/notifications/unlinkedAlert';
-import {
-  getCurrentDefault,
-  getCurrentProviders,
-  getParentIds,
-  getStateToPutForDefault,
-  getStateToPutForParent,
-  getStateToPutForProvider,
-  isEverythingDisabled,
-  isGroupedByProject,
-  isSufficientlyComplex,
-  mergeNotificationSettings,
-} from 'sentry/views/settings/account/notifications/utils';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 
+import {
+  DefaultSettings,
+  NotificationOptionsObject,
+  NotificationProvidersObject,
+} from './constants';
+import {ACCOUNT_NOTIFICATION_FIELDS} from './fields';
+import {NOTIFICATION_SETTING_FIELDS_V2, QUOTA_FIELDS} from './fields2';
+import NotificationSettingsByEntity from './notificationSettingsByEntity';
+import {Identity} from './types';
+import UnlinkedAlert from './unlinkedAlert';
+import {isGroupedByProject} from './utils';
+
 type Props = {
-  notificationType: string;
+  notificationType: string; // TODO(steve)? type better
   organizations: Organization[];
 } & DeprecatedAsyncComponent['props'];
 
 type State = {
+  defaultSettings: DefaultSettings | null;
   identities: Identity[];
-  notificationSettings: NotificationSettingsObject;
+  notificationOptions: NotificationOptionsObject[];
+  notificationProviders: NotificationProvidersObject[];
   organizationIntegrations: OrganizationIntegration[];
 } & DeprecatedAsyncComponent['state'];
 
@@ -71,13 +62,17 @@ const getQueryParams = (notificationType: string) => {
   return {type: notificationType};
 };
 
-class NotificationSettingsByType extends DeprecatedAsyncComponent<Props, State> {
+class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State> {
+  // topLevelOptionFormModel = new TopLevelOptionFormModel(this.props.notificationType);
+
   getDefaultState(): State {
     return {
       ...super.getDefaultState(),
-      notificationSettings: {},
+      notificationOptions: [],
+      notificationProviders: [],
       identities: [],
       organizationIntegrations: [],
+      defaultSettings: null,
     };
   }
 
@@ -85,9 +80,14 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent<Props, State>
     const {notificationType} = this.props;
     return [
       [
-        'notificationSettings',
-        `/users/me/notification-settings/`,
-        {query: getQueryParams(notificationType), v2: 'serializer'},
+        'notificationOptions',
+        `/users/me/notification-options/`,
+        {query: getQueryParams(notificationType)},
+      ],
+      [
+        'notificationProviders',
+        `/users/me/notification-providers/`,
+        {query: getQueryParams(notificationType)},
       ],
       ['identities', `/users/me/identities/`, {query: {provider: 'slack'}}],
       [
@@ -95,6 +95,7 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent<Props, State>
         `/users/me/organization-integrations/`,
         {query: {provider: 'slack'}},
       ],
+      ['defaultSettings', '/notification-defaults/'],
     ];
   }
 
@@ -114,189 +115,137 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent<Props, State>
     });
   }
 
-  /* Methods responsible for updating state and hitting the API. */
-
-  getStateToPutForProvider = (
-    changedData: NotificationSettingsByProviderObject
-  ): NotificationSettingsObject => {
-    const {notificationType} = this.props;
-    const {notificationSettings} = this.state;
-
-    const updatedNotificationSettings = getStateToPutForProvider(
-      notificationType,
-      notificationSettings,
-      changedData
-    );
-
-    this.setState({
-      notificationSettings: mergeNotificationSettings(
-        notificationSettings,
-        updatedNotificationSettings
-      ),
-    });
-
-    return updatedNotificationSettings;
-  };
-
-  getStateToPutForDependentSetting = (
-    changedData: NotificationSettingsByProviderObject,
-    notificationType: string
-  ) => {
-    const value = changedData[notificationType];
-    const {notificationSettings} = this.state;
-
-    // parent setting will control the which providers we send to
-    // just set every provider to the same value for the child/dependent setting
-    const userSettings = ALL_PROVIDER_NAMES.reduce((accum, provider) => {
-      accum[provider] = value;
-      return accum;
-    }, {});
-
-    // setting is a user-only setting
-    const updatedNotificationSettings = {
-      [notificationType]: {
-        user: {
-          me: userSettings,
-        },
-      },
-    };
-
-    this.setState({
-      notificationSettings: mergeNotificationSettings(
-        notificationSettings,
-        updatedNotificationSettings
-      ),
-    });
-
-    return updatedNotificationSettings;
-  };
-
-  getStateToPutForDefault = (
-    changedData: NotificationSettingsByProviderObject
-  ): NotificationSettingsObject => {
+  getInitialTopOptionData(): {[key: string]: string} {
     const {notificationType} = this.props;
-    const {notificationSettings} = this.state;
-
-    const updatedNotificationSettings = getStateToPutForDefault(
-      notificationType,
-      notificationSettings,
-      changedData,
-      getParentIds(notificationType, notificationSettings)
-    );
-
-    this.setState({
-      notificationSettings: mergeNotificationSettings(
-        notificationSettings,
-        updatedNotificationSettings
-      ),
-    });
-
-    return updatedNotificationSettings;
-  };
-
-  getStateToPutForParent = (
-    changedData: NotificationSettingsByProviderObject,
-    parentId: string
-  ): NotificationSettingsObject => {
-    const {notificationType} = this.props;
-    const {notificationSettings} = this.state;
-
-    const updatedNotificationSettings = getStateToPutForParent(
-      notificationType,
-      notificationSettings,
-      changedData,
-      parentId
+    const {notificationOptions, defaultSettings} = this.state;
+    const matchedOption = notificationOptions.find(
+      option => option.type === notificationType && option.scopeType === 'user'
     );
-
-    this.setState({
-      notificationSettings: mergeNotificationSettings(
-        notificationSettings,
-        updatedNotificationSettings
-      ),
-    });
-    return updatedNotificationSettings;
-  };
-
-  /* Methods responsible for rendering the page. */
-
-  getInitialData(): {[key: string]: string | string[]} {
-    const {notificationType} = this.props;
-    const {notificationSettings} = this.state;
-
-    // TODO: Backend should be in charge of providing defaults since it depends on the type
-    const provider = !isEverythingDisabled(notificationType, notificationSettings)
-      ? getCurrentProviders(notificationType, notificationSettings)
-      : ['email', 'slack'];
-
+    // if no match, fall back to the
+    let defaultValue: string;
+    if (!matchedOption) {
+      if (defaultSettings) {
+        defaultValue = defaultSettings.typeDefaults[notificationType];
+      } else {
+        // should never happen
+        defaultValue = 'never';
+      }
+    } else {
+      defaultValue = matchedOption.value;
+    }
+    // if we have child types, map the default
     const childTypes: string[] = typeMappedChildren[notificationType] || [];
     const childTypesDefaults = Object.fromEntries(
-      childTypes.map(childType => [
-        childType,
-        getCurrentDefault(childType, notificationSettings),
-      ])
+      childTypes.map(childType => {
+        const childMatchedOption = notificationOptions.find(
+          option => option.type === childType && option.scopeType === 'user'
+        );
+        return [childType, childMatchedOption ? childMatchedOption.value : defaultValue];
+      })
     );
 
     return {
-      [notificationType]: getCurrentDefault(notificationType, notificationSettings),
-      provider,
+      [notificationType]: defaultValue,
       ...childTypesDefaults,
     };
   }
 
+  getProviderInitialData(): {[key: string]: string[]} {
+    const {notificationType} = this.props;
+    const {notificationProviders, defaultSettings} = this.state;
+
+    const relevantProviderSettings = notificationProviders.filter(
+      option => option.scopeType === 'user' && option.type === notificationType
+    );
+    // user has no settings saved so use default
+    if (relevantProviderSettings.length === 0 && defaultSettings) {
+      return {provider: defaultSettings.providerDefaults};
+    }
+    const providers = relevantProviderSettings
+      .filter(option => option.value === 'always')
+      .map(option => option.provider);
+    return {provider: providers};
+  }
+
   getFields(): Field[] {
     const {notificationType} = this.props;
-    const {notificationSettings} = this.state;
 
     const help = isGroupedByProject(notificationType)
       ? t('This is the default for all projects.')
       : t('This is the default for all organizations.');
 
-    const defaultField: Field = Object.assign(
-      {},
-      NOTIFICATION_SETTING_FIELDS[notificationType],
-      {
-        help,
-        getData: data => this.getStateToPutForDefault(data),
-      }
-    );
-    if (isSufficientlyComplex(notificationType, notificationSettings)) {
-      defaultField.confirm = {never: CONFIRMATION_MESSAGE};
-    }
-
-    const fields: Field[] = [defaultField];
-    if (!isEverythingDisabled(notificationType, notificationSettings)) {
-      fields.push(
-        Object.assign(
-          {
-            help: t('Where personal notifications will be sent.'),
-            getData: data => this.getStateToPutForProvider(data),
-          },
-          NOTIFICATION_SETTING_FIELDS.provider
-        )
-      );
-    }
-
+    const fields: Field[] = [];
     // if a quota notification is not disabled, add in our dependent fields
-    if (
-      notificationType === 'quota' &&
-      !isEverythingDisabled(notificationType, notificationSettings)
-    ) {
+    // but do not show the top level controller
+    if (notificationType === 'quota') {
       fields.push(
         ...QUOTA_FIELDS.map(field => ({
           ...field,
           type: 'select' as const,
-          getData: data =>
-            this.getStateToPutForDependentSetting(
-              data as NotificationSettingsByProviderObject,
-              field.name
-            ),
+          getData: data => {
+            return {
+              type: field.name,
+              scopeType: 'user',
+              scopeIdentifier: ConfigStore.get('user').id,
+              value: data[field.name],
+            };
+          },
         }))
       );
+    } else {
+      const defaultField: Field = Object.assign(
+        {},
+        NOTIFICATION_SETTING_FIELDS_V2[notificationType],
+        {
+          help,
+          defaultValue: 'always',
+          getData: data => {
+            return {
+              type: notificationType,
+              scopeType: 'user',
+              scopeIdentifier: ConfigStore.get('user').id,
+              value: data[notificationType],
+            };
+          },
+        }
+      );
+      fields.push(defaultField);
     }
 
     return fields;
   }
 
+  getProviderFields(): Field[] {
+    const {notificationType} = this.props;
+    const {organizationIntegrations} = this.state;
+    // get the choices but only the ones that are available to the user
+    const choices = (
+      NOTIFICATION_SETTING_FIELDS_V2.provider.choices as [string, string][]
+    ).filter(([providerSlug]) => {
+      if (providerSlug === 'email') {
+        return true;
+      }
+      return organizationIntegrations.some(
+        organizationIntegration => organizationIntegration.provider.slug === providerSlug
+      );
+    });
+
+    const defaultField = Object.assign({}, NOTIFICATION_SETTING_FIELDS_V2.provider, {
+      choices,
+      getData: data => {
+        return {
+          type: notificationType,
+          scopeType: 'user',
+          scopeIdentifier: ConfigStore.get('user').id,
+          providers: data.provider,
+          value: data[notificationType],
+        };
+      },
+    });
+    const fields: Field[] = [defaultField];
+    return fields;
+  }
+
   getUnlinkedOrgs = (): OrganizationSummary[] => {
     const {organizations} = this.props;
     const {identities, organizationIntegrations} = this.state;
@@ -318,14 +267,70 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent<Props, State>
     });
   };
 
+  handleRemoveNotificationOption = async (id: string) => {
+    await this.api.requestPromise(`/users/me/notification-options/${id}/`, {
+      method: 'DELETE',
+    });
+    this.setState(state => {
+      const newNotificationOptions = state.notificationOptions.filter(
+        option => !(option.id.toString() === id.toString())
+      );
+      return {
+        notificationOptions: newNotificationOptions,
+      };
+    });
+  };
+
+  handleAddNotificationOption = async (data: Omit<NotificationOptionsObject, 'id'>) => {
+    // TODO: add error handling
+    const notificationOption = await this.api.requestPromise(
+      '/users/me/notification-options/',
+      {
+        method: 'PUT',
+        data,
+      }
+    );
+
+    this.setState(state => {
+      return {
+        notificationOptions: [...state.notificationOptions, notificationOption],
+      };
+    });
+  };
+
+  handleEditNotificationOption = async (data: NotificationOptionsObject) => {
+    try {
+      const notificationOption: NotificationOptionsObject = await this.api.requestPromise(
+        '/users/me/notification-options/',
+        {
+          method: 'PUT',
+          data,
+        }
+      );
+      this.setState(state => {
+        // Replace the item in state
+        const newNotificationOptions = state.notificationOptions.map(option => {
+          if (option.id === data.id) {
+            return notificationOption;
+          }
+          return option;
+        });
+
+        return {notificationOptions: newNotificationOptions};
+      });
+      addSuccessMessage(t('Updated notification setting'));
+    } catch (err) {
+      addErrorMessage(t('Unable to update notification setting'));
+    }
+  };
+
   renderBody() {
     const {notificationType} = this.props;
-    const {notificationSettings} = this.state;
-    const hasSlack = getCurrentProviders(notificationType, notificationSettings).includes(
-      'slack'
-    );
+    const {notificationOptions} = this.state;
+    const hasSlack = true;
     const unlinkedOrgs = this.getUnlinkedOrgs();
     const {title, description} = ACCOUNT_NOTIFICATION_FIELDS[notificationType];
+    const entityType = isGroupedByProject(notificationType) ? 'project' : 'organization';
     return (
       <Fragment>
         <SentryDocumentTitle title={title} />
@@ -337,11 +342,11 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent<Props, State>
         <Form
           saveOnBlur
           apiMethod="PUT"
-          apiEndpoint="/users/me/notification-settings/"
-          initialData={this.getInitialData()}
+          apiEndpoint="/users/me/notification-options/"
+          initialData={this.getInitialTopOptionData()}
           onSubmitSuccess={() => this.trackTuningUpdated('general')}
         >
-          <JsonForm
+          <TopJsonForm
             title={
               isGroupedByProject(notificationType)
                 ? t('All Projects')
@@ -350,26 +355,44 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent<Props, State>
             fields={this.getFields()}
           />
         </Form>
-        {!isEverythingDisabled(notificationType, notificationSettings) &&
-          (isGroupedByProject(notificationType) ? (
-            <NotificationSettingsByProjects
-              notificationType={notificationType}
-              notificationSettings={notificationSettings}
-              onChange={this.getStateToPutForParent}
-              onSubmitSuccess={() => this.trackTuningUpdated('project')}
-              organizations={this.props.organizations}
-            />
-          ) : (
-            <NotificationSettingsByOrganization
-              notificationType={notificationType}
-              notificationSettings={notificationSettings}
-              onChange={this.getStateToPutForParent}
-              onSubmitSuccess={() => this.trackTuningUpdated('organization')}
-            />
-          ))}
+        {notificationType !== 'reports' ? (
+          <Form
+            saveOnBlur
+            apiMethod="PUT"
+            apiEndpoint="/users/me/notification-providers/"
+            initialData={this.getProviderInitialData()}
+          >
+            <BottomJsonForm fields={this.getProviderFields()} />
+          </Form>
+        ) : null}
+        <NotificationSettingsByEntity
+          notificationType={notificationType}
+          notificationOptions={notificationOptions}
+          organizations={this.props.organizations}
+          handleRemoveNotificationOption={this.handleRemoveNotificationOption}
+          handleAddNotificationOption={this.handleAddNotificationOption}
+          handleEditNotificationOption={this.handleEditNotificationOption}
+          entityType={entityType}
+        />
       </Fragment>
     );
   }
 }
 
-export default withOrganizations(NotificationSettingsByType);
+export default withOrganizations(NotificationSettingsByTypeV2);
+
+export const TopJsonForm = styled(JsonForm)`
+  ${Panel} {
+    border-bottom: 0;
+    margin-bottom: 0;
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+`;
+
+export const BottomJsonForm = styled(JsonForm)`
+  ${Panel} {
+    border-top-right-radius: 0;
+    border-top-left-radius: 0;
+  }
+`;

+ 0 - 262
static/app/views/settings/account/notifications/notificationSettingsByTypeV2.spec.tsx

@@ -1,262 +0,0 @@
-import selectEvent from 'react-select-event';
-import {NotificationDefaults} from 'sentry-fixture/notificationDefaults';
-import {Organization} from 'sentry-fixture/organization';
-
-import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-
-import ConfigStore from 'sentry/stores/configStore';
-import {Organization as TOrganization} from 'sentry/types';
-import {OrganizationIntegration} from 'sentry/types/integrations';
-
-import {NotificationOptionsObject, NotificationProvidersObject} from './constants';
-import NotificationSettingsByType from './notificationSettingsByTypeV2';
-import {Identity} from './types';
-
-function renderMockRequests({
-  notificationOptions = [],
-  notificationProviders = [],
-  identities = [],
-  organizationIntegrations = [],
-}: {
-  identities?: Identity[];
-  notificationOptions?: NotificationOptionsObject[];
-  notificationProviders?: NotificationProvidersObject[];
-  organizationIntegrations?: OrganizationIntegration[];
-}) {
-  MockApiClient.addMockResponse({
-    url: '/users/me/notification-options/',
-    method: 'GET',
-    body: notificationOptions,
-  });
-
-  MockApiClient.addMockResponse({
-    url: '/users/me/notification-providers/',
-    method: 'GET',
-    body: notificationProviders,
-  });
-
-  MockApiClient.addMockResponse({
-    url: '/users/me/identities/',
-    method: 'GET',
-    body: identities,
-  });
-
-  MockApiClient.addMockResponse({
-    url: '/users/me/organization-integrations/',
-    method: 'GET',
-    body: organizationIntegrations,
-  });
-
-  MockApiClient.addMockResponse({
-    url: `/organizations/org-slug/projects/`,
-    method: 'GET',
-    body: [
-      {
-        id: '4',
-        slug: 'foo',
-        name: 'foo',
-      },
-    ],
-  });
-}
-
-function renderComponent({
-  notificationOptions = [],
-  notificationProviders = [],
-  identities = [],
-  organizationIntegrations = [],
-  organizations = [],
-  notificationType = 'alerts',
-}: {
-  identities?: Identity[];
-  notificationOptions?: NotificationOptionsObject[];
-  notificationProviders?: NotificationProvidersObject[];
-  notificationType?: string;
-  organizationIntegrations?: OrganizationIntegration[];
-  organizations?: TOrganization[];
-}) {
-  const org = Organization();
-  renderMockRequests({
-    notificationOptions,
-    notificationProviders,
-    identities,
-    organizationIntegrations,
-  });
-  organizations = organizations.length ? organizations : [org];
-
-  return render(
-    <NotificationSettingsByType
-      notificationType={notificationType}
-      organizations={organizations}
-    />
-  );
-}
-
-describe('NotificationSettingsByType', function () {
-  afterEach(() => {
-    MockApiClient.clearMockResponses();
-    jest.clearAllMocks();
-  });
-  beforeEach(() => {
-    MockApiClient.addMockResponse({
-      url: '/notification-defaults/',
-      method: 'GET',
-      body: NotificationDefaults(),
-    });
-  });
-
-  it('should render when default is disabled', function () {
-    renderComponent({
-      notificationOptions: [
-        {
-          id: '1',
-          scopeIdentifier: '2',
-          scopeType: 'user',
-          type: 'alerts',
-          value: 'never',
-        },
-      ],
-    });
-
-    // There is only one field and it is the default and it is set to "off".
-    expect(screen.getByRole('textbox', {name: 'Issue Alerts'})).toBeInTheDocument();
-    expect(screen.getByText('Off')).toBeInTheDocument();
-  });
-
-  it('should default to the subdomain org', async function () {
-    const organization = Organization();
-    const otherOrganization = Organization({
-      id: '2',
-      slug: 'other-org',
-      name: 'other org',
-    });
-    ConfigStore.set('customerDomain', {
-      ...ConfigStore.get('customerDomain')!,
-      subdomain: otherOrganization.slug,
-    });
-    // renderMockRequests({
-    //   alerts: {user: {me: {email: 'always', slack: 'always'}}},
-    // });
-    const projectsMock = MockApiClient.addMockResponse({
-      url: `/organizations/${otherOrganization.slug}/projects/`,
-      method: 'GET',
-      body: [],
-    });
-    renderComponent({organizations: [organization, otherOrganization]});
-    expect(await screen.findByText(otherOrganization.name)).toBeInTheDocument();
-    expect(projectsMock).toHaveBeenCalledTimes(1);
-  });
-
-  it('renders all the quota subcatories', async function () {
-    renderComponent({notificationType: 'quota'});
-
-    // check for all the quota subcategories
-    expect(
-      await screen.findByText(
-        'Receive notifications when your organization exceeds the following limits.'
-      )
-    ).toBeInTheDocument();
-    expect(
-      await screen.findByText('Receive notifications about your error quotas.')
-    ).toBeInTheDocument();
-    expect(screen.getByText('Errors')).toBeInTheDocument();
-    expect(screen.getByText('Transactions')).toBeInTheDocument();
-    expect(screen.getByText('Replays')).toBeInTheDocument();
-    expect(screen.getByText('Attachments')).toBeInTheDocument();
-    expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
-  });
-  it('adds a project override and removes it', async function () {
-    renderComponent({});
-
-    await selectEvent.select(screen.getByText('Project\u2026'), 'foo');
-    await selectEvent.select(screen.getByText('Value\u2026'), 'On');
-
-    const addSettingMock = MockApiClient.addMockResponse({
-      url: `/users/me/notification-options/`,
-      method: 'PUT',
-      body: {
-        id: '7',
-        scopeIdentifier: '4',
-        scopeType: 'project',
-        type: 'alerts',
-        value: 'always',
-      },
-    });
-
-    // click the add button
-    await userEvent.click(screen.getByRole('button', {name: 'Add override'}));
-    expect(addSettingMock).toHaveBeenCalledTimes(1);
-
-    // check it hits delete
-    const deleteSettingMock = MockApiClient.addMockResponse({
-      url: `/users/me/notification-options/7/`,
-      method: 'DELETE',
-      body: {},
-    });
-    await userEvent.click(screen.getByRole('button', {name: 'Delete'}));
-    expect(deleteSettingMock).toHaveBeenCalledTimes(1);
-  });
-  it('edits a project override', async function () {
-    renderComponent({
-      notificationOptions: [
-        {
-          id: '7',
-          scopeIdentifier: '4',
-          scopeType: 'project',
-          type: 'alerts',
-          value: 'always',
-        },
-      ],
-    });
-    const editSettingMock = MockApiClient.addMockResponse({
-      url: `/users/me/notification-options/`,
-      method: 'PUT',
-      body: {
-        id: '7',
-        scopeIdentifier: '4',
-        scopeType: 'project',
-        type: 'alerts',
-        value: 'never',
-      },
-    });
-
-    expect(await screen.findByText('foo')).toBeInTheDocument();
-    await selectEvent.select(screen.getAllByText('On')[1], 'Off');
-
-    expect(editSettingMock).toHaveBeenCalledTimes(1);
-    expect(editSettingMock).toHaveBeenCalledWith(
-      expect.anything(),
-      expect.objectContaining({
-        data: {
-          id: '7',
-          scopeIdentifier: '4',
-          scopeType: 'project',
-          type: 'alerts',
-          value: 'never',
-        },
-      })
-    );
-  });
-  it('renders and sets the provider options', async function () {
-    renderComponent({
-      notificationProviders: [
-        {
-          id: '1',
-          type: 'alerts',
-          scopeType: 'user',
-          scopeIdentifier: '1',
-          provider: 'email',
-          value: 'never',
-        },
-      ],
-    });
-    const changeProvidersMock = MockApiClient.addMockResponse({
-      url: `/users/me/notification-providers/`,
-      method: 'PUT',
-      body: [],
-    });
-    const multiSelect = screen.getByRole('textbox', {name: 'Delivery Method'});
-    await selectEvent.select(multiSelect, ['Email']);
-    expect(changeProvidersMock).toHaveBeenCalledTimes(1);
-  });
-});

+ 0 - 398
static/app/views/settings/account/notifications/notificationSettingsByTypeV2.tsx

@@ -1,398 +0,0 @@
-import {Fragment} from 'react';
-import styled from '@emotion/styled';
-
-import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
-import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
-import Form from 'sentry/components/forms/form';
-import JsonForm from 'sentry/components/forms/jsonForm';
-import {Field} from 'sentry/components/forms/types';
-import Panel from 'sentry/components/panels/panel';
-import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
-import {t} from 'sentry/locale';
-import ConfigStore from 'sentry/stores/configStore';
-import {Organization, OrganizationSummary} from 'sentry/types';
-import {OrganizationIntegration} from 'sentry/types/integrations';
-import {trackAnalytics} from 'sentry/utils/analytics';
-import withOrganizations from 'sentry/utils/withOrganizations';
-import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
-import TextBlock from 'sentry/views/settings/components/text/textBlock';
-
-import {
-  DefaultSettings,
-  NotificationOptionsObject,
-  NotificationProvidersObject,
-} from './constants';
-import {ACCOUNT_NOTIFICATION_FIELDS} from './fields';
-import {NOTIFICATION_SETTING_FIELDS_V2, QUOTA_FIELDS} from './fields2';
-import NotificationSettingsByEntity from './notificationSettingsByEntity';
-import {Identity} from './types';
-import UnlinkedAlert from './unlinkedAlert';
-import {isGroupedByProject} from './utils';
-
-type Props = {
-  notificationType: string; // TODO(steve)? type better
-  organizations: Organization[];
-} & DeprecatedAsyncComponent['props'];
-
-type State = {
-  defaultSettings: DefaultSettings | null;
-  identities: Identity[];
-  notificationOptions: NotificationOptionsObject[];
-  notificationProviders: NotificationProvidersObject[];
-  organizationIntegrations: OrganizationIntegration[];
-} & DeprecatedAsyncComponent['state'];
-
-const typeMappedChildren = {
-  quota: [
-    'quotaErrors',
-    'quotaTransactions',
-    'quotaAttachments',
-    'quotaReplays',
-    'quotaWarnings',
-    'quotaSpendAllocations',
-  ],
-};
-
-const getQueryParams = (notificationType: string) => {
-  // if we need multiple settings on this page
-  // then omit the type so we can load all settings
-  if (notificationType in typeMappedChildren) {
-    return null;
-  }
-  return {type: notificationType};
-};
-
-class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State> {
-  // topLevelOptionFormModel = new TopLevelOptionFormModel(this.props.notificationType);
-
-  getDefaultState(): State {
-    return {
-      ...super.getDefaultState(),
-      notificationOptions: [],
-      notificationProviders: [],
-      identities: [],
-      organizationIntegrations: [],
-      defaultSettings: null,
-    };
-  }
-
-  getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
-    const {notificationType} = this.props;
-    return [
-      [
-        'notificationOptions',
-        `/users/me/notification-options/`,
-        {query: getQueryParams(notificationType)},
-      ],
-      [
-        'notificationProviders',
-        `/users/me/notification-providers/`,
-        {query: getQueryParams(notificationType)},
-      ],
-      ['identities', `/users/me/identities/`, {query: {provider: 'slack'}}],
-      [
-        'organizationIntegrations',
-        `/users/me/organization-integrations/`,
-        {query: {provider: 'slack'}},
-      ],
-      ['defaultSettings', '/notification-defaults/'],
-    ];
-  }
-
-  componentDidMount() {
-    super.componentDidMount();
-    trackAnalytics('notification_settings.tuning_page_viewed', {
-      organization: null,
-      notification_type: this.props.notificationType,
-    });
-  }
-
-  trackTuningUpdated(tuningFieldType: string) {
-    trackAnalytics('notification_settings.updated_tuning_setting', {
-      organization: null,
-      notification_type: this.props.notificationType,
-      tuning_field_type: tuningFieldType,
-    });
-  }
-
-  getInitialTopOptionData(): {[key: string]: string} {
-    const {notificationType} = this.props;
-    const {notificationOptions, defaultSettings} = this.state;
-    const matchedOption = notificationOptions.find(
-      option => option.type === notificationType && option.scopeType === 'user'
-    );
-    // if no match, fall back to the
-    let defaultValue: string;
-    if (!matchedOption) {
-      if (defaultSettings) {
-        defaultValue = defaultSettings.typeDefaults[notificationType];
-      } else {
-        // should never happen
-        defaultValue = 'never';
-      }
-    } else {
-      defaultValue = matchedOption.value;
-    }
-    // if we have child types, map the default
-    const childTypes: string[] = typeMappedChildren[notificationType] || [];
-    const childTypesDefaults = Object.fromEntries(
-      childTypes.map(childType => {
-        const childMatchedOption = notificationOptions.find(
-          option => option.type === childType && option.scopeType === 'user'
-        );
-        return [childType, childMatchedOption ? childMatchedOption.value : defaultValue];
-      })
-    );
-
-    return {
-      [notificationType]: defaultValue,
-      ...childTypesDefaults,
-    };
-  }
-
-  getProviderInitialData(): {[key: string]: string[]} {
-    const {notificationType} = this.props;
-    const {notificationProviders, defaultSettings} = this.state;
-
-    const relevantProviderSettings = notificationProviders.filter(
-      option => option.scopeType === 'user' && option.type === notificationType
-    );
-    // user has no settings saved so use default
-    if (relevantProviderSettings.length === 0 && defaultSettings) {
-      return {provider: defaultSettings.providerDefaults};
-    }
-    const providers = relevantProviderSettings
-      .filter(option => option.value === 'always')
-      .map(option => option.provider);
-    return {provider: providers};
-  }
-
-  getFields(): Field[] {
-    const {notificationType} = this.props;
-
-    const help = isGroupedByProject(notificationType)
-      ? t('This is the default for all projects.')
-      : t('This is the default for all organizations.');
-
-    const fields: Field[] = [];
-    // if a quota notification is not disabled, add in our dependent fields
-    // but do not show the top level controller
-    if (notificationType === 'quota') {
-      fields.push(
-        ...QUOTA_FIELDS.map(field => ({
-          ...field,
-          type: 'select' as const,
-          getData: data => {
-            return {
-              type: field.name,
-              scopeType: 'user',
-              scopeIdentifier: ConfigStore.get('user').id,
-              value: data[field.name],
-            };
-          },
-        }))
-      );
-    } else {
-      const defaultField: Field = Object.assign(
-        {},
-        NOTIFICATION_SETTING_FIELDS_V2[notificationType],
-        {
-          help,
-          defaultValue: 'always',
-          getData: data => {
-            return {
-              type: notificationType,
-              scopeType: 'user',
-              scopeIdentifier: ConfigStore.get('user').id,
-              value: data[notificationType],
-            };
-          },
-        }
-      );
-      fields.push(defaultField);
-    }
-
-    return fields;
-  }
-
-  getProviderFields(): Field[] {
-    const {notificationType} = this.props;
-    const {organizationIntegrations} = this.state;
-    // get the choices but only the ones that are available to the user
-    const choices = (
-      NOTIFICATION_SETTING_FIELDS_V2.provider.choices as [string, string][]
-    ).filter(([providerSlug]) => {
-      if (providerSlug === 'email') {
-        return true;
-      }
-      return organizationIntegrations.some(
-        organizationIntegration => organizationIntegration.provider.slug === providerSlug
-      );
-    });
-
-    const defaultField = Object.assign({}, NOTIFICATION_SETTING_FIELDS_V2.provider, {
-      choices,
-      getData: data => {
-        return {
-          type: notificationType,
-          scopeType: 'user',
-          scopeIdentifier: ConfigStore.get('user').id,
-          providers: data.provider,
-          value: data[notificationType],
-        };
-      },
-    });
-    const fields: Field[] = [defaultField];
-    return fields;
-  }
-
-  getUnlinkedOrgs = (): OrganizationSummary[] => {
-    const {organizations} = this.props;
-    const {identities, organizationIntegrations} = this.state;
-    const integrationExternalIDsByOrganizationID = Object.fromEntries(
-      organizationIntegrations.map(organizationIntegration => [
-        organizationIntegration.organizationId,
-        organizationIntegration.externalId,
-      ])
-    );
-
-    const identitiesByExternalId = Object.fromEntries(
-      identities.map(identity => [identity?.identityProvider?.externalId, identity])
-    );
-
-    return organizations.filter(organization => {
-      const externalID = integrationExternalIDsByOrganizationID[organization.id];
-      const identity = identitiesByExternalId[externalID];
-      return identity === undefined || identity === null;
-    });
-  };
-
-  handleRemoveNotificationOption = async (id: string) => {
-    await this.api.requestPromise(`/users/me/notification-options/${id}/`, {
-      method: 'DELETE',
-    });
-    this.setState(state => {
-      const newNotificationOptions = state.notificationOptions.filter(
-        option => !(option.id.toString() === id.toString())
-      );
-      return {
-        notificationOptions: newNotificationOptions,
-      };
-    });
-  };
-
-  handleAddNotificationOption = async (data: Omit<NotificationOptionsObject, 'id'>) => {
-    // TODO: add error handling
-    const notificationOption = await this.api.requestPromise(
-      '/users/me/notification-options/',
-      {
-        method: 'PUT',
-        data,
-      }
-    );
-
-    this.setState(state => {
-      return {
-        notificationOptions: [...state.notificationOptions, notificationOption],
-      };
-    });
-  };
-
-  handleEditNotificationOption = async (data: NotificationOptionsObject) => {
-    try {
-      const notificationOption: NotificationOptionsObject = await this.api.requestPromise(
-        '/users/me/notification-options/',
-        {
-          method: 'PUT',
-          data,
-        }
-      );
-      this.setState(state => {
-        // Replace the item in state
-        const newNotificationOptions = state.notificationOptions.map(option => {
-          if (option.id === data.id) {
-            return notificationOption;
-          }
-          return option;
-        });
-
-        return {notificationOptions: newNotificationOptions};
-      });
-      addSuccessMessage(t('Updated notification setting'));
-    } catch (err) {
-      addErrorMessage(t('Unable to update notification setting'));
-    }
-  };
-
-  renderBody() {
-    const {notificationType} = this.props;
-    const {notificationOptions} = this.state;
-    const hasSlack = true;
-    const unlinkedOrgs = this.getUnlinkedOrgs();
-    const {title, description} = ACCOUNT_NOTIFICATION_FIELDS[notificationType];
-    const entityType = isGroupedByProject(notificationType) ? 'project' : 'organization';
-    return (
-      <Fragment>
-        <SentryDocumentTitle title={title} />
-        <SettingsPageHeader title={title} />
-        {description && <TextBlock>{description}</TextBlock>}
-        {hasSlack && unlinkedOrgs.length > 0 && (
-          <UnlinkedAlert organizations={unlinkedOrgs} />
-        )}
-        <Form
-          saveOnBlur
-          apiMethod="PUT"
-          apiEndpoint="/users/me/notification-options/"
-          initialData={this.getInitialTopOptionData()}
-          onSubmitSuccess={() => this.trackTuningUpdated('general')}
-        >
-          <TopJsonForm
-            title={
-              isGroupedByProject(notificationType)
-                ? t('All Projects')
-                : t('All Organizations')
-            }
-            fields={this.getFields()}
-          />
-        </Form>
-        {notificationType !== 'reports' ? (
-          <Form
-            saveOnBlur
-            apiMethod="PUT"
-            apiEndpoint="/users/me/notification-providers/"
-            initialData={this.getProviderInitialData()}
-          >
-            <BottomJsonForm fields={this.getProviderFields()} />
-          </Form>
-        ) : null}
-        <NotificationSettingsByEntity
-          notificationType={notificationType}
-          notificationOptions={notificationOptions}
-          organizations={this.props.organizations}
-          handleRemoveNotificationOption={this.handleRemoveNotificationOption}
-          handleAddNotificationOption={this.handleAddNotificationOption}
-          handleEditNotificationOption={this.handleEditNotificationOption}
-          entityType={entityType}
-        />
-      </Fragment>
-    );
-  }
-}
-
-export default withOrganizations(NotificationSettingsByTypeV2);
-
-export const TopJsonForm = styled(JsonForm)`
-  ${Panel} {
-    border-bottom: 0;
-    margin-bottom: 0;
-    border-bottom-right-radius: 0;
-    border-bottom-left-radius: 0;
-  }
-`;
-
-export const BottomJsonForm = styled(JsonForm)`
-  ${Panel} {
-    border-top-right-radius: 0;
-    border-top-left-radius: 0;
-  }
-`;

+ 1 - 9
static/app/views/settings/account/notifications/notificationSettingsController.tsx

@@ -5,7 +5,6 @@ import type {Organization} from 'sentry/types';
 import withOrganizations from 'sentry/utils/withOrganizations';
 
 import NotificationSettings from './notificationSettings';
-import NotificationSettingsV2 from './notificationSettingsV2';
 
 interface NotificationSettingsControllerProps extends RouteComponentProps<{}, {}> {
   organizations: Organization[];
@@ -13,19 +12,12 @@ interface NotificationSettingsControllerProps extends RouteComponentProps<{}, {}
 }
 
 export function NotificationSettingsController({
-  organizations,
   organizationsLoading,
-  ...props
 }: NotificationSettingsControllerProps) {
   if (organizationsLoading) {
     return <LoadingIndicator />;
   }
-
-  // check if feature is enabled for any organization
-  const hasFeature = organizations.some(org =>
-    org.features.includes('notification-settings-v2')
-  );
-  return hasFeature ? <NotificationSettingsV2 /> : <NotificationSettings {...props} />;
+  return <NotificationSettings />;
 }
 
 export default withOrganizations(NotificationSettingsController);

Some files were not shown because too many files changed in this diff