integrationExternalMappingForm.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import capitalize from 'lodash/capitalize';
  4. import {FieldFromConfig} from 'sentry/components/forms';
  5. import {SelectAsyncControlProps} from 'sentry/components/forms/controls/selectAsyncControl';
  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. name: `${type}Id`,
  75. type: 'select_async',
  76. required: true,
  77. label: isInline ? undefined : tct('Sentry [type]', {type: capitalize(type)}),
  78. placeholder: t('Select Sentry %s', capitalize(type)),
  79. url: dataEndpoint,
  80. defaultOptions: this.getDefaultOptions(mapping),
  81. onResults: result => {
  82. onResults?.(result, isInline ? mapping?.externalName : mappingKey);
  83. return sentryNamesMapper(result).map(sentryNameToOption);
  84. },
  85. },
  86. ];
  87. // We only add the field for externalName if it's the full (not inline) form
  88. if (!isInline) {
  89. fields.unshift({
  90. name: 'externalName',
  91. type: 'string',
  92. required: true,
  93. label: isInline ? undefined : tct('External [type]', {type: capitalize(type)}),
  94. placeholder: type === 'user' ? t('@username') : t('@org/teamname'),
  95. });
  96. }
  97. return fields;
  98. }
  99. get extraFormFieldProps() {
  100. const {isInline} = this.props;
  101. return isInline
  102. ? {
  103. // We need to submit the entire model since it could be a new one or an update
  104. getData: () => this.model.getData(),
  105. // We need to update the model onBlur for inline forms since the model's 'onPreSubmit' hook
  106. // does NOT run when using `saveOnBlur`.
  107. onBlur: () => this.updateModel(),
  108. }
  109. : {flexibleControlStateSize: true};
  110. }
  111. // This function is necessary since the endpoint we submit to changes depending on the value selected
  112. updateModel() {
  113. const {getBaseFormEndpoint, mapping} = this.props;
  114. const updatedMapping: ExternalActorMapping = {
  115. ...mapping,
  116. ...(this.model.getData() as ExternalActorMapping),
  117. };
  118. if (updatedMapping) {
  119. const options = getExternalActorEndpointDetails(
  120. getBaseFormEndpoint(updatedMapping),
  121. updatedMapping
  122. );
  123. this.model.setFormOptions(options);
  124. }
  125. }
  126. render() {
  127. const {isInline, onCancel, onSubmitError, onSubmitSuccess} = this.props;
  128. return (
  129. <FormWrapper>
  130. <Form
  131. requireChanges
  132. model={this.model}
  133. initialData={this.initialData}
  134. onCancel={onCancel}
  135. onSubmitSuccess={onSubmitSuccess}
  136. onSubmitError={onSubmitError}
  137. saveOnBlur={isInline}
  138. allowUndo={isInline}
  139. onPreSubmit={() => this.updateModel()}
  140. >
  141. {this.formFields.map(field => (
  142. <FieldFromConfig
  143. key={field.name}
  144. field={field}
  145. inline={false}
  146. stacked
  147. {...this.extraFormFieldProps}
  148. />
  149. ))}
  150. </Form>
  151. </FormWrapper>
  152. );
  153. }
  154. }
  155. // Prevents errors from appearing off the modal
  156. const FormWrapper = styled('div')`
  157. position: relative;
  158. `;