integrationExternalUserMappings.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import {Fragment} from 'react';
  2. import type {WithRouterProps} from 'react-router';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  6. import {t} from 'sentry/locale';
  7. import type {
  8. ExternalActorMapping,
  9. ExternalActorMappingOrSuggestion,
  10. ExternalUser,
  11. Integration,
  12. } from 'sentry/types/integrations';
  13. import type {Member, Organization} from 'sentry/types/organization';
  14. import {sentryNameToOption} from 'sentry/utils/integrationUtil';
  15. import withOrganization from 'sentry/utils/withOrganization';
  16. // eslint-disable-next-line no-restricted-imports
  17. import withSentryRouter from 'sentry/utils/withSentryRouter';
  18. import IntegrationExternalMappingForm from './integrationExternalMappingForm';
  19. import IntegrationExternalMappings from './integrationExternalMappings';
  20. type Props = DeprecatedAsyncComponent['props'] &
  21. WithRouterProps & {
  22. integration: Integration;
  23. organization: Organization;
  24. };
  25. type State = DeprecatedAsyncComponent['state'] & {
  26. initialResults: Member[];
  27. members: (Member & {externalUsers: ExternalUser[]})[];
  28. };
  29. class IntegrationExternalUserMappings extends DeprecatedAsyncComponent<Props, State> {
  30. getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
  31. const {organization} = this.props;
  32. return [
  33. // We paginate on this query, since we're filtering by hasExternalUsers:true
  34. [
  35. 'members',
  36. `/organizations/${organization.slug}/members/`,
  37. {query: {query: 'hasExternalUsers:true', expand: 'externalUsers'}},
  38. ],
  39. // We use this query as defaultOptions to reduce identical API calls
  40. ['initialResults', `/organizations/${organization.slug}/members/`],
  41. ];
  42. }
  43. handleDelete = async (mapping: ExternalActorMapping) => {
  44. const {organization} = this.props;
  45. try {
  46. await this.api.requestPromise(
  47. `/organizations/${organization.slug}/external-users/${mapping.id}/`,
  48. {
  49. method: 'DELETE',
  50. }
  51. );
  52. // remove config and update state
  53. addSuccessMessage(t('Deletion successful'));
  54. this.fetchData();
  55. } catch {
  56. // no 4xx errors should happen on delete
  57. addErrorMessage(t('An error occurred'));
  58. }
  59. };
  60. handleSubmitSuccess = () => {
  61. // Don't bother updating state. The info is in array of objects for each object in another array of objects.
  62. // Easier and less error-prone to re-fetch the data and re-calculate state.
  63. this.fetchData();
  64. };
  65. get mappings() {
  66. const {integration} = this.props;
  67. const {members} = this.state;
  68. const externalUserMappings = members.reduce<ExternalActorMapping[]>((acc, member) => {
  69. const {externalUsers, user} = member;
  70. acc.push(
  71. ...externalUsers
  72. .filter(externalUser => externalUser.provider === integration.provider.key)
  73. .map(externalUser => ({...externalUser, sentryName: user?.name ?? member.name}))
  74. );
  75. return acc;
  76. }, []);
  77. return externalUserMappings.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
  78. }
  79. get dataEndpoint() {
  80. const {organization} = this.props;
  81. return `/organizations/${organization.slug}/members/`;
  82. }
  83. get baseFormEndpoint() {
  84. const {organization} = this.props;
  85. return `/organizations/${organization.slug}/external-users/`;
  86. }
  87. get defaultUserOptions() {
  88. const {initialResults} = this.state;
  89. return this.sentryNamesMapper(initialResults).map(sentryNameToOption);
  90. }
  91. sentryNamesMapper(members: Member[]) {
  92. return members
  93. .filter(member => member.user)
  94. .map(({user, email, name}) => {
  95. const label = email !== name ? `${name} - ${email}` : `${email}`;
  96. return {id: user?.id!, name: label};
  97. });
  98. }
  99. openModal = (mapping?: ExternalActorMappingOrSuggestion) => {
  100. const {integration} = this.props;
  101. openModal(({Body, Header, closeModal}) => (
  102. <Fragment>
  103. <Header closeButton>{t('Configure External User Mapping')}</Header>
  104. <Body>
  105. <IntegrationExternalMappingForm
  106. type="user"
  107. integration={integration}
  108. dataEndpoint={this.dataEndpoint}
  109. getBaseFormEndpoint={() => this.baseFormEndpoint}
  110. defaultOptions={this.defaultUserOptions}
  111. mapping={mapping}
  112. sentryNamesMapper={this.sentryNamesMapper}
  113. onCancel={closeModal}
  114. onSubmitSuccess={() => {
  115. this.handleSubmitSuccess();
  116. closeModal();
  117. }}
  118. />
  119. </Body>
  120. </Fragment>
  121. ));
  122. };
  123. renderBody() {
  124. const {integration, organization} = this.props;
  125. const {membersPageLinks} = this.state;
  126. return (
  127. <Fragment>
  128. <IntegrationExternalMappings
  129. type="user"
  130. integration={integration}
  131. organization={organization}
  132. mappings={this.mappings}
  133. dataEndpoint={this.dataEndpoint}
  134. getBaseFormEndpoint={() => this.baseFormEndpoint}
  135. defaultOptions={this.defaultUserOptions}
  136. sentryNamesMapper={this.sentryNamesMapper}
  137. onCreate={this.openModal}
  138. onDelete={this.handleDelete}
  139. pageLinks={membersPageLinks}
  140. />
  141. </Fragment>
  142. );
  143. }
  144. }
  145. export default withSentryRouter(withOrganization(IntegrationExternalUserMappings));