header.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import type {ReactNode} from 'react';
  2. import type {InjectedRouter} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  5. import Badge from 'sentry/components/badge/badge';
  6. import {Button} from 'sentry/components/button';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import GlobalEventProcessingAlert from 'sentry/components/globalEventProcessingAlert';
  9. import * as Layout from 'sentry/components/layouts/thirds';
  10. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  11. import QueryCount from 'sentry/components/queryCount';
  12. import {TabList, Tabs} from 'sentry/components/tabs';
  13. import {Tooltip} from 'sentry/components/tooltip';
  14. import {SLOW_TOOLTIP_DELAY} from 'sentry/constants';
  15. import {IconPause, IconPlay} from 'sentry/icons';
  16. import {t} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import type {Organization} from 'sentry/types/organization';
  19. import useProjects from 'sentry/utils/useProjects';
  20. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  21. import IssueListSetAsDefault from 'sentry/views/issueList/issueListSetAsDefault';
  22. import type {QueryCounts} from './utils';
  23. import {
  24. CUSTOM_TAB_VALUE,
  25. FOR_REVIEW_QUERIES,
  26. getTabs,
  27. IssueSortOptions,
  28. Query,
  29. TAB_MAX_COUNT,
  30. } from './utils';
  31. type IssueListHeaderProps = {
  32. displayReprocessingTab: boolean;
  33. onRealtimeChange: (realtime: boolean) => void;
  34. organization: Organization;
  35. query: string;
  36. queryCounts: QueryCounts;
  37. realtimeActive: boolean;
  38. router: InjectedRouter;
  39. selectedProjectIds: number[];
  40. sort: string;
  41. queryCount?: number;
  42. };
  43. type IssueListHeaderTabProps = {
  44. name: string;
  45. count?: number;
  46. hasMore?: boolean;
  47. tooltipHoverable?: boolean;
  48. tooltipTitle?: ReactNode;
  49. };
  50. function IssueListHeaderTabContent({
  51. count = 0,
  52. hasMore = false,
  53. name,
  54. tooltipHoverable,
  55. tooltipTitle,
  56. }: IssueListHeaderTabProps) {
  57. return (
  58. <Tooltip
  59. title={tooltipTitle}
  60. position="bottom"
  61. isHoverable={tooltipHoverable}
  62. delay={SLOW_TOOLTIP_DELAY}
  63. >
  64. {name}{' '}
  65. {count > 0 && (
  66. <Badge>
  67. <QueryCount hideParens count={count} max={hasMore ? TAB_MAX_COUNT : 1000} />
  68. </Badge>
  69. )}
  70. </Tooltip>
  71. );
  72. }
  73. function IssueListHeader({
  74. organization,
  75. query,
  76. sort,
  77. queryCounts,
  78. realtimeActive,
  79. onRealtimeChange,
  80. router,
  81. displayReprocessingTab,
  82. selectedProjectIds,
  83. }: IssueListHeaderProps) {
  84. const {projects} = useProjects();
  85. const tabs = getTabs();
  86. const visibleTabs = displayReprocessingTab
  87. ? tabs
  88. : tabs.filter(([tab]) => tab !== Query.REPROCESSING);
  89. const tabValues = new Set(visibleTabs.map(([val]) => val));
  90. // Remove cursor and page when switching tabs
  91. const {cursor: _cursor, page: _page, ...queryParms} = router?.location?.query ?? {};
  92. const sortParam =
  93. queryParms.sort === IssueSortOptions.INBOX ? undefined : queryParms.sort;
  94. const selectedProjects = projects.filter(({id}) =>
  95. selectedProjectIds.includes(Number(id))
  96. );
  97. const realtimeTitle = realtimeActive
  98. ? t('Pause real-time updates')
  99. : t('Enable real-time updates');
  100. return (
  101. <Layout.Header noActionWrap>
  102. <Layout.HeaderContent>
  103. <Layout.Title>
  104. {t('Issues')}
  105. <PageHeadingQuestionTooltip
  106. docsUrl="https://docs.sentry.io/product/issues/"
  107. title={t(
  108. 'Detailed views of errors and performance problems in your application grouped by events with a similar set of characteristics.'
  109. )}
  110. />
  111. </Layout.Title>
  112. </Layout.HeaderContent>
  113. <Layout.HeaderActions>
  114. <ButtonBar gap={1}>
  115. <IssueListSetAsDefault {...{sort, query, organization}} />
  116. <Button
  117. size="sm"
  118. data-test-id="real-time"
  119. title={realtimeTitle}
  120. aria-label={realtimeTitle}
  121. icon={realtimeActive ? <IconPause /> : <IconPlay />}
  122. onClick={() => onRealtimeChange(!realtimeActive)}
  123. />
  124. </ButtonBar>
  125. </Layout.HeaderActions>
  126. <StyledGlobalEventProcessingAlert projects={selectedProjects} />
  127. <StyledTabs value={tabValues.has(query) ? query : CUSTOM_TAB_VALUE}>
  128. <TabList hideBorder>
  129. {visibleTabs.map(
  130. ([tabQuery, {name: queryName, tooltipTitle, tooltipHoverable, hidden}]) => {
  131. const to = normalizeUrl({
  132. query: {
  133. ...queryParms,
  134. query: tabQuery,
  135. sort: FOR_REVIEW_QUERIES.includes(tabQuery || '')
  136. ? IssueSortOptions.INBOX
  137. : sortParam,
  138. },
  139. pathname: `/organizations/${organization.slug}/issues/`,
  140. });
  141. return (
  142. <TabList.Item
  143. key={tabQuery}
  144. to={to}
  145. textValue={queryName}
  146. hidden={hidden}
  147. >
  148. <GuideAnchor
  149. disabled={tabQuery !== Query.ARCHIVED}
  150. target="issue_stream_archive_tab"
  151. position="bottom"
  152. >
  153. <IssueListHeaderTabContent
  154. tooltipTitle={tooltipTitle}
  155. tooltipHoverable={tooltipHoverable}
  156. name={queryName}
  157. count={queryCounts[tabQuery]?.count}
  158. hasMore={queryCounts[tabQuery]?.hasMore}
  159. />
  160. </GuideAnchor>
  161. </TabList.Item>
  162. );
  163. }
  164. )}
  165. </TabList>
  166. </StyledTabs>
  167. </Layout.Header>
  168. );
  169. }
  170. export default IssueListHeader;
  171. const StyledGlobalEventProcessingAlert = styled(GlobalEventProcessingAlert)`
  172. grid-column: 1/-1;
  173. margin-top: ${space(1)};
  174. margin-bottom: ${space(1)};
  175. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  176. margin-top: ${space(2)};
  177. margin-bottom: 0;
  178. }
  179. `;
  180. const StyledTabs = styled(Tabs)`
  181. grid-column: 1/-1;
  182. `;