relatedIssues.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import pick from 'lodash/pick';
  5. import Button from 'app/components/button';
  6. import {SectionHeading} from 'app/components/charts/styles';
  7. import EmptyStateWarning from 'app/components/emptyStateWarning';
  8. import GroupList from 'app/components/issues/groupList';
  9. import {Panel, PanelBody} from 'app/components/panels';
  10. import {DEFAULT_RELATIVE_PERIODS} from 'app/constants';
  11. import {URL_PARAM} from 'app/constants/globalSelectionHeader';
  12. import {t, tct} from 'app/locale';
  13. import space from 'app/styles/space';
  14. import {OrganizationSummary} from 'app/types';
  15. import {trackAnalyticsEvent} from 'app/utils/analytics';
  16. import {TRACING_FIELDS} from 'app/utils/discover/fields';
  17. import {decodeScalar} from 'app/utils/queryString';
  18. import {stringifyQueryObject, tokenizeSearch} from 'app/utils/tokenizeSearch';
  19. type Props = {
  20. organization: OrganizationSummary;
  21. location: Location;
  22. transaction: string;
  23. statsPeriod?: string;
  24. start?: string;
  25. end?: string;
  26. };
  27. class RelatedIssues extends Component<Props> {
  28. getIssuesEndpoint() {
  29. const {transaction, organization, start, end, statsPeriod, location} = this.props;
  30. const queryParams = {
  31. start,
  32. end,
  33. statsPeriod,
  34. limit: 5,
  35. sort: 'new',
  36. ...pick(location.query, [...Object.values(URL_PARAM), 'cursor']),
  37. };
  38. const currentFilter = tokenizeSearch(decodeScalar(location.query.query, ''));
  39. currentFilter.getTagKeys().forEach(tagKey => {
  40. // Remove aggregates and transaction event fields
  41. if (
  42. // aggregates
  43. tagKey.match(/\w+\(.*\)/) ||
  44. // transaction event fields
  45. TRACING_FIELDS.includes(tagKey) ||
  46. // event type can be "transaction" but we're searching for issues
  47. tagKey === 'event.type'
  48. ) {
  49. currentFilter.removeTag(tagKey);
  50. }
  51. });
  52. currentFilter.addQuery('is:unresolved').setTagValues('transaction', [transaction]);
  53. // Filter out key_transaction from being passed to issues as it will cause an error.
  54. currentFilter.removeTag('key_transaction');
  55. currentFilter.removeTag('team_key_transaction');
  56. return {
  57. path: `/organizations/${organization.slug}/issues/`,
  58. queryParams: {
  59. ...queryParams,
  60. query: stringifyQueryObject(currentFilter),
  61. },
  62. };
  63. }
  64. handleOpenClick = () => {
  65. const {organization} = this.props;
  66. trackAnalyticsEvent({
  67. eventKey: 'performance_views.summary.open_issues',
  68. eventName: 'Performance Views: Open issues from transaction summary',
  69. organization_id: parseInt(organization.id, 10),
  70. });
  71. };
  72. renderEmptyMessage = () => {
  73. const {statsPeriod} = this.props;
  74. const selectedTimePeriod = statsPeriod && DEFAULT_RELATIVE_PERIODS[statsPeriod];
  75. const displayedPeriod = selectedTimePeriod
  76. ? selectedTimePeriod.toLowerCase()
  77. : t('given timeframe');
  78. return (
  79. <Panel>
  80. <PanelBody>
  81. <EmptyStateWarning>
  82. <p>
  83. {tct('No new issues for this transaction for the [timePeriod].', {
  84. timePeriod: displayedPeriod,
  85. })}
  86. </p>
  87. </EmptyStateWarning>
  88. </PanelBody>
  89. </Panel>
  90. );
  91. };
  92. render() {
  93. const {organization} = this.props;
  94. const {path, queryParams} = this.getIssuesEndpoint();
  95. const issueSearch = {
  96. pathname: `/organizations/${organization.slug}/issues/`,
  97. query: queryParams,
  98. };
  99. return (
  100. <Fragment>
  101. <ControlsWrapper>
  102. <SectionHeading>{t('Related Issues')}</SectionHeading>
  103. <Button
  104. data-test-id="issues-open"
  105. size="small"
  106. to={issueSearch}
  107. onClick={this.handleOpenClick}
  108. >
  109. {t('Open in Issues')}
  110. </Button>
  111. </ControlsWrapper>
  112. <TableWrapper>
  113. <GroupList
  114. orgId={organization.slug}
  115. endpointPath={path}
  116. queryParams={queryParams}
  117. query=""
  118. canSelectGroups={false}
  119. renderEmptyMessage={this.renderEmptyMessage}
  120. withChart={false}
  121. withPagination={false}
  122. />
  123. </TableWrapper>
  124. </Fragment>
  125. );
  126. }
  127. }
  128. const ControlsWrapper = styled('div')`
  129. display: flex;
  130. align-items: center;
  131. justify-content: space-between;
  132. margin-bottom: ${space(1)};
  133. `;
  134. const TableWrapper = styled('div')`
  135. margin-bottom: ${space(4)};
  136. ${Panel} {
  137. /* smaller space between table and pagination */
  138. margin-bottom: -${space(1)};
  139. }
  140. `;
  141. export default RelatedIssues;