header.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import type {ReactNode} from 'react';
  2. import type {InjectedRouter} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {openDataConsentModal} from 'sentry/actionCreators/modal';
  5. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  6. import Badge from 'sentry/components/badge';
  7. import {Button} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import GlobalEventProcessingAlert from 'sentry/components/globalEventProcessingAlert';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  12. import QueryCount from 'sentry/components/queryCount';
  13. import {TabList, Tabs} from 'sentry/components/tabs';
  14. import {Tooltip} from 'sentry/components/tooltip';
  15. import {SLOW_TOOLTIP_DELAY} from 'sentry/constants';
  16. import {t} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import type {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 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. router,
  79. displayReprocessingTab,
  80. selectedProjectIds,
  81. }: IssueListHeaderProps) {
  82. const {projects} = useProjects();
  83. const tabs = getTabs(organization);
  84. const visibleTabs = displayReprocessingTab
  85. ? tabs
  86. : tabs.filter(([tab]) => tab !== Query.REPROCESSING);
  87. const tabValues = new Set(visibleTabs.map(([val]) => val));
  88. // Remove cursor and page when switching tabs
  89. const {cursor: _cursor, page: _page, ...queryParms} = router?.location?.query ?? {};
  90. const sortParam =
  91. queryParms.sort === IssueSortOptions.INBOX ? undefined : queryParms.sort;
  92. const selectedProjects = projects.filter(({id}) =>
  93. selectedProjectIds.includes(Number(id))
  94. );
  95. return (
  96. <Layout.Header noActionWrap>
  97. <Layout.HeaderContent>
  98. <Layout.Title>
  99. {t('Issues')}
  100. <PageHeadingQuestionTooltip
  101. docsUrl="https://docs.sentry.io/product/issues/"
  102. title={t(
  103. 'Detailed views of errors and performance problems in your application grouped by events with a similar set of characteristics.'
  104. )}
  105. />
  106. </Layout.Title>
  107. </Layout.HeaderContent>
  108. <Layout.HeaderActions>
  109. <ButtonBar gap={1}>
  110. <IssueListSetAsDefault {...{sort, query, organization}} />
  111. {/* <Button
  112. size="sm"
  113. data-test-id="real-time"
  114. title={realtimeTitle}
  115. aria-label={realtimeTitle}
  116. icon={realtimeActive ? <IconPause /> : <IconPlay />}
  117. onClick={() => onRealtimeChange(!realtimeActive)}
  118. /> */}
  119. <Button aria-label="test" onClick={() => openDataConsentModal()} />
  120. </ButtonBar>
  121. </Layout.HeaderActions>
  122. <StyledGlobalEventProcessingAlert projects={selectedProjects} />
  123. <StyledTabs value={tabValues.has(query) ? query : CUSTOM_TAB_VALUE}>
  124. <TabList hideBorder>
  125. {visibleTabs.map(
  126. ([tabQuery, {name: queryName, tooltipTitle, tooltipHoverable, hidden}]) => {
  127. const to = normalizeUrl({
  128. query: {
  129. ...queryParms,
  130. query: tabQuery,
  131. sort: FOR_REVIEW_QUERIES.includes(tabQuery || '')
  132. ? IssueSortOptions.INBOX
  133. : sortParam,
  134. },
  135. pathname: `/organizations/${organization.slug}/issues/`,
  136. });
  137. return (
  138. <TabList.Item
  139. key={tabQuery}
  140. to={to}
  141. textValue={queryName}
  142. hidden={hidden}
  143. >
  144. <GuideAnchor
  145. disabled={tabQuery !== Query.ARCHIVED}
  146. target="issue_stream_archive_tab"
  147. position="bottom"
  148. >
  149. <IssueListHeaderTabContent
  150. tooltipTitle={tooltipTitle}
  151. tooltipHoverable={tooltipHoverable}
  152. name={queryName}
  153. count={queryCounts[tabQuery]?.count}
  154. hasMore={queryCounts[tabQuery]?.hasMore}
  155. />
  156. </GuideAnchor>
  157. </TabList.Item>
  158. );
  159. }
  160. )}
  161. </TabList>
  162. </StyledTabs>
  163. </Layout.Header>
  164. );
  165. }
  166. export default IssueListHeader;
  167. const StyledGlobalEventProcessingAlert = styled(GlobalEventProcessingAlert)`
  168. grid-column: 1/-1;
  169. margin-top: ${space(1)};
  170. margin-bottom: ${space(1)};
  171. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  172. margin-top: ${space(2)};
  173. margin-bottom: 0;
  174. }
  175. `;
  176. const StyledTabs = styled(Tabs)`
  177. grid-column: 1/-1;
  178. `;