projectIssues.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import pick from 'lodash/pick';
  5. import {Client} from 'sentry/api';
  6. import Button from 'sentry/components/button';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import {SectionHeading} from 'sentry/components/charts/styles';
  9. import DiscoverButton from 'sentry/components/discoverButton';
  10. import GroupList from 'sentry/components/issues/groupList';
  11. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  12. import Pagination from 'sentry/components/pagination';
  13. import {Panel, PanelBody} from 'sentry/components/panels';
  14. import {DEFAULT_RELATIVE_PERIODS, DEFAULT_STATS_PERIOD} from 'sentry/constants';
  15. import {URL_PARAM} from 'sentry/constants/pageFilters';
  16. import {t, tct} from 'sentry/locale';
  17. import space from 'sentry/styles/space';
  18. import {Organization} from 'sentry/types';
  19. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  20. import {decodeScalar} from 'sentry/utils/queryString';
  21. import NoGroupsHandler from '../issueList/noGroupsHandler';
  22. type Props = {
  23. api: Client;
  24. location: Location;
  25. organization: Organization;
  26. projectId: number;
  27. query?: string;
  28. };
  29. function ProjectIssues({organization, location, projectId, query, api}: Props) {
  30. const [pageLinks, setPageLinks] = useState<string | undefined>();
  31. const [onCursor, setOnCursor] = useState<(() => void) | undefined>();
  32. function handleOpenInIssuesClick() {
  33. trackAnalyticsEvent({
  34. eventKey: 'project_detail.open_issues',
  35. eventName: 'Project Detail: Open issues from project detail',
  36. organization_id: parseInt(organization.id, 10),
  37. });
  38. }
  39. function handleOpenInDiscoverClick() {
  40. trackAnalyticsEvent({
  41. eventKey: 'project_detail.open_discover',
  42. eventName: 'Project Detail: Open discover from project detail',
  43. organization_id: parseInt(organization.id, 10),
  44. });
  45. }
  46. function handleFetchSuccess(groupListState, cursorHandler) {
  47. setPageLinks(groupListState.pageLinks);
  48. setOnCursor(() => cursorHandler);
  49. }
  50. function getDiscoverUrl() {
  51. return {
  52. pathname: `/organizations/${organization.slug}/discover/results/`,
  53. query: {
  54. name: t('Frequent Unhandled Issues'),
  55. field: ['issue', 'title', 'count()', 'count_unique(user)', 'project'],
  56. sort: ['-count'],
  57. query: ['event.type:error error.unhandled:true', query].join(' ').trim(),
  58. display: 'top5',
  59. ...normalizeDateTimeParams(pick(location.query, [...Object.values(URL_PARAM)])),
  60. },
  61. };
  62. }
  63. const endpointPath = `/organizations/${organization.slug}/issues/`;
  64. const issueQuery = ['is:unresolved error.unhandled:true ', query].join(' ').trim();
  65. const queryParams = {
  66. limit: 5,
  67. ...normalizeDateTimeParams(
  68. pick(location.query, [...Object.values(URL_PARAM), 'cursor'])
  69. ),
  70. query: issueQuery,
  71. sort: 'freq',
  72. };
  73. const issueSearch = {
  74. pathname: endpointPath,
  75. query: queryParams,
  76. };
  77. function renderEmptyMessage() {
  78. const selectedTimePeriod = location.query.start
  79. ? null
  80. : DEFAULT_RELATIVE_PERIODS[
  81. decodeScalar(location.query.statsPeriod, DEFAULT_STATS_PERIOD)
  82. ];
  83. const displayedPeriod = selectedTimePeriod
  84. ? selectedTimePeriod.toLowerCase()
  85. : t('given timeframe');
  86. return (
  87. <Panel>
  88. <PanelBody>
  89. <NoGroupsHandler
  90. api={api}
  91. organization={organization}
  92. query={issueQuery}
  93. selectedProjectIds={[projectId]}
  94. groupIds={[]}
  95. emptyMessage={tct('No unhandled issues for the [timePeriod].', {
  96. timePeriod: displayedPeriod,
  97. })}
  98. />
  99. </PanelBody>
  100. </Panel>
  101. );
  102. }
  103. return (
  104. <Fragment>
  105. <ControlsWrapper>
  106. <SectionHeading>{t('Frequent Unhandled Issues')}</SectionHeading>
  107. <ButtonBar gap={1}>
  108. <Button
  109. data-test-id="issues-open"
  110. size="xsmall"
  111. to={issueSearch}
  112. onClick={handleOpenInIssuesClick}
  113. >
  114. {t('Open in Issues')}
  115. </Button>
  116. <DiscoverButton
  117. onClick={handleOpenInDiscoverClick}
  118. to={getDiscoverUrl()}
  119. size="xsmall"
  120. >
  121. {t('Open in Discover')}
  122. </DiscoverButton>
  123. <StyledPagination pageLinks={pageLinks} onCursor={onCursor} size="xsmall" />
  124. </ButtonBar>
  125. </ControlsWrapper>
  126. <GroupList
  127. orgId={organization.slug}
  128. endpointPath={endpointPath}
  129. queryParams={queryParams}
  130. query=""
  131. canSelectGroups={false}
  132. renderEmptyMessage={renderEmptyMessage}
  133. withChart={false}
  134. withPagination={false}
  135. onFetchSuccess={handleFetchSuccess}
  136. />
  137. </Fragment>
  138. );
  139. }
  140. const ControlsWrapper = styled('div')`
  141. display: flex;
  142. align-items: center;
  143. justify-content: space-between;
  144. margin-bottom: ${space(1)};
  145. flex-wrap: wrap;
  146. @media (max-width: ${p => p.theme.breakpoints[0]}) {
  147. display: block;
  148. }
  149. `;
  150. const StyledPagination = styled(Pagination)`
  151. margin: 0;
  152. `;
  153. export default ProjectIssues;