integrationExternalMappingForm.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import capitalize from 'lodash/capitalize';
  4. import {SelectAsyncControlProps} from 'sentry/components/forms/controls/selectAsyncControl';
  5. import FieldFromConfig from 'sentry/components/forms/fieldFromConfig';
  6. import Form, {FormProps} from 'sentry/components/forms/form';
  7. import FormModel from 'sentry/components/forms/model';
  8. import {Field} from 'sentry/components/forms/types';
  9. import {t, tct} from 'sentry/locale';
  10. import {
  11. ExternalActorMapping,
  12. ExternalActorMappingOrSuggestion,
  13. Integration,
  14. } from 'sentry/types';
  15. import {
  16. getExternalActorEndpointDetails,
  17. isExternalActorMapping,
  18. sentryNameToOption,
  19. } from 'sentry/utils/integrationUtil';
  20. type Props = Pick<FormProps, 'onCancel' | 'onSubmitSuccess' | 'onSubmitError'> &
  21. Pick<SelectAsyncControlProps, 'defaultOptions'> & {
  22. dataEndpoint: string;
  23. getBaseFormEndpoint: (mapping?: ExternalActorMappingOrSuggestion) => string;
  24. integration: Integration;
  25. sentryNamesMapper: (v: any) => {id: string; name: string}[];
  26. type: 'user' | 'team';
  27. isInline?: boolean;
  28. mapping?: ExternalActorMappingOrSuggestion;
  29. mappingKey?: string;
  30. onResults?: (data: any, mappingKey?: string) => void;
  31. };
  32. export default class IntegrationExternalMappingForm extends Component<Props> {
  33. model = new FormModel();
  34. get initialData() {
  35. const {integration, mapping} = this.props;
  36. return {
  37. provider: integration.provider.key,
  38. integrationId: integration.id,
  39. ...mapping,
  40. };
  41. }
  42. getDefaultOptions(mapping?: ExternalActorMappingOrSuggestion) {
  43. const {defaultOptions, type} = this.props;
  44. if (typeof defaultOptions !== 'object') {
  45. return defaultOptions;
  46. }
  47. const options = [...(defaultOptions ?? [])];
  48. if (!mapping || !isExternalActorMapping(mapping) || !mapping.sentryName) {
  49. return options;
  50. }
  51. // For organizations with >100 entries, we want to make sure their
  52. // saved mapping gets populated in the results if it wouldn't have
  53. // been in the initial 100 API results, which is why we add it here
  54. const mappingId = mapping[`${type}Id`];
  55. const isMappingInOptionsAlready = options.some(
  56. ({value}) => mappingId && value === mappingId
  57. );
  58. return isMappingInOptionsAlready
  59. ? options
  60. : [{value: mappingId, label: mapping.sentryName}, ...options];
  61. }
  62. get formFields(): Field[] {
  63. const {
  64. dataEndpoint,
  65. isInline,
  66. mapping,
  67. mappingKey,
  68. onResults,
  69. sentryNamesMapper,
  70. type,
  71. } = this.props;
  72. const fields: Field[] = [
  73. {
  74. flexibleControlStateSize: true,
  75. name: `${type}Id`,
  76. type: 'select_async',
  77. required: true,
  78. label: isInline ? undefined : tct('Sentry [type]', {type: capitalize(type)}),
  79. placeholder: t('Select Sentry %s', capitalize(type)),
  80. url: dataEndpoint,
  81. defaultOptions: this.getDefaultOptions(mapping),
  82. onResults: result => {
  83. onResults?.(result, isInline ? mapping?.externalName : mappingKey);
  84. return sentryNamesMapper(result).map(sentryNameToOption);
  85. },
  86. },
  87. ];
  88. // We only add the field for externalName if it's the full (not inline) form
  89. if (!isInline) {
  90. fields.unshift({
  91. name: 'externalName',
  92. type: 'string',
  93. flexibleControlStateSize: true,
  94. required: true,
  95. label: isInline ? undefined : tct('External [type]', {type: capitalize(type)}),
  96. placeholder: type === 'user' ? t('@username') : t('@org/teamname'),
  97. });
  98. }
  99. return fields;
  100. }
  101. get extraFormFieldProps() {
  102. const {isInline} = this.props;
  103. return isInline
  104. ? {
  105. // We need to submit the entire model since it could be a new one or an update
  106. getData: () => this.model.getData(),
  107. // We need to update the model onBlur for inline forms since the model's 'onPreSubmit' hook
  108. // does NOT run when using `saveOnBlur`.
  109. onBlur: () => this.updateModel(),
  110. }
  111. : {flexibleControlStateSize: true};
  112. }
  113. // This function is necessary since the endpoint we submit to changes depending on the value selected
  114. updateModel() {
  115. const {getBaseFormEndpoint, mapping} = this.props;
  116. const updatedMapping: ExternalActorMapping = {
  117. ...mapping,
  118. ...(this.model.getData() as ExternalActorMapping),
  119. };
  120. if (updatedMapping) {
  121. const options = getExternalActorEndpointDetails(
  122. getBaseFormEndpoint(updatedMapping),
  123. updatedMapping
  124. );
  125. this.model.setFormOptions(options);
  126. }
  127. }
  128. render() {
  129. const {isInline, onCancel, onSubmitError, onSubmitSuccess} = this.props;
  130. return (
  131. <FormWrapper>
  132. <Form
  133. requireChanges
  134. model={this.model}
  135. initialData={this.initialData}
  136. onCancel={onCancel}
  137. onSubmitSuccess={onSubmitSuccess}
  138. onSubmitError={onSubmitError}
  139. saveOnBlur={isInline}
  140. allowUndo={isInline}
  141. onPreSubmit={() => this.updateModel()}
  142. >
  143. {this.formFields.map(field => (
  144. <FieldFromConfig
  145. key={field.name}
  146. field={field}
  147. inline={false}
  148. stacked
  149. {...this.extraFormFieldProps}
  150. />
  151. ))}
  152. </Form>
  153. </FormWrapper>
  154. );
  155. }
  156. }
  157. // Prevents errors from appearing off the modal
  158. const FormWrapper = styled('div')`
  159. position: relative;
  160. width: inherit;
  161. `;