Browse Source

feat(notifications): adds approval notifications to the UI (#29651)

This PR adds the UI for approval notifications which is hidden behind the feature flag organization:slack-requsts. These approval notifications require someone with the right permissions to approve or reject requests. These notifications are at an organization level as well.
Stephen Cefali 3 years ago
parent
commit
43dff9f472

+ 1 - 0
src/sentry/models/notificationsetting.py

@@ -72,6 +72,7 @@ class NotificationSetting(Model):
             (NotificationSettingTypes.DEPLOY, "deploy"),
             (NotificationSettingTypes.ISSUE_ALERTS, "issue"),
             (NotificationSettingTypes.WORKFLOW, "workflow"),
+            (NotificationSettingTypes.APPROVAL, "approval"),
         ),
         null=False,
     )

+ 2 - 0
src/sentry/notifications/defaults.py

@@ -12,12 +12,14 @@ NOTIFICATION_SETTINGS_ALL_SOMETIMES = {
     NotificationSettingTypes.DEPLOY: NotificationSettingOptionValues.COMMITTED_ONLY,
     NotificationSettingTypes.ISSUE_ALERTS: NotificationSettingOptionValues.ALWAYS,
     NotificationSettingTypes.WORKFLOW: NotificationSettingOptionValues.SUBSCRIBE_ONLY,
+    NotificationSettingTypes.APPROVAL: NotificationSettingOptionValues.ALWAYS,
 }
 
 NOTIFICATION_SETTINGS_ALL_NEVER = {
     NotificationSettingTypes.DEPLOY: NotificationSettingOptionValues.NEVER,
     NotificationSettingTypes.ISSUE_ALERTS: NotificationSettingOptionValues.NEVER,
     NotificationSettingTypes.WORKFLOW: NotificationSettingOptionValues.NEVER,
+    NotificationSettingTypes.APPROVAL: NotificationSettingOptionValues.NEVER,
 }
 
 NOTIFICATION_SETTING_DEFAULTS = {

+ 2 - 2
src/sentry/notifications/helpers.py

@@ -241,13 +241,13 @@ def validate(type: NotificationSettingTypes, value: NotificationSettingOptionVal
 
 def get_scope_type(type: NotificationSettingTypes) -> NotificationScopeType:
     """In which scope (proj or org) can a user set more specific settings?"""
-    if type in [NotificationSettingTypes.DEPLOY]:
+    if type in [NotificationSettingTypes.DEPLOY, NotificationSettingTypes.APPROVAL]:
         return NotificationScopeType.ORGANIZATION
 
     if type in [NotificationSettingTypes.WORKFLOW, NotificationSettingTypes.ISSUE_ALERTS]:
         return NotificationScopeType.PROJECT
 
-    raise Exception(f"type {type}, must be alerts, deploy, or workflow")
+    raise Exception(f"type {type}, must be alerts, deploy, workflow, or approval")
 
 
 def get_scope(

+ 10 - 0
src/sentry/notifications/types.py

@@ -41,12 +41,16 @@ class NotificationSettingTypes(Enum):
     # Notifications for changes in assignment, resolution, comments, etc.
     WORKFLOW = 30
 
+    # Notifications that require approval like a request to invite a member
+    APPROVAL = 40
+
 
 NOTIFICATION_SETTING_TYPES = {
     NotificationSettingTypes.DEFAULT: "default",
     NotificationSettingTypes.DEPLOY: "deploy",
     NotificationSettingTypes.ISSUE_ALERTS: "alerts",
     NotificationSettingTypes.WORKFLOW: "workflow",
+    NotificationSettingTypes.APPROVAL: "approval",
 }
 
 
@@ -104,6 +108,7 @@ class FineTuningAPIKey(Enum):
     EMAIL = "email"
     REPORTS = "reports"
     WORKFLOW = "workflow"
+    APPROVAL = "approval"
 
 
 class UserOptionsSettingsKey(Enum):
@@ -112,6 +117,7 @@ class UserOptionsSettingsKey(Enum):
     SELF_ASSIGN = "selfAssignOnResolve"
     SUBSCRIBE_BY_DEFAULT = "subscribeByDefault"
     WORKFLOW = "workflowNotifications"
+    APPROVAL = "approvalNotifications"
 
 
 VALID_VALUES_FOR_KEY = {
@@ -124,6 +130,10 @@ VALID_VALUES_FOR_KEY = {
         NotificationSettingOptionValues.ALWAYS,
         NotificationSettingOptionValues.NEVER,
     },
+    NotificationSettingTypes.APPROVAL: {
+        NotificationSettingOptionValues.ALWAYS,
+        NotificationSettingOptionValues.NEVER,
+    },
     NotificationSettingTypes.WORKFLOW: {
         NotificationSettingOptionValues.ALWAYS,
         NotificationSettingOptionValues.SUBSCRIBE_ONLY,

+ 2 - 0
static/app/data/forms/accountNotificationSettings.tsx

@@ -1,6 +1,8 @@
 import {t} from 'app/locale';
 import {Field, JsonFormObject} from 'app/views/settings/components/forms/type';
 
+// TODO: cleanup unused fields and exports
+
 // Export route to make these forms searchable by label/help
 export const route = '/settings/account/notifications/';
 

+ 1 - 1
static/app/views/settings/account/accountNotificationFineTuning.tsx

@@ -168,7 +168,7 @@ class AccountNotificationFineTuning extends AsyncView<Props, State> {
     const {params} = this.props;
     const {fineTuneType} = params;
 
-    if (['alerts', 'deploy', 'workflow'].includes(fineTuneType)) {
+    if (['alerts', 'deploy', 'workflow', 'approval'].includes(fineTuneType)) {
       return <NotificationSettingsByType notificationType={fineTuneType} />;
     }
 

+ 1 - 0
static/app/views/settings/account/notifications/constants.tsx

@@ -29,6 +29,7 @@ export const NOTIFICATION_SETTINGS_TYPES = [
   'alerts',
   'workflow',
   'deploy',
+  'approval',
   'reports',
   'email',
 ];

+ 8 - 1
static/app/views/settings/account/notifications/fields.tsx

@@ -10,6 +10,7 @@ export type FineTuneField = {
   defaultFieldName?: string;
 };
 
+// TODO: clean up unused fields
 export const ACCOUNT_NOTIFICATION_FIELDS: Record<string, FineTuneField> = {
   alerts: {
     title: 'Project Alerts',
@@ -69,7 +70,13 @@ export const ACCOUNT_NOTIFICATION_FIELDS: Record<string, FineTuneField> = {
     ],
     defaultFieldName: 'weeklyReports',
   },
-
+  approval: {
+    title: t('Approvals'),
+    description: t('Notifications from teammates that require review or approval.'),
+    type: 'select',
+    // No choices here because it's going to have dynamic content
+    // Component will create choices,
+  },
   email: {
     title: t('Email Routing'),
     description: t(

+ 10 - 0
static/app/views/settings/account/notifications/fields2.tsx

@@ -56,6 +56,16 @@ export const NOTIFICATION_SETTING_FIELDS: Record<string, NotificationSettingFiel
       ['email+slack', t('Send to Email and Slack')],
     ],
   },
+  approval: {
+    name: 'approval',
+    type: 'select',
+    label: t('Approvals'),
+    choices: [
+      ['always', t('On')],
+      ['never', t('Off')],
+    ],
+    help: t('Notifications from teammates that require review or approval.'),
+  },
   reports: {
     name: 'weekly reports',
     type: 'blank',

+ 18 - 4
static/app/views/settings/account/notifications/notificationSettings.tsx

@@ -5,6 +5,8 @@ import AsyncComponent from 'app/components/asyncComponent';
 import Link from 'app/components/links/link';
 import {IconMail} from 'app/icons';
 import {t} from 'app/locale';
+import {Organization} from 'app/types';
+import withOrganizations from 'app/utils/withOrganizations';
 import {
   CONFIRMATION_MESSAGE,
   NOTIFICATION_SETTINGS_TYPES,
@@ -26,7 +28,9 @@ import {FieldObject} from 'app/views/settings/components/forms/type';
 import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
 import TextBlock from 'app/views/settings/components/text/textBlock';
 
-type Props = AsyncComponent['props'];
+type Props = AsyncComponent['props'] & {
+  organizations: Organization[];
+};
 
 type State = {
   notificationSettings: NotificationSettingsObject;
@@ -79,11 +83,21 @@ class NotificationSettings extends AsyncComponent<Props, State> {
     return updatedNotificationSettings;
   };
 
+  get notificationSettingsType() {
+    const hasApprovalFeatureFlag =
+      this.props.organizations.filter(org => org.features?.includes('slack-requests'))
+        .length > 0;
+    // filter out approvals if the feature flag isn't set
+    return NOTIFICATION_SETTINGS_TYPES.filter(
+      type => type !== 'approval' || hasApprovalFeatureFlag
+    );
+  }
+
   getInitialData(): {[key: string]: string} {
     const {notificationSettings} = this.state;
 
     return Object.fromEntries(
-      NOTIFICATION_SETTINGS_TYPES.map(notificationType => [
+      this.notificationSettingsType.map(notificationType => [
         notificationType,
         decideDefault(notificationType, notificationSettings),
       ])
@@ -94,7 +108,7 @@ class NotificationSettings extends AsyncComponent<Props, State> {
     const {notificationSettings} = this.state;
 
     const fields: FieldObject[] = [];
-    for (const notificationType of NOTIFICATION_SETTINGS_TYPES) {
+    for (const notificationType of this.notificationSettingsType) {
       const field = Object.assign({}, NOTIFICATION_SETTING_FIELDS[notificationType], {
         getData: data => this.getStateToPutForDefault(data, notificationType),
         help: (
@@ -162,4 +176,4 @@ class NotificationSettings extends AsyncComponent<Props, State> {
   }
 }
 
-export default NotificationSettings;
+export default withOrganizations(NotificationSettings);

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