issues.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import ActorAvatar from 'sentry/components/avatar/actorAvatar';
  4. import Count from 'sentry/components/count';
  5. import EventOrGroupExtraDetails from 'sentry/components/eventOrGroupExtraDetails';
  6. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  7. import Panel from 'sentry/components/panels/panel';
  8. import PanelHeader from 'sentry/components/panels/panelHeader';
  9. import PanelItem from 'sentry/components/panels/panelItem';
  10. import {IconWrapper} from 'sentry/components/sidebarSection';
  11. import GroupChart from 'sentry/components/stream/groupChart';
  12. import {IconUser} from 'sentry/icons';
  13. import {t, tct, tn} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Group} from 'sentry/types/group';
  16. import {useApiQuery} from 'sentry/utils/queryClient';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import usePageFilters from 'sentry/utils/usePageFilters';
  19. import {IssueSummary} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/issues/issueSummary';
  20. const TABLE_WIDTH_BREAKPOINTS = {
  21. FIRST: 800,
  22. SECOND: 600,
  23. THIRD: 500,
  24. FOURTH: 400,
  25. };
  26. function Issue({data}: {data: Group}) {
  27. const organization = useOrganization();
  28. return (
  29. <StyledPanelItem as="tr">
  30. <IssueSummaryWrapper>
  31. <IssueSummary data={data} organization={organization} />
  32. <EventOrGroupExtraDetails data={data} />
  33. </IssueSummaryWrapper>
  34. <ChartWrapper>
  35. <GroupChart
  36. stats={data.filtered ? data.filtered.stats?.['24h'] : data.stats?.['24h']}
  37. secondaryStats={data.filtered ? data.stats?.['24h'] : []}
  38. showSecondaryPoints
  39. showMarkLine
  40. />
  41. </ChartWrapper>
  42. <EventsWrapper>
  43. <PrimaryCount value={data.filtered ? data.filtered.count : data.count} />
  44. </EventsWrapper>
  45. <UserCountWrapper>
  46. <PrimaryCount value={data.filtered ? data.filtered.userCount : data.userCount} />
  47. </UserCountWrapper>
  48. <AssineeWrapper>
  49. {data.assignedTo ? (
  50. <ActorAvatar actor={data.assignedTo} hasTooltip size={24} />
  51. ) : (
  52. <StyledIconWrapper>
  53. <IconUser size="md" />
  54. </StyledIconWrapper>
  55. )}
  56. </AssineeWrapper>
  57. </StyledPanelItem>
  58. );
  59. }
  60. function IssueListHeader({issues}: {issues?: Group[]}) {
  61. return (
  62. <StyledPanelHeader as="tr">
  63. <IssueHeading>
  64. {tct(`[count] [text]`, {
  65. count: issues?.length ?? 0,
  66. text: tn('Related Issue', 'Related Issues', issues?.length ?? 0),
  67. })}
  68. </IssueHeading>
  69. <GraphHeading>{t('Graph')}</GraphHeading>
  70. <EventsHeading>{t('Events')}</EventsHeading>
  71. <UsersHeading>{t('Users')}</UsersHeading>
  72. <AssigneeHeading>{t('Assignee')}</AssigneeHeading>
  73. </StyledPanelHeader>
  74. );
  75. }
  76. function useInsightIssues(
  77. issueTypes: string[],
  78. message?: string
  79. ): {isLoading: boolean; issues?: Group[]} {
  80. const organization = useOrganization();
  81. const {selection} = usePageFilters();
  82. let query = `issue.type:[${issueTypes.join(',')}]`;
  83. // note: backend supports a maximum number of characters for message (seems to vary).
  84. // so, we query the first 200 characters of `message`, then filter for exact `message`
  85. // matches in application code
  86. query += ` message:"${message?.slice(0, 200).replaceAll('"', '\\"')}"`;
  87. const {isPending, data: maybeMatchingIssues} = useApiQuery<Group[]>(
  88. [
  89. `/organizations/${organization.slug}/issues/`,
  90. {
  91. query: {
  92. expand: ['inbox', 'owners'],
  93. query,
  94. shortIdLookup: 1,
  95. // hack: set an arbitrary large upper limit so that the api response likely contains the exact message,
  96. // even though we only search for the first 200 characters of the message
  97. limit: 100,
  98. project: selection.projects,
  99. environment: selection.environments,
  100. ...normalizeDateTimeParams(selection.datetime),
  101. },
  102. },
  103. ],
  104. {
  105. staleTime: 2 * 60 * 1000,
  106. enabled: !!message,
  107. }
  108. );
  109. if (!message) {
  110. return {isLoading: false, issues: []};
  111. }
  112. // the api response contains issues that match the first 200 characters of the message. now,
  113. // filter by the issues that match the exact message the user is searching for
  114. const issues = maybeMatchingIssues?.filter(issue => issue.metadata.value === message);
  115. return {isLoading: isPending, issues};
  116. }
  117. export default function InsightIssuesList({
  118. issueTypes,
  119. message,
  120. }: {
  121. issueTypes: string[];
  122. message?: string;
  123. }) {
  124. const {isLoading, issues} = useInsightIssues(issueTypes, message);
  125. if (isLoading || issues?.length === 0) {
  126. return <Fragment />;
  127. }
  128. return (
  129. <StyledPanel>
  130. <IssueListHeader issues={issues} />
  131. {issues?.map(issue => <Issue data={issue} key={issue.id} />)}
  132. </StyledPanel>
  133. );
  134. }
  135. const Heading = styled('th')`
  136. display: flex;
  137. align-self: center;
  138. margin: 0 ${space(2)};
  139. width: 60px;
  140. color: ${p => p.theme.subText};
  141. `;
  142. const IssueHeading = styled(Heading)`
  143. flex: 1;
  144. width: 66.66%;
  145. margin: 0;
  146. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  147. width: 50%;
  148. }
  149. `;
  150. const GraphHeading = styled(Heading)`
  151. width: 160px;
  152. display: flex;
  153. justify-content: center;
  154. @container (width < ${TABLE_WIDTH_BREAKPOINTS.FIRST}px) {
  155. display: none;
  156. }
  157. `;
  158. const EventsHeading = styled(Heading)`
  159. @container (width < ${TABLE_WIDTH_BREAKPOINTS.SECOND}px) {
  160. display: none;
  161. }
  162. `;
  163. const UsersHeading = styled(Heading)`
  164. display: flex;
  165. justify-content: center;
  166. @container (width < ${TABLE_WIDTH_BREAKPOINTS.THIRD}px) {
  167. display: none;
  168. }
  169. `;
  170. const AssigneeHeading = styled(Heading)`
  171. @container (width < ${TABLE_WIDTH_BREAKPOINTS.FOURTH}px) {
  172. display: none;
  173. }
  174. `;
  175. const StyledPanel = styled(Panel)`
  176. container-type: inline-size;
  177. `;
  178. const StyledPanelHeader = styled(PanelHeader)`
  179. padding-top: ${space(1)};
  180. padding-bottom: ${space(1)};
  181. `;
  182. const StyledIconWrapper = styled(IconWrapper)`
  183. margin: 0;
  184. `;
  185. const IssueSummaryWrapper = styled('td')`
  186. overflow: hidden;
  187. flex: 1;
  188. width: 66.66%;
  189. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  190. width: 50%;
  191. }
  192. `;
  193. const ColumnWrapper = styled('td')`
  194. display: flex;
  195. justify-content: flex-end;
  196. align-self: center;
  197. width: 60px;
  198. margin: 0 ${space(2)};
  199. `;
  200. const EventsWrapper = styled(ColumnWrapper)`
  201. @container (width < ${TABLE_WIDTH_BREAKPOINTS.SECOND}px) {
  202. display: none;
  203. }
  204. `;
  205. const UserCountWrapper = styled(ColumnWrapper)`
  206. @container (width < ${TABLE_WIDTH_BREAKPOINTS.THIRD}px) {
  207. display: none;
  208. }
  209. `;
  210. const AssineeWrapper = styled(ColumnWrapper)`
  211. @container (width < ${TABLE_WIDTH_BREAKPOINTS.FOURTH}px) {
  212. display: none;
  213. }
  214. `;
  215. const ChartWrapper = styled('td')`
  216. width: 200px;
  217. align-self: center;
  218. @container (width < ${TABLE_WIDTH_BREAKPOINTS.FIRST}px) {
  219. display: none;
  220. }
  221. `;
  222. const PrimaryCount = styled(Count)`
  223. font-size: ${p => p.theme.fontSizeLarge};
  224. font-variant-numeric: tabular-nums;
  225. `;
  226. const StyledPanelItem = styled(PanelItem)`
  227. padding-top: ${space(1)};
  228. padding-bottom: ${space(1)};
  229. height: 84px;
  230. `;