notificationSettingsByProjects.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  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 Pagination from 'sentry/components/pagination';
  8. import Panel from 'sentry/components/panels/panel';
  9. import PanelBody from 'sentry/components/panels/panelBody';
  10. import PanelHeader from 'sentry/components/panels/panelHeader';
  11. import {t} from 'sentry/locale';
  12. import {Organization, Project} from 'sentry/types';
  13. import {sortProjects} from 'sentry/utils';
  14. import {
  15. MIN_PROJECTS_FOR_PAGINATION,
  16. MIN_PROJECTS_FOR_SEARCH,
  17. NotificationSettingsByProviderObject,
  18. NotificationSettingsObject,
  19. } from 'sentry/views/settings/account/notifications/constants';
  20. import {OrganizationSelectHeader} from 'sentry/views/settings/account/notifications/notificationSettingsByType';
  21. import {
  22. getParentData,
  23. getParentField,
  24. groupByOrganization,
  25. } from 'sentry/views/settings/account/notifications/utils';
  26. import {
  27. RenderSearch,
  28. SearchWrapper,
  29. } from 'sentry/views/settings/components/defaultSearchBar';
  30. export type NotificationSettingsByProjectsBaseProps = {
  31. notificationSettings: NotificationSettingsObject;
  32. notificationType: string;
  33. onChange: (
  34. changedData: NotificationSettingsByProviderObject,
  35. parentId: string
  36. ) => NotificationSettingsObject;
  37. onSubmitSuccess: () => void;
  38. };
  39. export type Props = {
  40. handleOrgChange: Function;
  41. organizationId: string;
  42. organizations: Organization[];
  43. } & NotificationSettingsByProjectsBaseProps &
  44. DeprecatedAsyncComponent['props'];
  45. type State = {
  46. projects: Project[];
  47. } & DeprecatedAsyncComponent['state'];
  48. class NotificationSettingsByProjects extends DeprecatedAsyncComponent<Props, State> {
  49. getDefaultState(): State {
  50. return {
  51. ...super.getDefaultState(),
  52. projects: [],
  53. };
  54. }
  55. getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
  56. return [
  57. [
  58. 'projects',
  59. `/projects/`,
  60. {
  61. query: {organizationId: this.props.organizationId},
  62. },
  63. ],
  64. ];
  65. }
  66. /**
  67. * Check the notification settings for how many projects there are.
  68. */
  69. getProjectCount = (): number => {
  70. const {notificationType, notificationSettings} = this.props;
  71. return Object.values(notificationSettings[notificationType]?.project || {}).length;
  72. };
  73. /**
  74. * The UI expects projects to be grouped by organization but can also use
  75. * this function to make a single group with all organizations.
  76. */
  77. getGroupedProjects = (): {[key: string]: Project[]} => {
  78. const {projects: stateProjects} = this.state;
  79. return Object.fromEntries(
  80. Object.values(groupByOrganization(sortProjects(stateProjects))).map(
  81. ({organization, projects}) => [`${organization.name} Projects`, projects]
  82. )
  83. );
  84. };
  85. handleOrgChange = (option: {label: string; value: string}) => {
  86. // handleOrgChange(option: {label: string; value: string}) {
  87. this.props.handleOrgChange(option);
  88. setTimeout(() => this.reloadData(), 0);
  89. };
  90. renderBody() {
  91. const {notificationType, notificationSettings, onChange, onSubmitSuccess} =
  92. this.props;
  93. const {projects, projectsPageLinks} = this.state;
  94. const canSearch = this.getProjectCount() >= MIN_PROJECTS_FOR_SEARCH;
  95. const shouldPaginate = projects.length >= MIN_PROJECTS_FOR_PAGINATION;
  96. const renderSearch: RenderSearch = ({defaultSearchBar}) => (
  97. <StyledSearchWrapper>{defaultSearchBar}</StyledSearchWrapper>
  98. );
  99. return (
  100. <Fragment>
  101. <PanelHeader>
  102. <OrganizationSelectHeader
  103. organizations={this.props.organizations}
  104. organizationId={this.props.organizationId}
  105. handleOrgChange={this.handleOrgChange}
  106. />
  107. {canSearch &&
  108. this.renderSearchInput({
  109. stateKey: 'projects',
  110. url: `/projects/?organizationId=${this.props.organizationId}`,
  111. placeholder: t('Search Projects'),
  112. children: renderSearch,
  113. })}
  114. </PanelHeader>
  115. <PanelBody>
  116. <Form
  117. saveOnBlur
  118. apiMethod="PUT"
  119. apiEndpoint="/users/me/notification-settings/"
  120. initialData={getParentData(notificationType, notificationSettings, projects)}
  121. onSubmitSuccess={onSubmitSuccess}
  122. >
  123. {projects.length === 0 ? (
  124. <EmptyMessage>{t('No projects found')}</EmptyMessage>
  125. ) : (
  126. Object.entries(this.getGroupedProjects()).map(([groupTitle, parents]) => (
  127. <StyledJsonForm
  128. collapsible
  129. key={groupTitle}
  130. // title={groupTitle}
  131. fields={parents.map(parent =>
  132. getParentField(
  133. notificationType,
  134. notificationSettings,
  135. parent,
  136. onChange
  137. )
  138. )}
  139. />
  140. ))
  141. )}
  142. </Form>
  143. </PanelBody>
  144. {canSearch && shouldPaginate && (
  145. <Pagination pageLinks={projectsPageLinks} {...this.props} />
  146. )}
  147. </Fragment>
  148. );
  149. }
  150. }
  151. export default NotificationSettingsByProjects;
  152. const StyledSearchWrapper = styled(SearchWrapper)`
  153. * {
  154. width: 100%;
  155. }
  156. `;
  157. export const StyledJsonForm = styled(JsonForm)`
  158. ${Panel} {
  159. border: 0;
  160. margin-bottom: 0;
  161. }
  162. `;