integrationExternalMappingForm.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import capitalize from 'lodash/capitalize';
  4. import {t, tct} from 'sentry/locale';
  5. import {ExternalActorMapping, Integration} from 'sentry/types';
  6. import {getExternalActorEndpointDetails} from 'sentry/utils/integrationUtil';
  7. import {FieldFromConfig} from 'sentry/views/settings/components/forms';
  8. import Form from 'sentry/views/settings/components/forms/form';
  9. import FormModel from 'sentry/views/settings/components/forms/model';
  10. import {Field} from 'sentry/views/settings/components/forms/type';
  11. type Props = Pick<Form['props'], 'onCancel' | 'onSubmitSuccess' | 'onSubmitError'> & {
  12. integration: Integration;
  13. mapping?: ExternalActorMapping;
  14. type: 'user' | 'team';
  15. getBaseFormEndpoint: (mapping?: ExternalActorMapping) => string;
  16. sentryNamesMapper: (v: any) => {id: string; name: string}[];
  17. dataEndpoint: string;
  18. onResults?: (data: any, mappingKey?: string) => void;
  19. isInline?: boolean;
  20. mappingKey?: string;
  21. };
  22. export default class IntegrationExternalMappingForm extends Component<Props> {
  23. model = new FormModel();
  24. get initialData() {
  25. const {integration, mapping} = this.props;
  26. return {
  27. provider: integration.provider.key,
  28. integrationId: integration.id,
  29. ...mapping,
  30. };
  31. }
  32. get formFields(): Field[] {
  33. const {
  34. dataEndpoint,
  35. isInline,
  36. mapping,
  37. mappingKey,
  38. onResults,
  39. sentryNamesMapper,
  40. type,
  41. } = this.props;
  42. const optionMapper = sentryNames =>
  43. sentryNames.map(({name, id}) => ({value: id, label: name}));
  44. const fields: Field[] = [
  45. {
  46. name: `${type}Id`,
  47. type: 'select_async',
  48. required: true,
  49. label: isInline ? undefined : tct('Sentry [type]', {type: capitalize(type)}),
  50. placeholder: t(`Select Sentry ${capitalize(type)}`),
  51. url: dataEndpoint,
  52. onResults: result => {
  53. onResults?.(result, isInline ? mapping?.externalName : mappingKey);
  54. // TODO(Leander): The code below only fixes the problem when viewed, not when edited
  55. // Pagination still has bugs for results not on initial return of the query
  56. // For organizations with >100 entries, 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 (
  60. mapping &&
  61. !result.find(entry => {
  62. const id = type === 'user' ? entry.user.id : entry.id;
  63. return id === mapping[`${type}Id`];
  64. })
  65. ) {
  66. return optionMapper([
  67. {id: mapping[`${type}Id`], name: mapping.sentryName},
  68. ...sentryNamesMapper(result),
  69. ]);
  70. }
  71. return optionMapper(sentryNamesMapper(result));
  72. },
  73. },
  74. ];
  75. // We only add the field for externalName if it's the full (not inline) form
  76. if (!isInline) {
  77. fields.unshift({
  78. name: 'externalName',
  79. type: 'string',
  80. required: true,
  81. label: isInline ? undefined : tct('External [type]', {type: capitalize(type)}),
  82. placeholder: type === 'user' ? t('@username') : t('@org/teamname'),
  83. });
  84. }
  85. return fields;
  86. }
  87. get extraFormFieldProps() {
  88. const {isInline} = this.props;
  89. return isInline
  90. ? {
  91. // We need to submit the entire model since it could be a new one or an update
  92. getData: () => this.model.getData(),
  93. // We need to update the model onBlur for inline forms since the model's 'onPreSubmit' hook
  94. // does NOT run when using `saveOnBlur`.
  95. onBlur: () => this.updateModel(),
  96. }
  97. : {flexibleControlStateSize: true};
  98. }
  99. // This function is necessary since the endpoint we submit to changes depending on the value selected
  100. updateModel() {
  101. const mapping = this.model.getData() as ExternalActorMapping;
  102. const {getBaseFormEndpoint} = this.props;
  103. if (mapping) {
  104. const endpointDetails = getExternalActorEndpointDetails(
  105. getBaseFormEndpoint(mapping),
  106. mapping
  107. );
  108. this.model.setFormOptions({...this.model.options, ...endpointDetails});
  109. }
  110. }
  111. render() {
  112. const {isInline, onCancel, onSubmitError, onSubmitSuccess} = this.props;
  113. return (
  114. <FormWrapper>
  115. <Form
  116. requireChanges
  117. model={this.model}
  118. initialData={this.initialData}
  119. onCancel={onCancel}
  120. onSubmitSuccess={onSubmitSuccess}
  121. onSubmitError={onSubmitError}
  122. saveOnBlur={isInline}
  123. allowUndo={isInline}
  124. onPreSubmit={() => this.updateModel()}
  125. >
  126. {this.formFields.map(field => (
  127. <FieldFromConfig
  128. key={field.name}
  129. field={field}
  130. inline={false}
  131. stacked
  132. {...this.extraFormFieldProps}
  133. />
  134. ))}
  135. </Form>
  136. </FormWrapper>
  137. );
  138. }
  139. }
  140. // Prevents errors from appearing off the modal
  141. const FormWrapper = styled('div')`
  142. position: relative;
  143. `;