pluginActions.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import {Component, Fragment} from 'react';
  2. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  3. import {closeModal, ModalRenderProps, openModal} from 'sentry/actionCreators/modal';
  4. import {Client} from 'sentry/api';
  5. import IssueSyncListElement from 'sentry/components/issueSyncListElement';
  6. import NavTabs from 'sentry/components/navTabs';
  7. import {t, tct} from 'sentry/locale';
  8. import plugins from 'sentry/plugins';
  9. import {Group, Organization, Plugin, Project} from 'sentry/types';
  10. import {trackAnalytics} from 'sentry/utils/analytics';
  11. import {getAnalyticsDataForGroup} from 'sentry/utils/events';
  12. import withApi from 'sentry/utils/withApi';
  13. import withOrganization from 'sentry/utils/withOrganization';
  14. type PluginIssue = {
  15. issue_id: string;
  16. label: string;
  17. url: string;
  18. };
  19. export type TitledPlugin = Plugin & {
  20. // issue serializer adds more fields
  21. // TODO: should be able to use name instead of title
  22. title: string;
  23. };
  24. type Props = {
  25. api: Client;
  26. group: Group;
  27. organization: Organization;
  28. plugin: TitledPlugin;
  29. project: Project;
  30. };
  31. type State = {
  32. issue: PluginIssue | null;
  33. pluginLoading: boolean;
  34. };
  35. class PluginActions extends Component<Props, State> {
  36. state: State = {
  37. issue: null,
  38. pluginLoading: false,
  39. };
  40. componentDidMount() {
  41. this.loadPlugin(this.props.plugin);
  42. }
  43. componentDidUpdate(prevProps: Props) {
  44. if (this.props.plugin.id !== prevProps.plugin.id) {
  45. this.loadPlugin(this.props.plugin);
  46. }
  47. }
  48. deleteIssue = () => {
  49. const plugin = {
  50. ...this.props.plugin,
  51. issue: null,
  52. };
  53. // override plugin.issue so that 'create/link' Modal
  54. // doesn't think the plugin still has an issue linked
  55. const endpoint = `/issues/${this.props.group.id}/plugins/${plugin.slug}/unlink/`;
  56. this.props.api.request(endpoint, {
  57. success: () => {
  58. this.loadPlugin(plugin);
  59. addSuccessMessage(t('Successfully unlinked issue.'));
  60. },
  61. error: () => {
  62. addErrorMessage(t('Unable to unlink issue'));
  63. },
  64. });
  65. };
  66. loadPlugin = (data: any) => {
  67. this.setState(
  68. {
  69. pluginLoading: true,
  70. },
  71. () => {
  72. plugins.load(data, () => {
  73. const issue = data.issue || null;
  74. this.setState({pluginLoading: false, issue});
  75. });
  76. }
  77. );
  78. };
  79. handleModalClose = (data?: any) => {
  80. this.setState({
  81. issue:
  82. data?.id && data?.link
  83. ? {issue_id: data.id, url: data.link, label: data.label}
  84. : null,
  85. });
  86. closeModal();
  87. };
  88. openModal = () => {
  89. const {issue} = this.state;
  90. const {project, group, organization} = this.props;
  91. const plugin = {...this.props.plugin, issue};
  92. trackAnalytics('issue_details.external_issue_modal_opened', {
  93. organization,
  94. ...getAnalyticsDataForGroup(group),
  95. external_issue_provider: plugin.slug,
  96. external_issue_type: 'plugin',
  97. });
  98. openModal(
  99. deps => (
  100. <PluginActionsModal
  101. {...deps}
  102. project={project}
  103. group={group}
  104. organization={organization}
  105. plugin={plugin}
  106. onSuccess={this.handleModalClose}
  107. />
  108. ),
  109. {onClose: this.handleModalClose}
  110. );
  111. };
  112. render() {
  113. const {issue} = this.state;
  114. const plugin = {...this.props.plugin, issue};
  115. return (
  116. <IssueSyncListElement
  117. onOpen={this.openModal}
  118. externalIssueDisplayName={issue ? issue.label : null}
  119. externalIssueId={issue ? issue.issue_id : null}
  120. externalIssueLink={issue ? issue.url : null}
  121. onClose={this.deleteIssue}
  122. integrationType={plugin.id}
  123. />
  124. );
  125. }
  126. }
  127. type ModalProps = ModalRenderProps & {
  128. group: Group;
  129. onSuccess: (data: any) => void;
  130. organization: Organization;
  131. plugin: TitledPlugin & {issue: PluginIssue | null};
  132. project: Project;
  133. };
  134. type ModalState = {
  135. actionType: 'create' | 'link' | null;
  136. };
  137. class PluginActionsModal extends Component<ModalProps, ModalState> {
  138. state: ModalState = {
  139. actionType: 'create',
  140. };
  141. render() {
  142. const {Header, Body, group, project, organization, plugin, onSuccess} = this.props;
  143. const {actionType} = this.state;
  144. return (
  145. <Fragment>
  146. <Header closeButton>
  147. <h4>{tct('[name] Issue', {name: plugin.name || plugin.title})}</h4>
  148. </Header>
  149. <NavTabs underlined>
  150. <li className={actionType === 'create' ? 'active' : ''}>
  151. <a onClick={() => this.setState({actionType: 'create'})}>{t('Create')}</a>
  152. </li>
  153. <li className={actionType === 'link' ? 'active' : ''}>
  154. <a onClick={() => this.setState({actionType: 'link'})}>{t('Link')}</a>
  155. </li>
  156. </NavTabs>
  157. {actionType && (
  158. // need the key here so React will re-render
  159. // with new action prop
  160. <Body key={actionType}>
  161. {plugins.get(plugin).renderGroupActions({
  162. plugin,
  163. group,
  164. project,
  165. organization,
  166. actionType,
  167. onSuccess,
  168. })}
  169. </Body>
  170. )}
  171. </Fragment>
  172. );
  173. }
  174. }
  175. export {PluginActions};
  176. export default withApi(withOrganization(PluginActions));