externalIssueActions.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import IssueSyncListElement from 'sentry/components/issueSyncListElement';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import {Group, GroupIntegration} from 'sentry/types';
  9. import {trackAnalytics} from 'sentry/utils/analytics';
  10. import {getAnalyticsDataForGroup} from 'sentry/utils/events';
  11. import useApi from 'sentry/utils/useApi';
  12. import useOrganization from 'sentry/utils/useOrganization';
  13. import IntegrationItem from 'sentry/views/settings/organizationIntegrations/integrationItem';
  14. import ExternalIssueForm from './externalIssueForm';
  15. type Props = {
  16. configurations: GroupIntegration[];
  17. group: Group;
  18. onChange: (onSuccess?: () => void, onError?: () => void) => void;
  19. };
  20. type LinkedIssues = {
  21. linked: GroupIntegration[];
  22. unlinked: GroupIntegration[];
  23. };
  24. function ExternalIssueActions({configurations, group, onChange}: Props) {
  25. const organization = useOrganization();
  26. const api = useApi();
  27. const {linked, unlinked} = configurations
  28. .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
  29. .reduce(
  30. (acc: LinkedIssues, curr) => {
  31. if (curr.externalIssues.length) {
  32. acc.linked.push(curr);
  33. } else {
  34. acc.unlinked.push(curr);
  35. }
  36. return acc;
  37. },
  38. {linked: [], unlinked: []}
  39. );
  40. const deleteIssue = (integration: GroupIntegration) => {
  41. const {externalIssues} = integration;
  42. // Currently we do not support a case where there is multiple external issues.
  43. // For example, we shouldn't have more than 1 jira ticket created for an issue for each jira configuration.
  44. const issue = externalIssues[0];
  45. const {id} = issue;
  46. const endpoint = `/organizations/${organization.slug}/issues/${group.id}/integrations/${integration.id}/?externalIssue=${id}`;
  47. api.request(endpoint, {
  48. method: 'DELETE',
  49. success: () => {
  50. onChange(
  51. () => addSuccessMessage(t('Successfully unlinked issue.')),
  52. () => addErrorMessage(t('Unable to unlink issue.'))
  53. );
  54. },
  55. error: () => {
  56. addErrorMessage(t('Unable to unlink issue.'));
  57. },
  58. });
  59. };
  60. const doOpenModal = (integration: GroupIntegration) => {
  61. trackAnalytics('issue_details.external_issue_modal_opened', {
  62. organization,
  63. ...getAnalyticsDataForGroup(group),
  64. external_issue_provider: integration.provider.key,
  65. external_issue_type: 'first_party',
  66. });
  67. openModal(
  68. deps => (
  69. <ExternalIssueForm {...deps} {...{group, onChange, integration, organization}} />
  70. ),
  71. {closeEvents: 'escape-key'}
  72. );
  73. };
  74. return (
  75. <Fragment>
  76. {linked.map(config => {
  77. const {provider, externalIssues} = config;
  78. const issue = externalIssues[0];
  79. return (
  80. <IssueSyncListElement
  81. key={issue.id}
  82. externalIssueLink={issue.url}
  83. externalIssueId={issue.id}
  84. externalIssueKey={issue.key}
  85. externalIssueDisplayName={issue.displayName}
  86. onClose={() => deleteIssue(config)}
  87. integrationType={provider.key}
  88. hoverCardHeader={t('%s Integration', provider.name)}
  89. hoverCardBody={
  90. <div>
  91. <IssueTitle>{issue.title}</IssueTitle>
  92. {issue.description && (
  93. <IssueDescription>{issue.description}</IssueDescription>
  94. )}
  95. </div>
  96. }
  97. />
  98. );
  99. })}
  100. {unlinked.length > 0 && (
  101. <IssueSyncListElement
  102. integrationType={unlinked[0].provider.key}
  103. hoverCardHeader={t('%s Integration', unlinked[0].provider.name)}
  104. hoverCardBody={
  105. <Container>
  106. {unlinked.map(config => (
  107. <Wrapper onClick={() => doOpenModal(config)} key={config.id}>
  108. <IntegrationItem integration={config} />
  109. </Wrapper>
  110. ))}
  111. </Container>
  112. }
  113. onOpen={unlinked.length === 1 ? () => doOpenModal(unlinked[0]) : undefined}
  114. />
  115. )}
  116. </Fragment>
  117. );
  118. }
  119. const IssueTitle = styled('div')`
  120. font-size: 1.1em;
  121. font-weight: 600;
  122. ${p => p.theme.overflowEllipsis};
  123. `;
  124. const IssueDescription = styled('div')`
  125. margin-top: ${space(1)};
  126. ${p => p.theme.overflowEllipsis};
  127. `;
  128. const Wrapper = styled('div')`
  129. margin-bottom: ${space(2)};
  130. cursor: pointer;
  131. `;
  132. const Container = styled('div')`
  133. & > div:last-child {
  134. margin-bottom: ${space(1)};
  135. }
  136. `;
  137. export default ExternalIssueActions;