notificationSettingsByProjects.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import {Fragment} from 'react';
  2. import type {WithRouterProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  5. import EmptyMessage from 'sentry/components/emptyMessage';
  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 from 'sentry/components/panels/panel';
  10. import PanelBody from 'sentry/components/panels/panelBody';
  11. import PanelHeader from 'sentry/components/panels/panelHeader';
  12. import {t} from 'sentry/locale';
  13. import ConfigStore from 'sentry/stores/configStore';
  14. import {space} from 'sentry/styles/space';
  15. import {Organization, Project} from 'sentry/types';
  16. import {sortProjects} from 'sentry/utils';
  17. import parseLinkHeader from 'sentry/utils/parseLinkHeader';
  18. import withSentryRouter from 'sentry/utils/withSentryRouter';
  19. import {
  20. MIN_PROJECTS_FOR_SEARCH,
  21. NotificationSettingsByProviderObject,
  22. NotificationSettingsObject,
  23. } from 'sentry/views/settings/account/notifications/constants';
  24. import {OrganizationSelectHeader} from 'sentry/views/settings/account/notifications/organizationSelectHeader';
  25. import {
  26. getParentData,
  27. getParentField,
  28. groupByOrganization,
  29. } from 'sentry/views/settings/account/notifications/utils';
  30. import {RenderSearch} from 'sentry/views/settings/components/defaultSearchBar';
  31. export type NotificationSettingsByProjectsBaseProps = {
  32. notificationSettings: NotificationSettingsObject;
  33. notificationType: string;
  34. onChange: (
  35. changedData: NotificationSettingsByProviderObject,
  36. parentId: string
  37. ) => NotificationSettingsObject;
  38. onSubmitSuccess: () => void;
  39. };
  40. type Props = {
  41. organizations: Organization[];
  42. } & NotificationSettingsByProjectsBaseProps &
  43. DeprecatedAsyncComponent['props'] &
  44. WithRouterProps;
  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. const organizationId = this.getOrganizationId();
  57. return [
  58. [
  59. 'projects',
  60. `/projects/`,
  61. {
  62. query: {
  63. organizationId,
  64. cursor: this.props.location.query.cursor,
  65. },
  66. },
  67. ],
  68. ];
  69. }
  70. getOrganizationId(): string | undefined {
  71. const {location, organizations} = this.props;
  72. const customerDomain = ConfigStore.get('customerDomain');
  73. const orgFromSubdomain = organizations.find(
  74. ({slug}) => slug === customerDomain?.subdomain
  75. )?.id;
  76. return location?.query?.organizationId ?? orgFromSubdomain ?? organizations[0]?.id;
  77. }
  78. /**
  79. * Check the notification settings for how many projects there are.
  80. */
  81. getProjectCount = (): number => {
  82. const {notificationType, notificationSettings} = this.props;
  83. return Object.values(notificationSettings[notificationType]?.project || {}).length;
  84. };
  85. /**
  86. * The UI expects projects to be grouped by organization but can also use
  87. * this function to make a single group with all organizations.
  88. */
  89. getGroupedProjects = (): {[key: string]: Project[]} => {
  90. const {projects: stateProjects} = this.state;
  91. return Object.fromEntries(
  92. Object.values(groupByOrganization(sortProjects(stateProjects))).map(
  93. ({organization, projects}) => [`${organization.name} Projects`, projects]
  94. )
  95. );
  96. };
  97. handleOrgChange = (organizationId: string) => {
  98. this.props.router.replace({
  99. ...this.props.location,
  100. query: {organizationId},
  101. });
  102. };
  103. renderBody() {
  104. const {notificationType, notificationSettings, onChange, onSubmitSuccess} =
  105. this.props;
  106. const {projects, projectsPageLinks} = this.state;
  107. const canSearch = this.getProjectCount() >= MIN_PROJECTS_FOR_SEARCH;
  108. const paginationObject = parseLinkHeader(projectsPageLinks ?? '');
  109. const hasMore = paginationObject?.next?.results;
  110. const hasPrevious = paginationObject?.previous?.results;
  111. const renderSearch: RenderSearch = ({defaultSearchBar}) => defaultSearchBar;
  112. const orgId = this.getOrganizationId();
  113. return (
  114. <Fragment>
  115. <Panel>
  116. <StyledPanelHeader>
  117. <OrganizationSelectHeader
  118. organizations={this.props.organizations}
  119. organizationId={orgId}
  120. handleOrgChange={this.handleOrgChange}
  121. />
  122. {canSearch &&
  123. this.renderSearchInput({
  124. stateKey: 'projects',
  125. url: `/projects/?organizationId=${orgId}`,
  126. placeholder: t('Search Projects'),
  127. children: renderSearch,
  128. })}
  129. </StyledPanelHeader>
  130. <PanelBody>
  131. <Form
  132. saveOnBlur
  133. apiMethod="PUT"
  134. apiEndpoint="/users/me/notification-settings/"
  135. initialData={getParentData(
  136. notificationType,
  137. notificationSettings,
  138. projects
  139. )}
  140. onSubmitSuccess={onSubmitSuccess}
  141. >
  142. {projects.length === 0 ? (
  143. <EmptyMessage>{t('No projects found')}</EmptyMessage>
  144. ) : (
  145. Object.entries(this.getGroupedProjects()).map(([groupTitle, parents]) => (
  146. <StyledJsonForm
  147. collapsible
  148. key={groupTitle}
  149. // title={groupTitle}
  150. fields={parents.map(parent =>
  151. getParentField(
  152. notificationType,
  153. notificationSettings,
  154. parent,
  155. onChange
  156. )
  157. )}
  158. />
  159. ))
  160. )}
  161. </Form>
  162. </PanelBody>
  163. </Panel>
  164. {canSearch && (hasMore || hasPrevious) && (
  165. <Pagination pageLinks={projectsPageLinks} />
  166. )}
  167. </Fragment>
  168. );
  169. }
  170. }
  171. export default withSentryRouter(NotificationSettingsByProjects);
  172. const StyledPanelHeader = styled(PanelHeader)`
  173. flex-wrap: wrap;
  174. gap: ${space(1)};
  175. & > form:last-child {
  176. flex-grow: 1;
  177. }
  178. `;
  179. export const StyledJsonForm = styled(JsonForm)`
  180. ${Panel} {
  181. border: 0;
  182. margin-bottom: 0;
  183. }
  184. `;