accountNotificationFineTuning.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import Form from 'sentry/components/forms/form';
  5. import JsonForm from 'sentry/components/forms/jsonForm';
  6. import SelectField from 'sentry/components/forms/selectField';
  7. import Pagination from 'sentry/components/pagination';
  8. import {Panel, PanelBody, PanelHeader} from 'sentry/components/panels';
  9. import {fields} from 'sentry/data/forms/accountNotificationSettings';
  10. import {t} from 'sentry/locale';
  11. import {Organization, Project, UserEmail} from 'sentry/types';
  12. import withOrganizations from 'sentry/utils/withOrganizations';
  13. import AsyncView from 'sentry/views/asyncView';
  14. import {
  15. ACCOUNT_NOTIFICATION_FIELDS,
  16. FineTuneField,
  17. } from 'sentry/views/settings/account/notifications/fields';
  18. import NotificationSettingsByType from 'sentry/views/settings/account/notifications/notificationSettingsByType';
  19. import {
  20. groupByOrganization,
  21. isGroupedByProject,
  22. } from 'sentry/views/settings/account/notifications/utils';
  23. import EmptyMessage from 'sentry/views/settings/components/emptyMessage';
  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 (['alerts', 'deploy', 'workflow', 'approval', 'quota'].includes(fineTuneType)) {
  151. return <NotificationSettingsByType notificationType={fineTuneType} />;
  152. }
  153. const {notifications, projects, fineTuneData, projectsPageLinks} = this.state;
  154. const isProject = isGroupedByProject(fineTuneType);
  155. const field = ACCOUNT_NOTIFICATION_FIELDS[fineTuneType];
  156. const {title, description} = field;
  157. const [stateKey, url] = isProject ? this.getEndpoints()[2] : [];
  158. const hasProjects = !!projects?.length;
  159. if (fineTuneType === 'email') {
  160. // Fetch verified email addresses
  161. field.options = this.emailChoices.map(({email}) => ({value: email, label: email}));
  162. }
  163. if (!notifications || !fineTuneData) {
  164. return null;
  165. }
  166. return (
  167. <div>
  168. <SettingsPageHeader title={title} />
  169. {description && <TextBlock>{description}</TextBlock>}
  170. {field &&
  171. field.defaultFieldName &&
  172. // not implemented yet
  173. field.defaultFieldName !== 'weeklyReports' && (
  174. <Form
  175. saveOnBlur
  176. apiMethod="PUT"
  177. apiEndpoint="/users/me/notifications/"
  178. initialData={notifications}
  179. >
  180. <JsonForm
  181. title={`Default ${title}`}
  182. fields={[fields[field.defaultFieldName]]}
  183. />
  184. </Form>
  185. )}
  186. <Panel>
  187. <PanelBody>
  188. <PanelHeader hasButtons={isProject}>
  189. <Heading>{isProject ? t('Projects') : t('Organizations')}</Heading>
  190. <div>
  191. {isProject &&
  192. this.renderSearchInput({
  193. placeholder: t('Search Projects'),
  194. url,
  195. stateKey,
  196. })}
  197. </div>
  198. </PanelHeader>
  199. <Form
  200. saveOnBlur
  201. apiMethod="PUT"
  202. apiEndpoint={`/users/me/notifications/${fineTuneType}/`}
  203. initialData={fineTuneData}
  204. >
  205. {isProject && hasProjects && (
  206. <AccountNotificationsByProject projects={projects!} field={field} />
  207. )}
  208. {isProject && !hasProjects && (
  209. <EmptyMessage>{t('No projects found')}</EmptyMessage>
  210. )}
  211. {!isProject && (
  212. <AccountNotificationsByOrganizationContainer field={field} />
  213. )}
  214. </Form>
  215. </PanelBody>
  216. </Panel>
  217. {projects && <Pagination pageLinks={projectsPageLinks} {...this.props} />}
  218. </div>
  219. );
  220. }
  221. }
  222. const Heading = styled('div')`
  223. flex: 1;
  224. `;
  225. export default AccountNotificationFineTuning;