header.tsx 6.4 KB

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