issueSummary.tsx 4.5 KB

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