accountNotificationFineTuning.tsx 8.0 KB

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