issueSummary.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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. organization: Organization;
  18. event_id?: string;
  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. // hasSeen is undefined for GroupTombstone
  40. hasSeen={hasSeen === undefined ? true : hasSeen}
  41. withStackTracePreview
  42. />
  43. </ErrorBoundary>
  44. </Fragment>
  45. );
  46. }
  47. interface IssueTitleProps {
  48. data: Group;
  49. event_id?: string;
  50. }
  51. function IssueTitle(props: IssueTitleProps) {
  52. const organization = useOrganization();
  53. const commonEleProps = {
  54. 'data-test-id': status === 'resolved' ? 'resolved-issue' : null,
  55. };
  56. if (isTombstone(props.data)) {
  57. return (
  58. <TitleWithoutLink {...commonEleProps}>
  59. <IssueTitleChildren data={props.data} organization={organization} />
  60. </TitleWithoutLink>
  61. );
  62. }
  63. return (
  64. <TitleWithLink
  65. {...commonEleProps}
  66. to={{
  67. pathname: props.event_id
  68. ? `/organizations/${organization.slug}/issues/${props.data.id}/events/${props.event_id}/`
  69. : `/organizations/${organization.slug}/issues/${props.data.id}/`,
  70. }}
  71. >
  72. <IssueTitleChildren data={props.data} organization={organization} />
  73. </TitleWithLink>
  74. );
  75. }
  76. export function IssueSummary({data, event_id}: EventOrGroupHeaderProps) {
  77. const eventLocation = getLocation(data);
  78. const organization = useOrganization();
  79. const hasNewLayout = organization.features.includes('issue-stream-table-layout');
  80. return (
  81. <div data-test-id="event-issue-header">
  82. <Title>
  83. <IssueTitle data={data} event_id={event_id} />
  84. </Title>
  85. {eventLocation ? <Location>{eventLocation}</Location> : null}
  86. {!hasNewLayout ? (
  87. <StyledEventMessage
  88. data={data}
  89. level={'level' in data ? data.level : undefined}
  90. message={getMessage(data)}
  91. type={data.type}
  92. levelIndicatorSize="9px"
  93. />
  94. ) : null}
  95. </div>
  96. );
  97. }
  98. const Title = styled('div')`
  99. margin-bottom: ${space(0.25)};
  100. & em {
  101. font-size: ${p => p.theme.fontSizeMedium};
  102. font-style: normal;
  103. font-weight: ${p => p.theme.fontWeightNormal};
  104. color: ${p => p.theme.subText};
  105. }
  106. `;
  107. const LocationWrapper = styled('div')`
  108. overflow: hidden;
  109. max-width: 100%;
  110. text-overflow: ellipsis;
  111. white-space: nowrap;
  112. margin: 0 0 5px;
  113. direction: rtl;
  114. text-align: left;
  115. font-size: ${p => p.theme.fontSizeMedium};
  116. color: ${p => p.theme.subText};
  117. span {
  118. direction: ltr;
  119. }
  120. `;
  121. function Location(props) {
  122. const {children, ...rest} = props;
  123. return (
  124. <LocationWrapper {...rest}>
  125. {tct('in [location]', {
  126. location: <span>{children}</span>,
  127. })}
  128. </LocationWrapper>
  129. );
  130. }
  131. const StyledEventMessage = styled(EventMessage)`
  132. margin: 0 0 5px;
  133. gap: ${space(0.5)};
  134. `;
  135. const IconWrapper = styled('span')`
  136. position: relative;
  137. margin-right: 5px;
  138. `;
  139. const TitleWithLink = styled(GlobalSelectionLink)`
  140. align-items: center;
  141. ${p => p.theme.overflowEllipsis}
  142. `;
  143. const TitleWithoutLink = styled('span')`
  144. ${p => p.theme.overflowEllipsis}
  145. `;
  146. const StyledEventOrGroupTitle = styled(EventOrGroupTitle)<{
  147. hasSeen: boolean;
  148. }>`
  149. font-weight: ${p => (p.hasSeen ? 400 : 600)};
  150. `;