integrationExternalUserMappings.tsx 5.1 KB

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