repositoryProjectPathConfigForm.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import {useRef} from 'react';
  2. import pick from 'lodash/pick';
  3. import FieldFromConfig from 'sentry/components/forms/fieldFromConfig';
  4. import type {FormProps} from 'sentry/components/forms/form';
  5. import Form from 'sentry/components/forms/form';
  6. import FormModel from 'sentry/components/forms/model';
  7. import type {Field} from 'sentry/components/forms/types';
  8. import {t} from 'sentry/locale';
  9. import type {
  10. Integration,
  11. IntegrationRepository,
  12. Organization,
  13. Project,
  14. Repository,
  15. RepositoryProjectPathConfig,
  16. } from 'sentry/types';
  17. import {trackAnalytics} from 'sentry/utils/analytics';
  18. import {sentryNameToOption} from 'sentry/utils/integrationUtil';
  19. import useApi from 'sentry/utils/useApi';
  20. type Props = {
  21. integration: Integration;
  22. onCancel: FormProps['onCancel'];
  23. onSubmitSuccess: FormProps['onSubmitSuccess'];
  24. organization: Organization;
  25. projects: Project[];
  26. repos: Repository[];
  27. existingConfig?: RepositoryProjectPathConfig;
  28. };
  29. function RepositoryProjectPathConfigForm({
  30. existingConfig,
  31. integration,
  32. onCancel,
  33. onSubmitSuccess,
  34. organization,
  35. projects,
  36. repos,
  37. }: Props) {
  38. const api = useApi();
  39. const formRef = useRef(new FormModel());
  40. const repoChoices = repos.map(({name, id}) => ({value: id, label: name}));
  41. /**
  42. * Automatically switch to the default branch for the repo
  43. */
  44. function handleRepoChange(id: string) {
  45. const repo = repos.find(r => r.id === id);
  46. if (!repo) {
  47. return;
  48. }
  49. // Use the integration repo search to get the default branch
  50. api
  51. .requestPromise(
  52. `/organizations/${organization.slug}/integrations/${integration.id}/repos/`,
  53. {query: {search: repo.name}}
  54. )
  55. .then((data: {repos: IntegrationRepository[]}) => {
  56. const {defaultBranch} = data.repos.find(r => r.identifier === repo.name) ?? {};
  57. const isCurrentRepo = formRef.current.getValue('repositoryId') === repo.id;
  58. if (defaultBranch && isCurrentRepo) {
  59. formRef.current.setValue('defaultBranch', defaultBranch);
  60. }
  61. });
  62. }
  63. const formFields: Field[] = [
  64. {
  65. name: 'projectId',
  66. type: 'sentry_project_selector',
  67. required: true,
  68. label: t('Project'),
  69. projects,
  70. },
  71. {
  72. name: 'repositoryId',
  73. type: 'select_async',
  74. required: true,
  75. label: t('Repo'),
  76. placeholder: t('Choose repo'),
  77. url: `/organizations/${organization.slug}/repos/`,
  78. defaultOptions: repoChoices,
  79. onResults: results => results.map(sentryNameToOption),
  80. onChange: handleRepoChange,
  81. },
  82. {
  83. name: 'defaultBranch',
  84. type: 'string',
  85. required: true,
  86. label: t('Branch'),
  87. placeholder: t('Type your branch'),
  88. showHelpInTooltip: true,
  89. help: t(
  90. 'If an event does not have a release tied to a commit, we will use this branch when linking to your source code.'
  91. ),
  92. },
  93. {
  94. name: 'stackRoot',
  95. type: 'string',
  96. required: false,
  97. label: t('Stack Trace Root'),
  98. placeholder: t('Type root path of your stack traces'),
  99. showHelpInTooltip: true,
  100. help: t(
  101. 'Any stack trace starting with this path will be mapped with this rule. An empty string will match all paths.'
  102. ),
  103. },
  104. {
  105. name: 'sourceRoot',
  106. type: 'string',
  107. required: false,
  108. label: t('Source Code Root'),
  109. placeholder: t('Type root path of your source code, e.g. `src/`.'),
  110. showHelpInTooltip: true,
  111. help: t(
  112. 'When a rule matches, the stack trace root is replaced with this path to get the path in your repository. Leaving this empty means replacing the stack trace root with an empty string.'
  113. ),
  114. },
  115. ];
  116. function handlePreSubmit() {
  117. trackAnalytics('integrations.stacktrace_submit_config', {
  118. setup_type: 'manual',
  119. view: 'integration_configuration_detail',
  120. provider: integration.provider.key,
  121. organization,
  122. });
  123. }
  124. const initialData = {
  125. defaultBranch: 'master',
  126. stackRoot: '',
  127. sourceRoot: '',
  128. repositoryId: existingConfig?.repoId,
  129. integrationId: integration.id,
  130. ...pick(existingConfig, ['projectId', 'defaultBranch', 'stackRoot', 'sourceRoot']),
  131. };
  132. // endpoint changes if we are making a new row or updating an existing one
  133. const baseEndpoint = `/organizations/${organization.slug}/code-mappings/`;
  134. const endpoint = existingConfig ? `${baseEndpoint}${existingConfig.id}/` : baseEndpoint;
  135. const apiMethod = existingConfig ? 'PUT' : 'POST';
  136. return (
  137. <Form
  138. onSubmitSuccess={onSubmitSuccess}
  139. onPreSubmit={handlePreSubmit}
  140. initialData={initialData}
  141. apiEndpoint={endpoint}
  142. apiMethod={apiMethod}
  143. model={formRef.current}
  144. onCancel={onCancel}
  145. >
  146. {formFields.map(field => (
  147. <FieldFromConfig
  148. key={field.name}
  149. field={field}
  150. inline={false}
  151. stacked
  152. flexibleControlStateSize
  153. />
  154. ))}
  155. </Form>
  156. );
  157. }
  158. export default RepositoryProjectPathConfigForm;