header.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import {ReactNode} from 'react';
  2. import {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';
  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 {Organization} from 'sentry/types';
  19. import useProjects from 'sentry/utils/useProjects';
  20. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  21. import IssueListSetAsDefault from 'sentry/views/issueList/issueListSetAsDefault';
  22. import {
  23. CUSTOM_TAB_VALUE,
  24. FOR_REVIEW_QUERIES,
  25. getTabs,
  26. IssueSortOptions,
  27. Query,
  28. QueryCounts,
  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(organization);
  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: _, 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 size="xs" /> : <IconPlay size="xs" />}
  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. {[
  130. ...visibleTabs.map(
  131. ([tabQuery, {name: queryName, tooltipTitle, tooltipHoverable, hidden}]) => {
  132. const to = normalizeUrl({
  133. query: {
  134. ...queryParms,
  135. query: tabQuery,
  136. sort: FOR_REVIEW_QUERIES.includes(tabQuery || '')
  137. ? IssueSortOptions.INBOX
  138. : sortParam,
  139. },
  140. pathname: `/organizations/${organization.slug}/issues/`,
  141. });
  142. return (
  143. <TabList.Item
  144. key={tabQuery}
  145. to={to}
  146. textValue={queryName}
  147. hidden={hidden}
  148. >
  149. <GuideAnchor
  150. disabled={tabQuery !== Query.ARCHIVED}
  151. target="issue_stream_archive_tab"
  152. position="bottom"
  153. >
  154. <IssueListHeaderTabContent
  155. tooltipTitle={tooltipTitle}
  156. tooltipHoverable={tooltipHoverable}
  157. name={queryName}
  158. count={queryCounts[tabQuery]?.count}
  159. hasMore={queryCounts[tabQuery]?.hasMore}
  160. />
  161. </GuideAnchor>
  162. </TabList.Item>
  163. );
  164. }
  165. ),
  166. ]}
  167. </TabList>
  168. </StyledTabs>
  169. </Layout.Header>
  170. );
  171. }
  172. export default IssueListHeader;
  173. const StyledGlobalEventProcessingAlert = styled(GlobalEventProcessingAlert)`
  174. grid-column: 1/-1;
  175. margin-top: ${space(1)};
  176. margin-bottom: ${space(1)};
  177. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  178. margin-top: ${space(2)};
  179. margin-bottom: 0;
  180. }
  181. `;
  182. const StyledTabs = styled(Tabs)`
  183. grid-column: 1/-1;
  184. `;