stacktraceLinkModal.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {ModalRenderProps} from 'sentry/actionCreators/modal';
  5. import {Client} from 'sentry/api';
  6. import Alert from 'sentry/components/alert';
  7. import Button from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import InputField from 'sentry/components/forms/inputField';
  10. import {t, tct} from 'sentry/locale';
  11. import space from 'sentry/styles/space';
  12. import {Integration, Organization, Project} from 'sentry/types';
  13. import {
  14. getIntegrationIcon,
  15. trackIntegrationAnalytics,
  16. } from 'sentry/utils/integrationUtil';
  17. import withApi from 'sentry/utils/withApi';
  18. import FeedbackAlert from 'sentry/views/settings/account/notifications/feedbackAlert';
  19. type Props = ModalRenderProps & {
  20. api: Client;
  21. filename: string;
  22. integrations: Integration[];
  23. onSubmit: () => void;
  24. organization: Organization;
  25. project: Project;
  26. };
  27. type State = {
  28. sourceCodeInput: string;
  29. };
  30. class StacktraceLinkModal extends Component<Props, State> {
  31. state: State = {
  32. sourceCodeInput: '',
  33. };
  34. onHandleChange(sourceCodeInput: string) {
  35. this.setState({
  36. sourceCodeInput,
  37. });
  38. }
  39. onManualSetup(provider: string) {
  40. trackIntegrationAnalytics('integrations.stacktrace_manual_option_clicked', {
  41. view: 'stacktrace_issue_details',
  42. setup_type: 'manual',
  43. provider,
  44. organization: this.props.organization,
  45. });
  46. }
  47. handleSubmit = async () => {
  48. const {sourceCodeInput} = this.state;
  49. const {api, closeModal, filename, onSubmit, organization, project} = this.props;
  50. trackIntegrationAnalytics('integrations.stacktrace_submit_config', {
  51. setup_type: 'automatic',
  52. view: 'stacktrace_issue_details',
  53. organization,
  54. });
  55. const parsingEndpoint = `/projects/${organization.slug}/${project.slug}/repo-path-parsing/`;
  56. try {
  57. const configData = await api.requestPromise(parsingEndpoint, {
  58. method: 'POST',
  59. data: {
  60. sourceUrl: sourceCodeInput,
  61. stackPath: filename,
  62. },
  63. });
  64. const configEndpoint = `/organizations/${organization.slug}/code-mappings/`;
  65. await api.requestPromise(configEndpoint, {
  66. method: 'POST',
  67. data: {
  68. ...configData,
  69. projectId: project.id,
  70. integrationId: configData.integrationId,
  71. },
  72. });
  73. addSuccessMessage(t('Stack trace configuration saved.'));
  74. trackIntegrationAnalytics('integrations.stacktrace_complete_setup', {
  75. setup_type: 'automatic',
  76. provider: configData.config?.provider.key,
  77. view: 'stacktrace_issue_details',
  78. organization,
  79. });
  80. closeModal();
  81. onSubmit();
  82. } catch (err) {
  83. const errors = err?.responseJSON
  84. ? Array.isArray(err?.responseJSON)
  85. ? err?.responseJSON
  86. : Object.values(err?.responseJSON)
  87. : [];
  88. const apiErrors = errors.length > 0 ? `: ${errors.join(', ')}` : '';
  89. addErrorMessage(t('Something went wrong%s', apiErrors));
  90. }
  91. };
  92. render() {
  93. const {sourceCodeInput} = this.state;
  94. const {Header, Body, filename, integrations, organization} = this.props;
  95. const baseUrl = `/settings/${organization.slug}/integrations`;
  96. return (
  97. <Fragment>
  98. <Header closeButton>{t('Link Stack Trace To Source Code')}</Header>
  99. <Body>
  100. <ModalContainer>
  101. <div>
  102. <h6>{t('Automatic Setup')}</h6>
  103. {tct(
  104. 'Enter the source code URL corresponding to stack trace filename [filename] so we can automatically set up stack trace linking for this project.',
  105. {
  106. filename: <code>{filename}</code>,
  107. }
  108. )}
  109. </div>
  110. <SourceCodeInput>
  111. <StyledInputField
  112. inline={false}
  113. flexibleControlStateSize
  114. stacked
  115. name="source-code-input"
  116. type="text"
  117. value={sourceCodeInput}
  118. onChange={val => this.onHandleChange(val)}
  119. placeholder={t(
  120. `https://github.com/helloworld/Hello-World/blob/master/${filename}`
  121. )}
  122. />
  123. <ButtonBar>
  124. <Button
  125. data-test-id="quick-setup-button"
  126. type="button"
  127. onClick={() => this.handleSubmit()}
  128. >
  129. {t('Submit')}
  130. </Button>
  131. </ButtonBar>
  132. </SourceCodeInput>
  133. <div>
  134. <h6>{t('Manual Setup')}</h6>
  135. <Alert type="warning">
  136. {t(
  137. 'We recommend this for more complicated configurations, like projects with multiple repositories.'
  138. )}
  139. </Alert>
  140. {t(
  141. "To manually configure stack trace linking, select the integration you'd like to use for mapping:"
  142. )}
  143. </div>
  144. <ManualSetup>
  145. {integrations.map(integration => (
  146. <Button
  147. key={integration.id}
  148. type="button"
  149. onClick={() => this.onManualSetup(integration.provider.key)}
  150. to={`${baseUrl}/${integration.provider.key}/${integration.id}/?tab=codeMappings&referrer=stacktrace-issue-details`}
  151. >
  152. {getIntegrationIcon(integration.provider.key)}
  153. <IntegrationName>{integration.name}</IntegrationName>
  154. </Button>
  155. ))}
  156. </ManualSetup>
  157. <StyledFeedbackAlert />
  158. </ModalContainer>
  159. </Body>
  160. </Fragment>
  161. );
  162. }
  163. }
  164. const SourceCodeInput = styled('div')`
  165. display: grid;
  166. grid-template-columns: 5fr 1fr;
  167. gap: ${space(1)};
  168. `;
  169. const ManualSetup = styled('div')`
  170. display: grid;
  171. gap: ${space(1)};
  172. justify-items: center;
  173. `;
  174. const ModalContainer = styled('div')`
  175. display: grid;
  176. gap: ${space(3)};
  177. code {
  178. word-break: break-word;
  179. }
  180. `;
  181. const StyledFeedbackAlert = styled(FeedbackAlert)`
  182. margin-bottom: 0;
  183. `;
  184. const StyledInputField = styled(InputField)`
  185. padding: 0px;
  186. `;
  187. const IntegrationName = styled('p')`
  188. padding-left: 10px;
  189. `;
  190. export default withApi(StacktraceLinkModal);