diff --git a/static/app/views/settings/account/accountNotificationFineTuning.tsx b/static/app/views/settings/account/accountNotificationFineTuning.tsx index bb8501de30582f..1dbdbb5bff9d51 100644 --- a/static/app/views/settings/account/accountNotificationFineTuning.tsx +++ b/static/app/views/settings/account/accountNotificationFineTuning.tsx @@ -19,7 +19,9 @@ import { ACCOUNT_NOTIFICATION_FIELDS, FineTuneField, } from 'sentry/views/settings/account/notifications/fields'; -import NotificationSettingsByType from 'sentry/views/settings/account/notifications/notificationSettingsByType'; +import NotificationSettingsByType, { + OrganizationSelectHeader, +} from 'sentry/views/settings/account/notifications/notificationSettingsByType'; import { getNotificationTypeFromPathname, groupByOrganization, @@ -71,7 +73,6 @@ function AccountNotificationsByProject({projects, field}: ANBPProps) { {data.map(({name, projects: projectFields}) => (
- {name} {projectFields.map(f => ( | null; notifications: Record | null; + organizationId: string; projects: Project[] | null; }; class AccountNotificationFineTuning extends DeprecatedAsyncView { + getDefaultState() { + return { + ...super.getDefaultState(), + emails: [], + fineTuneData: null, + notifications: [], + projects: [], + organizationId: this.props.organizations[0].id, + }; + } + getEndpoints(): ReturnType { const {fineTuneType: pathnameType} = this.props.params; + const orgId = this.state?.organizationId || this.props.organizations[0].id; const fineTuneType = getNotificationTypeFromPathname(pathnameType); const endpoints = [ ['notifications', '/users/me/notifications/'], @@ -148,7 +162,7 @@ class AccountNotificationFineTuning extends DeprecatedAsyncView { ]; if (isGroupedByProject(fineTuneType)) { - endpoints.push(['projects', '/projects/']); + endpoints.push(['projects', `/projects/?organization_id=${orgId}`]); } endpoints.push(['emails', '/users/me/emails/']); @@ -178,6 +192,14 @@ class AccountNotificationFineTuning extends DeprecatedAsyncView { ); } + handleOrgChange = (option: {label: string; value: string}) => { + this.setState({organizationId: option.value}); + const self = this; + setTimeout(() => { + self.reloadData(); + }, 0); + }; + renderBody() { const {params} = this.props; const {fineTuneType: pathnameType} = params; @@ -204,7 +226,6 @@ class AccountNotificationFineTuning extends DeprecatedAsyncView { if (!notifications || !fineTuneData) { return null; } - return (
@@ -227,19 +248,25 @@ class AccountNotificationFineTuning extends DeprecatedAsyncView { )} + + {isProject ? ( + + + {this.renderSearchInput({ + placeholder: t('Search Projects'), + url, + stateKey, + })} + + ) : ( + {t('Organizations')} + )} + - - {isProject ? t('Projects') : t('Organizations')} -
- {isProject && - this.renderSearchInput({ - placeholder: t('Search Projects'), - url, - stateKey, - })} -
-
-
{ getEndpoints(): ReturnType { return [ - ['notificationSettings', `/users/me/notification-settings/`], + ['notificationSettings', `/users/me/notification-settings/`, {v2: 'serializer'}], ['legacyData', '/users/me/notifications/'], ]; } diff --git a/static/app/views/settings/account/notifications/notificationSettingsByOrganization.tsx b/static/app/views/settings/account/notifications/notificationSettingsByOrganization.tsx index 9bd73f0b1eeedd..5d62e902a33e5c 100644 --- a/static/app/views/settings/account/notifications/notificationSettingsByOrganization.tsx +++ b/static/app/views/settings/account/notifications/notificationSettingsByOrganization.tsx @@ -1,5 +1,4 @@ import Form from 'sentry/components/forms/form'; -import JsonForm from 'sentry/components/forms/jsonForm'; import {t} from 'sentry/locale'; import {OrganizationSummary} from 'sentry/types'; import withOrganizations from 'sentry/utils/withOrganizations'; @@ -7,6 +6,7 @@ import { NotificationSettingsByProviderObject, NotificationSettingsObject, } from 'sentry/views/settings/account/notifications/constants'; +import {StyledJsonForm} from 'sentry/views/settings/account/notifications/notificationSettingsByProjects'; import { getParentData, getParentField, @@ -38,11 +38,16 @@ function NotificationSettingsByOrganization({ initialData={getParentData(notificationType, notificationSettings, organizations)} onSubmitSuccess={onSubmitSuccess} > - - getParentField(notificationType, notificationSettings, organization, onChange) - )} + fields={organizations.map(organization => { + return getParentField( + notificationType, + notificationSettings, + organization, + onChange + ); + })} /> ); diff --git a/static/app/views/settings/account/notifications/notificationSettingsByProjects.spec.tsx b/static/app/views/settings/account/notifications/notificationSettingsByProjects.spec.tsx index 4ac17e252eb5bf..5d975c0c8e8a01 100644 --- a/static/app/views/settings/account/notifications/notificationSettingsByProjects.spec.tsx +++ b/static/app/views/settings/account/notifications/notificationSettingsByProjects.spec.tsx @@ -5,10 +5,10 @@ import {Project} from 'sentry/types'; import NotificationSettingsByProjects from 'sentry/views/settings/account/notifications/notificationSettingsByProjects'; const renderComponent = (projects: Project[]) => { - const {routerContext} = initializeOrg(); + const {routerContext, organization} = initializeOrg(); MockApiClient.addMockResponse({ - url: '/projects/', + url: `/projects/`, method: 'GET', body: projects, }); @@ -28,6 +28,9 @@ const renderComponent = (projects: Project[]) => { notificationSettings={notificationSettings} onChange={jest.fn()} onSubmitSuccess={jest.fn()} + organizationId={organization.id} + organizations={[organization]} + handleOrgChange={jest.fn()} />, {context: routerContext} ); diff --git a/static/app/views/settings/account/notifications/notificationSettingsByProjects.tsx b/static/app/views/settings/account/notifications/notificationSettingsByProjects.tsx index def32bddfa6861..418990727d44b4 100644 --- a/static/app/views/settings/account/notifications/notificationSettingsByProjects.tsx +++ b/static/app/views/settings/account/notifications/notificationSettingsByProjects.tsx @@ -6,8 +6,11 @@ import EmptyMessage from 'sentry/components/emptyMessage'; import Form from 'sentry/components/forms/form'; import JsonForm from 'sentry/components/forms/jsonForm'; 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 {t} from 'sentry/locale'; -import {Project} from 'sentry/types'; +import {Organization, Project} from 'sentry/types'; import {sortProjects} from 'sentry/utils'; import { MIN_PROJECTS_FOR_PAGINATION, @@ -15,6 +18,7 @@ import { NotificationSettingsByProviderObject, NotificationSettingsObject, } from 'sentry/views/settings/account/notifications/constants'; +import {OrganizationSelectHeader} from 'sentry/views/settings/account/notifications/notificationSettingsByType'; import { getParentData, getParentField, @@ -25,7 +29,7 @@ import { SearchWrapper, } from 'sentry/views/settings/components/defaultSearchBar'; -type Props = { +export type NotificationSettingsByProjectsBaseProps = { notificationSettings: NotificationSettingsObject; notificationType: string; onChange: ( @@ -33,7 +37,14 @@ type Props = { parentId: string ) => NotificationSettingsObject; onSubmitSuccess: () => void; -} & DeprecatedAsyncComponent['props']; +}; + +export type Props = { + handleOrgChange: Function; + organizationId: string; + organizations: Organization[]; +} & NotificationSettingsByProjectsBaseProps & + DeprecatedAsyncComponent['props']; type State = { projects: Project[]; @@ -48,7 +59,15 @@ class NotificationSettingsByProjects extends DeprecatedAsyncComponent { - return [['projects', '/projects/']]; + return [ + [ + 'projects', + `/projects/`, + { + query: {organizationId: this.props.organizationId}, + }, + ], + ]; } /** @@ -74,6 +93,12 @@ class NotificationSettingsByProjects extends DeprecatedAsyncComponent { + // handleOrgChange(option: {label: string; value: string}) { + this.props.handleOrgChange(option); + setTimeout(() => this.reloadData(), 0); + }; + renderBody() { const {notificationType, notificationSettings, onChange, onSubmitSuccess} = this.props; @@ -88,35 +113,50 @@ class NotificationSettingsByProjects extends DeprecatedAsyncComponent - {canSearch && - this.renderSearchInput({ - stateKey: 'projects', - url: '/projects/', - placeholder: t('Search Projects'), - children: renderSearch, - })} -
- {projects.length === 0 ? ( - {t('No projects found')} - ) : ( - Object.entries(this.getGroupedProjects()).map(([groupTitle, parents]) => ( - - getParentField(notificationType, notificationSettings, parent, onChange) - )} - /> - )) - )} - + + + + {canSearch && + this.renderSearchInput({ + stateKey: 'projects', + url: `/projects/?organizationId=${this.props.organizationId}`, + placeholder: t('Search Projects'), + children: renderSearch, + })} + + +
+ {projects.length === 0 ? ( + {t('No projects found')} + ) : ( + Object.entries(this.getGroupedProjects()).map(([groupTitle, parents]) => ( + + getParentField( + notificationType, + notificationSettings, + parent, + onChange + ) + )} + /> + )) + )} + +
{canSearch && shouldPaginate && ( )} @@ -132,3 +172,10 @@ const StyledSearchWrapper = styled(SearchWrapper)` width: 100%; } `; + +export const StyledJsonForm = styled(JsonForm)` + ${Panel} { + border: 0; + margin-bottom: 0; + } +`; diff --git a/static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx b/static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx index 5720e97d524bae..0142a84af99e72 100644 --- a/static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx +++ b/static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx @@ -30,7 +30,7 @@ function renderMockRequests( }); MockApiClient.addMockResponse({ - url: '/projects/', + url: `/projects/`, method: 'GET', body: [], }); diff --git a/static/app/views/settings/account/notifications/notificationSettingsByType.tsx b/static/app/views/settings/account/notifications/notificationSettingsByType.tsx index 725b0150f6131b..29f3fb1a9206bf 100644 --- a/static/app/views/settings/account/notifications/notificationSettingsByType.tsx +++ b/static/app/views/settings/account/notifications/notificationSettingsByType.tsx @@ -1,9 +1,11 @@ import {Fragment} from 'react'; import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; +import SelectControl from 'sentry/components/forms/controls/selectControl'; 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 {Organization, OrganizationSummary} from 'sentry/types'; @@ -40,6 +42,45 @@ import { import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; +type OrganizationSelectHeaderProps = { + handleOrgChange: Function; + organizationId: string; + organizations: Organization[]; +}; + +export function OrganizationSelectHeader({ + handleOrgChange, + organizationId, + organizations, +}: OrganizationSelectHeaderProps) { + const getOrganizationOptions = () => { + return organizations.map(org => { + return { + label: org.name, + value: org.id, + }; + }); + }; + + return ( + + {t('Settings for Organization')} + option.value} + value={organizationId} + styles={{ + container: (provided: {[x: string]: string | number | boolean}) => ({ + ...provided, + minWidth: `300px`, + }), + }} + /> + + ); +} + type Props = { notificationType: string; organizations: Organization[]; @@ -78,6 +119,7 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent notificationSettings: {}, identities: [], organizationIntegrations: [], + organizationId: this.props.organizations[0].id, }; } @@ -87,7 +129,7 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent [ 'notificationSettings', `/users/me/notification-settings/`, - {query: getQueryParams(notificationType)}, + {query: getQueryParams(notificationType), v2: 'serializer'}, ], ['identities', `/users/me/identities/`, {query: {provider: 'slack'}}], [ @@ -318,6 +360,10 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent }); }; + handleOrgChange = (option: {label: string; value: string}) => { + this.setState({organizationId: option.value}); + }; + renderBody() { const {notificationType} = this.props; const {notificationSettings} = this.state; @@ -350,22 +396,28 @@ class NotificationSettingsByType extends DeprecatedAsyncComponent fields={this.getFields()} /> - {!isEverythingDisabled(notificationType, notificationSettings) && - (isGroupedByProject(notificationType) ? ( - this.trackTuningUpdated('project')} - /> - ) : ( - this.trackTuningUpdated('organization')} - /> - ))} + {!isEverythingDisabled(notificationType, notificationSettings) && ( + + {isGroupedByProject(notificationType) ? ( + this.trackTuningUpdated('project')} + organizations={this.props.organizations} + organizationId={this.state.organizationId} + handleOrgChange={this.handleOrgChange} + /> + ) : ( + this.trackTuningUpdated('organization')} + /> + )} + + )} ); } diff --git a/static/app/views/settings/account/notifications/utils.tsx b/static/app/views/settings/account/notifications/utils.tsx index a5ad221e5b74dd..418dde83656db9 100644 --- a/static/app/views/settings/account/notifications/utils.tsx +++ b/static/app/views/settings/account/notifications/utils.tsx @@ -53,7 +53,18 @@ export const getFallBackValue = (notificationType: string): string => { return 'committed_only'; case 'workflow': return 'subscribe_only'; + case 'approval': + return 'always'; + case 'quota': + return 'always'; + case 'spikeProtection': + return 'always'; + case 'reports': + return 'always'; default: + // These are the expected potential settings with fallback of '' + // issue, quotaErrors, quotaTransactions, quotaAttachments, + // quotaReplays, quotaWarnings, quotaSpendAllocations return ''; } };