header.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import Color from 'color';
  4. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  5. import {Breadcrumbs} from 'sentry/components/breadcrumbs';
  6. import {Flex} from 'sentry/components/container/flex';
  7. import Count from 'sentry/components/count';
  8. import ErrorLevel from 'sentry/components/events/errorLevel';
  9. import {getBadgeProperties} from 'sentry/components/group/inboxBadges/statusBadge';
  10. import UnhandledTag from 'sentry/components/group/inboxBadges/unhandledTag';
  11. import Link from 'sentry/components/links/link';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Event} from 'sentry/types/event';
  16. import type {Group} from 'sentry/types/group';
  17. import type {Project} from 'sentry/types/project';
  18. import {getMessage, getTitle} from 'sentry/utils/events';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import GroupActions from 'sentry/views/issueDetails/actions/index';
  22. import {NewIssueExperienceButton} from 'sentry/views/issueDetails/actions/newIssueExperienceButton';
  23. import {Divider} from 'sentry/views/issueDetails/divider';
  24. import GroupPriority from 'sentry/views/issueDetails/groupPriority';
  25. import {ShortIdBreadcrumb} from 'sentry/views/issueDetails/shortIdBreadcrumb';
  26. import {GroupHeaderAssigneeSelector} from 'sentry/views/issueDetails/streamline/assigneeSelector';
  27. import {AttachmentsBadge} from 'sentry/views/issueDetails/streamline/attachmentsBadge';
  28. import {ReplayBadge} from 'sentry/views/issueDetails/streamline/replayBadge';
  29. import {UserFeedbackBadge} from 'sentry/views/issueDetails/streamline/userFeedbackBadge';
  30. import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute';
  31. import {ReprocessingStatus} from 'sentry/views/issueDetails/utils';
  32. interface GroupHeaderProps {
  33. event: Event | null;
  34. group: Group;
  35. groupReprocessingStatus: ReprocessingStatus;
  36. project: Project;
  37. }
  38. export default function StreamlinedGroupHeader({
  39. event,
  40. group,
  41. groupReprocessingStatus,
  42. project,
  43. }: GroupHeaderProps) {
  44. const location = useLocation();
  45. const organization = useOrganization();
  46. const {baseUrl} = useGroupDetailsRoute();
  47. const {sort: _sort, ...query} = location.query;
  48. const {count: eventCount, userCount} = group;
  49. const {title: primaryTitle, subtitle} = getTitle(group);
  50. const secondaryTitle = getMessage(group);
  51. const isComplete = group.status === 'resolved' || group.status === 'ignored';
  52. const disableActions = [
  53. ReprocessingStatus.REPROCESSING,
  54. ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT,
  55. ].includes(groupReprocessingStatus);
  56. const statusProps = getBadgeProperties(group.status, group.substatus);
  57. return (
  58. <Fragment>
  59. <Header>
  60. <Flex justify="space-between">
  61. <Breadcrumbs
  62. crumbs={[
  63. {
  64. label: 'Issues',
  65. to: {
  66. pathname: `/organizations/${organization.slug}/issues/`,
  67. query,
  68. },
  69. },
  70. {
  71. label: (
  72. <ShortIdBreadcrumb
  73. organization={organization}
  74. project={project}
  75. group={group}
  76. />
  77. ),
  78. },
  79. ]}
  80. />
  81. <NewIssueExperienceButton />
  82. </Flex>
  83. <HeaderGrid>
  84. <Flex gap={space(0.75)} align="baseline">
  85. <PrimaryTitle
  86. title={primaryTitle}
  87. isHoverable
  88. showOnlyOnOverflow
  89. delay={1000}
  90. >
  91. {primaryTitle}
  92. </PrimaryTitle>
  93. <SecondaryTitle
  94. title={secondaryTitle}
  95. isHoverable
  96. showOnlyOnOverflow
  97. delay={1000}
  98. isDefault={!secondaryTitle}
  99. >
  100. {secondaryTitle ?? t('No error message')}
  101. </SecondaryTitle>
  102. </Flex>
  103. <StatTitle>
  104. <StatLink
  105. to={`${baseUrl}events/${location.search}`}
  106. aria-label={t('View events')}
  107. >
  108. {t('Events')}
  109. </StatLink>
  110. </StatTitle>
  111. <StatTitle>
  112. {userCount === 0 ? (
  113. t('Users')
  114. ) : (
  115. <StatLink
  116. to={`${baseUrl}tags/user/${location.search}`}
  117. aria-label={t('View affected users')}
  118. >
  119. {t('Users')}
  120. </StatLink>
  121. )}
  122. </StatTitle>
  123. <Flex gap={space(1)} align="center" justify="flex-start">
  124. <ErrorLevel level={group.level} size={'10px'} />
  125. {group.isUnhandled && <UnhandledTag />}
  126. {statusProps?.status ? (
  127. <Fragment>
  128. <Divider />
  129. <Tooltip title={statusProps?.tooltip}>
  130. <Subtext>{statusProps?.status}</Subtext>
  131. </Tooltip>
  132. </Fragment>
  133. ) : null}
  134. {subtitle && (
  135. <Fragment>
  136. <Divider />
  137. <Subtitle title={subtitle} isHoverable showOnlyOnOverflow delay={1000}>
  138. <Subtext>{subtitle}</Subtext>
  139. </Subtitle>
  140. </Fragment>
  141. )}
  142. <AttachmentsBadge group={group} />
  143. <UserFeedbackBadge group={group} project={project} />
  144. <ReplayBadge group={group} project={project} />
  145. </Flex>
  146. <StatCount value={eventCount} aria-label={t('Event count')} />
  147. <GuideAnchor target="issue_header_stats">
  148. <StatCount value={userCount} aria-label={t('User count')} />
  149. </GuideAnchor>
  150. </HeaderGrid>
  151. </Header>
  152. <ActionBar isComplete={isComplete} role="banner">
  153. <GroupActions
  154. group={group}
  155. project={project}
  156. disabled={disableActions}
  157. event={event}
  158. query={location.query}
  159. />
  160. <WorkflowActions>
  161. <Workflow>
  162. {t('Priority')}
  163. <GroupPriority group={group} />
  164. </Workflow>
  165. <GuideAnchor target="issue_sidebar_owners" position="left">
  166. <Workflow>
  167. {t('Assignee')}
  168. <GroupHeaderAssigneeSelector
  169. group={group}
  170. project={project}
  171. event={event}
  172. />
  173. </Workflow>
  174. </GuideAnchor>
  175. </WorkflowActions>
  176. </ActionBar>
  177. </Fragment>
  178. );
  179. }
  180. const Header = styled('header')`
  181. background-color: ${p => p.theme.background};
  182. padding: ${space(1)} 24px;
  183. `;
  184. const HeaderGrid = styled('div')`
  185. display: grid;
  186. grid-template-columns: minmax(150px, 1fr) auto auto;
  187. column-gap: ${space(2)};
  188. align-items: center;
  189. `;
  190. const PrimaryTitle = styled(Tooltip)`
  191. overflow-x: hidden;
  192. text-overflow: ellipsis;
  193. white-space: nowrap;
  194. font-size: 20px;
  195. font-weight: ${p => p.theme.fontWeightBold};
  196. flex-shrink: 0;
  197. `;
  198. const SecondaryTitle = styled(Tooltip)<{isDefault: boolean}>`
  199. overflow-x: hidden;
  200. text-overflow: ellipsis;
  201. white-space: nowrap;
  202. font-style: ${p => (p.isDefault ? 'italic' : 'initial')};
  203. `;
  204. const StatTitle = styled('div')`
  205. display: block;
  206. color: ${p => p.theme.subText};
  207. font-size: ${p => p.theme.fontSizeSmall};
  208. font-weight: ${p => p.theme.fontWeightBold};
  209. line-height: 1;
  210. justify-self: flex-end;
  211. `;
  212. const StatLink = styled(Link)`
  213. color: ${p => p.theme.subText};
  214. text-decoration: ${p => (p['aria-disabled'] ? 'none' : 'underline')};
  215. text-decoration-style: dotted;
  216. `;
  217. const StatCount = styled(Count)`
  218. display: block;
  219. font-size: 20px;
  220. line-height: 1;
  221. text-align: right;
  222. `;
  223. const Subtext = styled('span')`
  224. color: ${p => p.theme.subText};
  225. `;
  226. const Subtitle = styled(Tooltip)`
  227. overflow: hidden;
  228. text-overflow: ellipsis;
  229. white-space: nowrap;
  230. `;
  231. const ActionBar = styled('div')<{isComplete: boolean}>`
  232. display: flex;
  233. justify-content: space-between;
  234. gap: ${space(1)};
  235. flex-wrap: wrap;
  236. padding: ${space(1)} 24px;
  237. border-bottom: 1px solid ${p => p.theme.translucentBorder};
  238. position: relative;
  239. transition: background 0.3s ease-in-out;
  240. background: ${p => (p.isComplete ? 'transparent' : p.theme.background)};
  241. &:before {
  242. z-index: -1;
  243. position: absolute;
  244. inset: 0;
  245. content: '';
  246. background: linear-gradient(
  247. to right,
  248. ${p => p.theme.background},
  249. ${p => Color(p.theme.success).lighten(0.5).alpha(0.15).string()}
  250. );
  251. }
  252. &:after {
  253. content: '';
  254. position: absolute;
  255. top: 0;
  256. right: 0;
  257. left: 24px;
  258. bottom: unset;
  259. height: 1px;
  260. background: ${p => p.theme.translucentBorder};
  261. }
  262. `;
  263. const WorkflowActions = styled('div')`
  264. display: flex;
  265. justify-content: flex-end;
  266. column-gap: ${space(2)};
  267. flex-wrap: wrap;
  268. @media (max-width: ${p => p.theme.breakpoints.large}) {
  269. justify-content: flex-start;
  270. }
  271. `;
  272. const Workflow = styled('div')`
  273. display: flex;
  274. gap: ${space(0.5)};
  275. color: ${p => p.theme.subText};
  276. align-items: center;
  277. `;