integrationExternalMappingForm.tsx 5.5 KB

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