header.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import Color from 'color';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  6. import {Breadcrumbs} from 'sentry/components/breadcrumbs';
  7. import {Button, LinkButton} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import {Flex} from 'sentry/components/container/flex';
  10. import Count from 'sentry/components/count';
  11. import ErrorLevel from 'sentry/components/events/errorLevel';
  12. import {getBadgeProperties} from 'sentry/components/group/inboxBadges/statusBadge';
  13. import UnhandledTag from 'sentry/components/group/inboxBadges/unhandledTag';
  14. import ExternalLink from 'sentry/components/links/externalLink';
  15. import Link from 'sentry/components/links/link';
  16. import {Tooltip} from 'sentry/components/tooltip';
  17. import {IconGlobe, IconInfo} from 'sentry/icons';
  18. import {t, tct} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import type {Event} from 'sentry/types/event';
  21. import type {Group} from 'sentry/types/group';
  22. import type {Project} from 'sentry/types/project';
  23. import {trackAnalytics} from 'sentry/utils/analytics';
  24. import {getMessage, getTitle} from 'sentry/utils/events';
  25. import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
  26. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  27. import {useLocation} from 'sentry/utils/useLocation';
  28. import useOrganization from 'sentry/utils/useOrganization';
  29. import {GroupActions} from 'sentry/views/issueDetails/actions/index';
  30. import {NewIssueExperienceButton} from 'sentry/views/issueDetails/actions/newIssueExperienceButton';
  31. import ShareIssueModal, {getShareUrl} from 'sentry/views/issueDetails/actions/shareModal';
  32. import {Divider} from 'sentry/views/issueDetails/divider';
  33. import GroupPriority from 'sentry/views/issueDetails/groupPriority';
  34. import {ShortIdBreadcrumb} from 'sentry/views/issueDetails/shortIdBreadcrumb';
  35. import {GroupHeaderAssigneeSelector} from 'sentry/views/issueDetails/streamline/header/assigneeSelector';
  36. import {AttachmentsBadge} from 'sentry/views/issueDetails/streamline/header/attachmentsBadge';
  37. import {ReplayBadge} from 'sentry/views/issueDetails/streamline/header/replayBadge';
  38. import {UserFeedbackBadge} from 'sentry/views/issueDetails/streamline/header/userFeedbackBadge';
  39. import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute';
  40. import {ReprocessingStatus} from 'sentry/views/issueDetails/utils';
  41. interface GroupHeaderProps {
  42. event: Event | null;
  43. group: Group;
  44. groupReprocessingStatus: ReprocessingStatus;
  45. project: Project;
  46. }
  47. export default function StreamlinedGroupHeader({
  48. event,
  49. group,
  50. groupReprocessingStatus,
  51. project,
  52. }: GroupHeaderProps) {
  53. const location = useLocation();
  54. const organization = useOrganization();
  55. const {baseUrl} = useGroupDetailsRoute();
  56. const {sort: _sort, ...query} = location.query;
  57. const {count: eventCount, userCount} = group;
  58. const {title: primaryTitle, subtitle} = getTitle(group);
  59. const secondaryTitle = getMessage(group);
  60. const isComplete = group.status === 'resolved' || group.status === 'ignored';
  61. const disableActions = [
  62. ReprocessingStatus.REPROCESSING,
  63. ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT,
  64. ].includes(groupReprocessingStatus);
  65. const statusProps = getBadgeProperties(group.status, group.substatus);
  66. const shareUrl = group?.shareId ? getShareUrl(group) : null;
  67. const issueTypeConfig = getConfigForIssueType(group, project);
  68. const [showLearnMore, setShowLearnMore] = useLocalStorageState(
  69. 'issue-details-learn-more',
  70. true
  71. );
  72. return (
  73. <Fragment>
  74. <Header>
  75. <Flex justify="space-between">
  76. <Flex align="center">
  77. <Breadcrumbs
  78. crumbs={[
  79. {
  80. label: 'Issues',
  81. to: {
  82. pathname: `/organizations/${organization.slug}/issues/`,
  83. query,
  84. },
  85. },
  86. {
  87. label: (
  88. <ShortIdBreadcrumb
  89. organization={organization}
  90. project={project}
  91. group={group}
  92. />
  93. ),
  94. },
  95. ]}
  96. />
  97. {group.isPublic && shareUrl ? (
  98. <Button
  99. size="xs"
  100. borderless
  101. aria-label={t('View issue share settings')}
  102. title={tct('This issue has been shared [link:with a public link].', {
  103. link: <ExternalLink href={shareUrl} />,
  104. })}
  105. tooltipProps={{isHoverable: true}}
  106. icon={
  107. <IconGlobe
  108. size="xs"
  109. color="subText"
  110. onClick={() =>
  111. openModal(modalProps => (
  112. <ShareIssueModal
  113. {...modalProps}
  114. organization={organization}
  115. projectSlug={group.project.slug}
  116. groupId={group.id}
  117. onToggle={() =>
  118. trackAnalytics('issue.shared_publicly', {
  119. organization,
  120. })
  121. }
  122. />
  123. ))
  124. }
  125. />
  126. }
  127. />
  128. ) : null}
  129. </Flex>
  130. <ButtonBar gap={0.5}>
  131. <LinkButton
  132. size="xs"
  133. external
  134. title={t('Learn more about the new UI')}
  135. href={`https://sentry.zendesk.com/hc/en-us/articles/30882241712795`}
  136. aria-label={t('Learn more about the new UI')}
  137. icon={<IconInfo />}
  138. analyticsEventKey="issue_details.streamline_ui_learn_more"
  139. analyticsEventName="Issue Details: Streamline UI Learn More"
  140. analyticsParams={{show_learn_more: showLearnMore}}
  141. onClick={() => setShowLearnMore(false)}
  142. >
  143. {showLearnMore ? t("See What's New") : null}
  144. </LinkButton>
  145. <NewIssueExperienceButton />
  146. </ButtonBar>
  147. </Flex>
  148. <HeaderGrid>
  149. <Flex gap={space(0.75)} align="baseline">
  150. <PrimaryTitle
  151. title={primaryTitle}
  152. isHoverable
  153. showOnlyOnOverflow
  154. delay={1000}
  155. >
  156. {primaryTitle}
  157. </PrimaryTitle>
  158. <SecondaryTitle
  159. title={secondaryTitle}
  160. isHoverable
  161. showOnlyOnOverflow
  162. delay={1000}
  163. isDefault={!secondaryTitle}
  164. >
  165. {secondaryTitle ?? t('No error message')}
  166. </SecondaryTitle>
  167. </Flex>
  168. <StatTitle>
  169. {issueTypeConfig.eventAndUserCounts.enabled && (
  170. <StatLink
  171. to={`${baseUrl}events/${location.search}`}
  172. aria-label={t('View events')}
  173. >
  174. {t('Events')}
  175. </StatLink>
  176. )}
  177. </StatTitle>
  178. <StatTitle>
  179. {issueTypeConfig.eventAndUserCounts.enabled &&
  180. (userCount === 0 ? (
  181. t('Users')
  182. ) : (
  183. <StatLink
  184. to={`${baseUrl}tags/user/${location.search}`}
  185. aria-label={t('View affected users')}
  186. >
  187. {t('Users')}
  188. </StatLink>
  189. ))}
  190. </StatTitle>
  191. <Flex gap={space(1)} align="center" justify="flex-start">
  192. <ErrorLevel level={group.level} size={'10px'} />
  193. {group.isUnhandled && <UnhandledTag />}
  194. {statusProps?.status ? (
  195. <Fragment>
  196. <Divider />
  197. <Tooltip title={statusProps?.tooltip}>
  198. <Subtext>{statusProps?.status}</Subtext>
  199. </Tooltip>
  200. </Fragment>
  201. ) : null}
  202. {subtitle && (
  203. <Fragment>
  204. <Divider />
  205. <Subtitle title={subtitle} isHoverable showOnlyOnOverflow delay={1000}>
  206. <Subtext>{subtitle}</Subtext>
  207. </Subtitle>
  208. </Fragment>
  209. )}
  210. <AttachmentsBadge group={group} />
  211. <UserFeedbackBadge group={group} project={project} />
  212. <ReplayBadge group={group} project={project} />
  213. </Flex>
  214. {issueTypeConfig.eventAndUserCounts.enabled && (
  215. <Fragment>
  216. <StatCount value={eventCount} aria-label={t('Event count')} />
  217. <GuideAnchor target="issue_header_stats">
  218. <StatCount value={userCount} aria-label={t('User count')} />
  219. </GuideAnchor>
  220. </Fragment>
  221. )}
  222. </HeaderGrid>
  223. </Header>
  224. <ActionBar isComplete={isComplete} role="banner">
  225. <GroupActions
  226. group={group}
  227. project={project}
  228. disabled={disableActions}
  229. event={event}
  230. />
  231. <WorkflowActions>
  232. <Workflow>
  233. {t('Priority')}
  234. <GroupPriority group={group} />
  235. </Workflow>
  236. <GuideAnchor target="issue_sidebar_owners" position="left">
  237. <Workflow>
  238. {t('Assignee')}
  239. <GroupHeaderAssigneeSelector
  240. group={group}
  241. project={project}
  242. event={event}
  243. />
  244. </Workflow>
  245. </GuideAnchor>
  246. </WorkflowActions>
  247. </ActionBar>
  248. </Fragment>
  249. );
  250. }
  251. const Header = styled('header')`
  252. background-color: ${p => p.theme.background};
  253. padding: ${space(1)} 24px;
  254. `;
  255. const HeaderGrid = styled('div')`
  256. display: grid;
  257. grid-template-columns: minmax(150px, 1fr) auto auto;
  258. column-gap: ${space(2)};
  259. align-items: center;
  260. `;
  261. const PrimaryTitle = styled(Tooltip)`
  262. overflow-x: hidden;
  263. text-overflow: ellipsis;
  264. white-space: nowrap;
  265. font-size: 20px;
  266. font-weight: ${p => p.theme.fontWeightBold};
  267. flex-shrink: 0;
  268. `;
  269. const SecondaryTitle = styled(Tooltip)<{isDefault: boolean}>`
  270. overflow-x: hidden;
  271. text-overflow: ellipsis;
  272. white-space: nowrap;
  273. font-style: ${p => (p.isDefault ? 'italic' : 'initial')};
  274. `;
  275. const StatTitle = styled('div')`
  276. display: block;
  277. color: ${p => p.theme.subText};
  278. font-size: ${p => p.theme.fontSizeSmall};
  279. font-weight: ${p => p.theme.fontWeightBold};
  280. line-height: 1;
  281. justify-self: flex-end;
  282. `;
  283. const StatLink = styled(Link)`
  284. color: ${p => p.theme.subText};
  285. text-decoration: ${p => (p['aria-disabled'] ? 'none' : 'underline')};
  286. text-decoration-style: dotted;
  287. `;
  288. const StatCount = styled(Count)`
  289. display: block;
  290. font-size: 20px;
  291. line-height: 1;
  292. text-align: right;
  293. `;
  294. const Subtext = styled('span')`
  295. color: ${p => p.theme.subText};
  296. `;
  297. const Subtitle = styled(Tooltip)`
  298. overflow: hidden;
  299. text-overflow: ellipsis;
  300. white-space: nowrap;
  301. `;
  302. const ActionBar = styled('div')<{isComplete: boolean}>`
  303. display: flex;
  304. justify-content: space-between;
  305. gap: ${space(1)};
  306. flex-wrap: wrap;
  307. padding: ${space(1)} 24px;
  308. border-bottom: 1px solid ${p => p.theme.translucentBorder};
  309. position: relative;
  310. transition: background 0.3s ease-in-out;
  311. background: ${p => (p.isComplete ? 'transparent' : p.theme.background)};
  312. &:before {
  313. z-index: -1;
  314. position: absolute;
  315. inset: 0;
  316. content: '';
  317. background: linear-gradient(
  318. to right,
  319. ${p => p.theme.background},
  320. ${p => Color(p.theme.success).lighten(0.5).alpha(0.15).string()}
  321. );
  322. }
  323. &:after {
  324. content: '';
  325. position: absolute;
  326. top: 0;
  327. right: 0;
  328. left: 24px;
  329. bottom: unset;
  330. height: 1px;
  331. background: ${p => p.theme.translucentBorder};
  332. }
  333. `;
  334. const WorkflowActions = styled('div')`
  335. display: flex;
  336. justify-content: flex-end;
  337. column-gap: ${space(2)};
  338. flex-wrap: wrap;
  339. @media (max-width: ${p => p.theme.breakpoints.large}) {
  340. justify-content: flex-start;
  341. }
  342. `;
  343. const Workflow = styled('div')`
  344. display: flex;
  345. gap: ${space(0.5)};
  346. color: ${p => p.theme.subText};
  347. align-items: center;
  348. `;