accountNotificationFineTuning.tsx 7.8 KB

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