accountNotificationFineTuning.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import EmptyMessage from 'sentry/components/emptyMessage';
  5. import SelectField from 'sentry/components/forms/fields/selectField';
  6. import Form from 'sentry/components/forms/form';
  7. import JsonForm from 'sentry/components/forms/jsonForm';
  8. import Pagination from 'sentry/components/pagination';
  9. import {Panel, PanelBody, PanelHeader} from 'sentry/components/panels';
  10. import {fields} from 'sentry/data/forms/accountNotificationSettings';
  11. import {t} from 'sentry/locale';
  12. import {Organization, Project, UserEmail} from 'sentry/types';
  13. import withOrganizations from 'sentry/utils/withOrganizations';
  14. import AsyncView from 'sentry/views/asyncView';
  15. import {
  16. ACCOUNT_NOTIFICATION_FIELDS,
  17. FineTuneField,
  18. } from 'sentry/views/settings/account/notifications/fields';
  19. import NotificationSettingsByType from 'sentry/views/settings/account/notifications/notificationSettingsByType';
  20. import {
  21. getNotificationTypeFromPathname,
  22. groupByOrganization,
  23. isGroupedByProject,
  24. } from 'sentry/views/settings/account/notifications/utils';
  25. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  26. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  27. const PanelBodyLineItem = styled(PanelBody)`
  28. font-size: 1rem;
  29. &:not(:last-child) {
  30. border-bottom: 1px solid ${p => p.theme.innerBorder};
  31. }
  32. `;
  33. const accountNotifications = [
  34. 'alerts',
  35. 'deploy',
  36. 'workflow',
  37. 'activeRelease',
  38. 'approval',
  39. 'quota',
  40. 'spikeProtection',
  41. ];
  42. type ANBPProps = {
  43. field: FineTuneField;
  44. projects: Project[];
  45. };
  46. const AccountNotificationsByProject = ({projects, field}: ANBPProps) => {
  47. const projectsByOrg = groupByOrganization(projects);
  48. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  49. const {title, description, ...fieldConfig} = field;
  50. // Display as select box in this view regardless of the type specified in the config
  51. const data = Object.values(projectsByOrg).map(org => ({
  52. name: org.organization.name,
  53. projects: org.projects.map(project => ({
  54. ...fieldConfig,
  55. // `name` key refers to field name
  56. // we use project.id because slugs are not unique across orgs
  57. name: project.id,
  58. label: project.slug,
  59. })),
  60. }));
  61. return (
  62. <Fragment>
  63. {data.map(({name, projects: projectFields}) => (
  64. <div key={name}>
  65. <PanelHeader>{name}</PanelHeader>
  66. {projectFields.map(f => (
  67. <PanelBodyLineItem key={f.name}>
  68. <SelectField
  69. defaultValue={f.defaultValue}
  70. name={f.name}
  71. options={f.options}
  72. label={f.label}
  73. />
  74. </PanelBodyLineItem>
  75. ))}
  76. </div>
  77. ))}
  78. </Fragment>
  79. );
  80. };
  81. type ANBOProps = {
  82. field: FineTuneField;
  83. organizations: Organization[];
  84. };
  85. const AccountNotificationsByOrganization = ({organizations, field}: ANBOProps) => {
  86. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  87. const {title, description, ...fieldConfig} = field;
  88. // Display as select box in this view regardless of the type specified in the config
  89. const data = organizations.map(org => ({
  90. ...fieldConfig,
  91. // `name` key refers to field name
  92. // we use org.id to remain consistent project.id use (which is required because slugs are not unique across orgs)
  93. name: org.id,
  94. label: org.slug,
  95. }));
  96. return (
  97. <Fragment>
  98. {data.map(f => (
  99. <PanelBodyLineItem key={f.name}>
  100. <SelectField
  101. defaultValue={f.defaultValue}
  102. name={f.name}
  103. options={f.options}
  104. label={f.label}
  105. />
  106. </PanelBodyLineItem>
  107. ))}
  108. </Fragment>
  109. );
  110. };
  111. const AccountNotificationsByOrganizationContainer = withOrganizations(
  112. AccountNotificationsByOrganization
  113. );
  114. type Props = AsyncView['props'] &
  115. RouteComponentProps<{fineTuneType: string}, {}> & {
  116. organizations: Organization[];
  117. };
  118. type State = AsyncView['state'] & {
  119. emails: UserEmail[] | null;
  120. fineTuneData: Record<string, any> | null;
  121. notifications: Record<string, any> | null;
  122. projects: Project[] | null;
  123. };
  124. class AccountNotificationFineTuning extends AsyncView<Props, State> {
  125. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  126. const {fineTuneType: pathnameType} = this.props.params;
  127. const fineTuneType = getNotificationTypeFromPathname(pathnameType);
  128. const endpoints = [
  129. ['notifications', '/users/me/notifications/'],
  130. ['fineTuneData', `/users/me/notifications/${fineTuneType}/`],
  131. ];
  132. if (isGroupedByProject(fineTuneType)) {
  133. endpoints.push(['projects', '/projects/']);
  134. }
  135. endpoints.push(['emails', '/users/me/emails/']);
  136. if (fineTuneType === 'email') {
  137. endpoints.push(['emails', '/users/me/emails/']);
  138. }
  139. return endpoints as ReturnType<AsyncView['getEndpoints']>;
  140. }
  141. // Return a sorted list of user's verified emails
  142. get emailChoices() {
  143. return (
  144. this.state.emails
  145. ?.filter(({isVerified}) => isVerified)
  146. ?.sort((a, b) => {
  147. // Sort by primary -> email
  148. if (a.isPrimary) {
  149. return -1;
  150. }
  151. if (b.isPrimary) {
  152. return 1;
  153. }
  154. return a.email < b.email ? -1 : 1;
  155. }) ?? []
  156. );
  157. }
  158. renderBody() {
  159. const {params} = this.props;
  160. const {fineTuneType: pathnameType} = params;
  161. const fineTuneType = getNotificationTypeFromPathname(pathnameType);
  162. if (accountNotifications.includes(fineTuneType)) {
  163. return <NotificationSettingsByType notificationType={fineTuneType} />;
  164. }
  165. const {notifications, projects, fineTuneData, projectsPageLinks} = this.state;
  166. const isProject = isGroupedByProject(fineTuneType);
  167. const field = ACCOUNT_NOTIFICATION_FIELDS[fineTuneType];
  168. const {title, description} = field;
  169. const [stateKey, url] = isProject ? this.getEndpoints()[2] : [];
  170. const hasProjects = !!projects?.length;
  171. if (fineTuneType === 'email') {
  172. // Fetch verified email addresses
  173. field.options = this.emailChoices.map(({email}) => ({value: email, label: email}));
  174. }
  175. if (!notifications || !fineTuneData) {
  176. return null;
  177. }
  178. return (
  179. <div>
  180. <SettingsPageHeader title={title} />
  181. {description && <TextBlock>{description}</TextBlock>}
  182. {field &&
  183. field.defaultFieldName &&
  184. // not implemented yet
  185. field.defaultFieldName !== 'weeklyReports' && (
  186. <Form
  187. saveOnBlur
  188. apiMethod="PUT"
  189. apiEndpoint="/users/me/notifications/"
  190. initialData={notifications}
  191. >
  192. <JsonForm
  193. title={`Default ${title}`}
  194. fields={[fields[field.defaultFieldName]]}
  195. />
  196. </Form>
  197. )}
  198. <Panel>
  199. <PanelBody>
  200. <PanelHeader hasButtons={isProject}>
  201. <Heading>{isProject ? t('Projects') : t('Organizations')}</Heading>
  202. <div>
  203. {isProject &&
  204. this.renderSearchInput({
  205. placeholder: t('Search Projects'),
  206. url,
  207. stateKey,
  208. })}
  209. </div>
  210. </PanelHeader>
  211. <Form
  212. saveOnBlur
  213. apiMethod="PUT"
  214. apiEndpoint={`/users/me/notifications/${fineTuneType}/`}
  215. initialData={fineTuneData}
  216. >
  217. {isProject && hasProjects && (
  218. <AccountNotificationsByProject projects={projects!} field={field} />
  219. )}
  220. {isProject && !hasProjects && (
  221. <EmptyMessage>{t('No projects found')}</EmptyMessage>
  222. )}
  223. {!isProject && (
  224. <AccountNotificationsByOrganizationContainer field={field} />
  225. )}
  226. </Form>
  227. </PanelBody>
  228. </Panel>
  229. {projects && <Pagination pageLinks={projectsPageLinks} {...this.props} />}
  230. </div>
  231. );
  232. }
  233. }
  234. const Heading = styled('div')`
  235. flex: 1;
  236. `;
  237. export default AccountNotificationFineTuning;