issueSummary.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import ErrorBoundary from 'sentry/components/errorBoundary';
  4. import EventOrGroupTitle from 'sentry/components/eventOrGroupTitle';
  5. import EventMessage from 'sentry/components/events/eventMessage';
  6. import EventTitleError from 'sentry/components/eventTitleError';
  7. import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
  8. import {IconStar} from 'sentry/icons';
  9. import {tct} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {Group} from 'sentry/types/group';
  12. import type {Organization} from 'sentry/types/organization';
  13. import {getLocation, getMessage, isTombstone} from 'sentry/utils/events';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. interface EventOrGroupHeaderProps {
  16. data: Group;
  17. event_id: string;
  18. organization: Organization;
  19. }
  20. /**
  21. * Displays an event or group/issue title (i.e. in Stream)
  22. */
  23. interface IssueTitleChildrenProps {
  24. data: Group;
  25. organization: Organization;
  26. }
  27. function IssueTitleChildren(props: IssueTitleChildrenProps) {
  28. const {isBookmarked, hasSeen} = props.data;
  29. return (
  30. <Fragment>
  31. {isBookmarked && (
  32. <IconWrapper>
  33. <IconStar isSolid color="yellow400" />
  34. </IconWrapper>
  35. )}
  36. <ErrorBoundary customComponent={<EventTitleError />} mini>
  37. <StyledEventOrGroupTitle
  38. data={props.data}
  39. organization={props.organization}
  40. // hasSeen is undefined for GroupTombstone
  41. hasSeen={hasSeen === undefined ? true : hasSeen}
  42. withStackTracePreview
  43. />
  44. </ErrorBoundary>
  45. </Fragment>
  46. );
  47. }
  48. interface IssueTitleProps {
  49. data: Group;
  50. event_id: string;
  51. }
  52. function IssueTitle(props: IssueTitleProps) {
  53. const organization = useOrganization();
  54. const commonEleProps = {
  55. 'data-test-id': status === 'resolved' ? 'resolved-issue' : null,
  56. };
  57. if (isTombstone(props.data)) {
  58. return (
  59. <TitleWithoutLink {...commonEleProps}>
  60. <IssueTitleChildren data={props.data} organization={organization} />
  61. </TitleWithoutLink>
  62. );
  63. }
  64. return (
  65. <TitleWithLink
  66. {...commonEleProps}
  67. to={{
  68. pathname: `/organizations/${organization.slug}/issues/${props.data.id}/events/${props.event_id}/`,
  69. }}
  70. >
  71. <IssueTitleChildren data={props.data} organization={organization} />
  72. </TitleWithLink>
  73. );
  74. }
  75. export function IssueSummary({data, event_id}: EventOrGroupHeaderProps) {
  76. const eventLocation = getLocation(data);
  77. return (
  78. <div data-test-id="event-issue-header">
  79. <Title>
  80. <IssueTitle data={data} event_id={event_id} />
  81. </Title>
  82. {eventLocation ? <Location>{eventLocation}</Location> : null}
  83. <StyledEventMessage
  84. level={'level' in data ? data.level : undefined}
  85. message={getMessage(data)}
  86. type={data.type}
  87. levelIndicatorSize="9px"
  88. />
  89. </div>
  90. );
  91. }
  92. const Title = styled('div')`
  93. margin-bottom: ${space(0.25)};
  94. & em {
  95. font-size: ${p => p.theme.fontSizeMedium};
  96. font-style: normal;
  97. font-weight: ${p => p.theme.fontWeightNormal};
  98. color: ${p => p.theme.subText};
  99. }
  100. `;
  101. const LocationWrapper = styled('div')`
  102. overflow: hidden;
  103. max-width: 100%;
  104. text-overflow: ellipsis;
  105. white-space: nowrap;
  106. margin: 0 0 5px;
  107. direction: rtl;
  108. text-align: left;
  109. font-size: ${p => p.theme.fontSizeMedium};
  110. color: ${p => p.theme.subText};
  111. span {
  112. direction: ltr;
  113. }
  114. `;
  115. function Location(props) {
  116. const {children, ...rest} = props;
  117. return (
  118. <LocationWrapper {...rest}>
  119. {tct('in [location]', {
  120. location: <span>{children}</span>,
  121. })}
  122. </LocationWrapper>
  123. );
  124. }
  125. const StyledEventMessage = styled(EventMessage)`
  126. margin: 0 0 5px;
  127. gap: ${space(0.5)};
  128. `;
  129. const IconWrapper = styled('span')`
  130. position: relative;
  131. margin-right: 5px;
  132. `;
  133. const TitleWithLink = styled(GlobalSelectionLink)`
  134. display: inline-flex;
  135. align-items: center;
  136. `;
  137. const TitleWithoutLink = styled('span')`
  138. display: inline-flex;
  139. `;
  140. const StyledEventOrGroupTitle = styled(EventOrGroupTitle)<{
  141. hasSeen: boolean;
  142. }>`
  143. font-weight: ${p => (p.hasSeen ? 400 : 600)};
  144. `;