integrationExternalMappingForm.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import capitalize from 'lodash/capitalize';
  4. import pick from 'lodash/pick';
  5. import {t, tct} from 'app/locale';
  6. import {ExternalActorMapping, Integration, Organization} from 'app/types';
  7. import {FieldFromConfig} from 'app/views/settings/components/forms';
  8. import Form from 'app/views/settings/components/forms/form';
  9. import {Field} from 'app/views/settings/components/forms/type';
  10. type Props = Pick<Form['props'], 'onSubmitSuccess' | 'onCancel'> &
  11. Partial<Pick<Form['props'], 'onSubmit'>> & {
  12. organization: Organization;
  13. integration: Integration;
  14. mapping?: ExternalActorMapping;
  15. type: 'user' | 'team';
  16. baseEndpoint?: string;
  17. sentryNamesMapper: (v: any) => {id: string; name: string}[];
  18. url: string;
  19. onResults?: (data: any) => void;
  20. };
  21. export default class IntegrationExternalMappingForm extends Component<Props> {
  22. get initialData() {
  23. const {integration, mapping} = this.props;
  24. return {
  25. externalName: '',
  26. userId: '',
  27. teamId: '',
  28. sentryName: '',
  29. provider: integration.provider.key,
  30. integrationId: integration.id,
  31. ...pick(mapping, ['externalName', 'userId', 'sentryName', 'teamId']),
  32. };
  33. }
  34. get formFields(): Field[] {
  35. const {type, sentryNamesMapper, url, mapping} = this.props;
  36. const optionMapper = sentryNames =>
  37. sentryNames.map(({name, id}) => ({value: id, label: name}));
  38. const fields: any[] = [
  39. {
  40. name: 'externalName',
  41. type: 'string',
  42. required: true,
  43. label: tct('External [type]', {type: capitalize(type)}),
  44. placeholder: t(`${type === 'team' ? '@org/teamname' : '@username'}`),
  45. },
  46. ];
  47. if (type === 'user') {
  48. fields.push({
  49. name: 'userId',
  50. type: 'select_async',
  51. required: true,
  52. label: tct('Sentry [type]', {type: capitalize(type)}),
  53. placeholder: t(`Choose your Sentry User`),
  54. url,
  55. onResults: result => {
  56. // For organizations with >100 users, we want to make sure their
  57. // saved mapping gets populated in the results if it wouldn't have
  58. // been in the initial 100 API results, which is why we add it here
  59. if (mapping && !result.find(({user}) => user.id === mapping.userId)) {
  60. result = [{id: mapping.userId, name: mapping.sentryName}, ...result];
  61. }
  62. this.props.onResults?.(result);
  63. return optionMapper(sentryNamesMapper(result));
  64. },
  65. });
  66. }
  67. if (type === 'team') {
  68. fields.push({
  69. name: 'teamId',
  70. type: 'select_async',
  71. required: true,
  72. label: tct('Sentry [type]', {type: capitalize(type)}),
  73. placeholder: t(`Choose your Sentry Team`),
  74. url,
  75. onResults: result => {
  76. // For organizations with >100 teams, we want to make sure their
  77. // saved mapping gets populated in the results if it wouldn't have
  78. // been in the initial 100 API results, which is why we add it here
  79. if (mapping && !result.find(({id}) => id === mapping.teamId)) {
  80. result = [{id: mapping.teamId, name: mapping.sentryName}, ...result];
  81. }
  82. // The team needs `this.props.onResults` so that we have team slug
  83. // when a user submits a team mapping, the endpoint needs the slug
  84. // as a path param: /teams/${organization.slug}/${team.slug}/external-teams/
  85. this.props.onResults?.(result);
  86. return optionMapper(sentryNamesMapper(result));
  87. },
  88. });
  89. }
  90. return fields;
  91. }
  92. render() {
  93. const {onSubmitSuccess, onCancel, mapping, baseEndpoint, onSubmit} = this.props;
  94. // endpoint changes if we are making a new row or updating an existing one
  95. const endpoint = !baseEndpoint
  96. ? undefined
  97. : mapping
  98. ? `${baseEndpoint}${mapping.id}/`
  99. : baseEndpoint;
  100. const apiMethod = !baseEndpoint ? undefined : mapping ? 'PUT' : 'POST';
  101. return (
  102. <FormWrapper>
  103. <Form
  104. requireChanges
  105. onSubmitSuccess={onSubmitSuccess}
  106. initialData={this.initialData}
  107. apiEndpoint={endpoint}
  108. apiMethod={apiMethod}
  109. onCancel={onCancel}
  110. onSubmit={onSubmit}
  111. >
  112. {this.formFields.map(field => (
  113. <FieldFromConfig
  114. key={field.name}
  115. field={field}
  116. inline={false}
  117. stacked
  118. flexibleControlStateSize
  119. />
  120. ))}
  121. </Form>
  122. </FormWrapper>
  123. );
  124. }
  125. }
  126. // Prevents errors from appearing off the modal
  127. const FormWrapper = styled('div')`
  128. position: relative;
  129. `;