header.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import * as React from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import GuideAnchor from 'app/components/assistant/guideAnchor';
  5. import Badge from 'app/components/badge';
  6. import Button from 'app/components/button';
  7. import ButtonBar from 'app/components/buttonBar';
  8. import GlobalEventProcessingAlert from 'app/components/globalEventProcessingAlert';
  9. import * as Layout from 'app/components/layouts/thirds';
  10. import Link from 'app/components/links/link';
  11. import QueryCount from 'app/components/queryCount';
  12. import Tooltip from 'app/components/tooltip';
  13. import {IconPause, IconPlay} from 'app/icons';
  14. import {t} from 'app/locale';
  15. import space from 'app/styles/space';
  16. import {Organization, Project} from 'app/types';
  17. import {trackAnalyticsEvent} from 'app/utils/analytics';
  18. import withProjects from 'app/utils/withProjects';
  19. import SavedSearchTab from './savedSearchTab';
  20. import {getTabs, IssueSortOptions, Query, QueryCounts, TAB_MAX_COUNT} from './utils';
  21. type WrapGuideProps = {
  22. children: React.ReactElement;
  23. tabQuery: string;
  24. query: string;
  25. to: React.ComponentProps<typeof GuideAnchor>['to'];
  26. };
  27. function WrapGuideTabs({children, tabQuery, query, to}: WrapGuideProps) {
  28. if (tabQuery === Query.FOR_REVIEW) {
  29. return (
  30. <GuideAnchor target="inbox_guide_tab" disabled={query === Query.FOR_REVIEW} to={to}>
  31. <GuideAnchor target="for_review_guide_tab">{children}</GuideAnchor>
  32. </GuideAnchor>
  33. );
  34. }
  35. return children;
  36. }
  37. type Props = {
  38. organization: Organization;
  39. query: string;
  40. sort: string;
  41. queryCounts: QueryCounts;
  42. realtimeActive: boolean;
  43. router: InjectedRouter;
  44. onRealtimeChange: (realtime: boolean) => void;
  45. displayReprocessingTab: boolean;
  46. selectedProjectIds: number[];
  47. projects: Project[];
  48. queryCount?: number;
  49. } & React.ComponentProps<typeof SavedSearchTab>;
  50. function IssueListHeader({
  51. organization,
  52. query,
  53. sort,
  54. queryCount,
  55. queryCounts,
  56. realtimeActive,
  57. onRealtimeChange,
  58. onSavedSearchSelect,
  59. onSavedSearchDelete,
  60. savedSearchList,
  61. router,
  62. displayReprocessingTab,
  63. selectedProjectIds,
  64. projects,
  65. }: Props) {
  66. const tabs = getTabs(organization);
  67. const visibleTabs = displayReprocessingTab
  68. ? tabs
  69. : tabs.filter(([tab]) => tab !== Query.REPROCESSING);
  70. const savedSearchTabActive = !visibleTabs.some(([tabQuery]) => tabQuery === query);
  71. // Remove cursor and page when switching tabs
  72. const {cursor: _, page: __, ...queryParms} = router?.location?.query ?? {};
  73. const sortParam =
  74. queryParms.sort === IssueSortOptions.INBOX ? undefined : queryParms.sort;
  75. function trackTabClick(tabQuery: string) {
  76. // Clicking on inbox tab and currently another tab is active
  77. if (tabQuery === Query.FOR_REVIEW && query !== Query.FOR_REVIEW) {
  78. trackAnalyticsEvent({
  79. eventKey: 'inbox_tab.clicked',
  80. eventName: 'Clicked Inbox Tab',
  81. organization_id: organization.id,
  82. });
  83. }
  84. }
  85. const selectedProjects = projects.filter(({id}) =>
  86. selectedProjectIds.includes(Number(id))
  87. );
  88. return (
  89. <React.Fragment>
  90. <BorderlessHeader>
  91. <StyledHeaderContent>
  92. <StyledLayoutTitle>{t('Issues')}</StyledLayoutTitle>
  93. </StyledHeaderContent>
  94. <Layout.HeaderActions>
  95. <ButtonBar gap={1}>
  96. <Button
  97. size="small"
  98. data-test-id="real-time"
  99. title={t('%s real-time updates', realtimeActive ? t('Pause') : t('Enable'))}
  100. onClick={() => onRealtimeChange(!realtimeActive)}
  101. >
  102. {realtimeActive ? <IconPause size="xs" /> : <IconPlay size="xs" />}
  103. </Button>
  104. </ButtonBar>
  105. </Layout.HeaderActions>
  106. <StyledGlobalEventProcessingAlert projects={selectedProjects} />
  107. </BorderlessHeader>
  108. <TabLayoutHeader>
  109. <Layout.HeaderNavTabs underlined>
  110. {visibleTabs.map(
  111. ([tabQuery, {name: queryName, tooltipTitle, tooltipHoverable}]) => {
  112. const to = {
  113. query: {
  114. ...queryParms,
  115. query: tabQuery,
  116. sort:
  117. tabQuery === Query.FOR_REVIEW ? IssueSortOptions.INBOX : sortParam,
  118. },
  119. pathname: `/organizations/${organization.slug}/issues/`,
  120. };
  121. return (
  122. <li key={tabQuery} className={query === tabQuery ? 'active' : ''}>
  123. <Link to={to} onClick={() => trackTabClick(tabQuery)}>
  124. <WrapGuideTabs query={query} tabQuery={tabQuery} to={to}>
  125. <Tooltip
  126. title={tooltipTitle}
  127. position="bottom"
  128. isHoverable={tooltipHoverable}
  129. delay={1000}
  130. >
  131. {queryName}{' '}
  132. {queryCounts[tabQuery]?.count > 0 && (
  133. <Badge
  134. type={
  135. tabQuery === Query.FOR_REVIEW &&
  136. queryCounts[tabQuery]!.count > 0
  137. ? 'review'
  138. : 'default'
  139. }
  140. >
  141. <QueryCount
  142. hideParens
  143. count={queryCounts[tabQuery].count}
  144. max={queryCounts[tabQuery].hasMore ? TAB_MAX_COUNT : 1000}
  145. />
  146. </Badge>
  147. )}
  148. </Tooltip>
  149. </WrapGuideTabs>
  150. </Link>
  151. </li>
  152. );
  153. }
  154. )}
  155. <SavedSearchTab
  156. organization={organization}
  157. query={query}
  158. sort={sort}
  159. savedSearchList={savedSearchList}
  160. onSavedSearchSelect={onSavedSearchSelect}
  161. onSavedSearchDelete={onSavedSearchDelete}
  162. isActive={savedSearchTabActive}
  163. queryCount={queryCount}
  164. />
  165. </Layout.HeaderNavTabs>
  166. </TabLayoutHeader>
  167. </React.Fragment>
  168. );
  169. }
  170. export default withProjects(IssueListHeader);
  171. const StyledLayoutTitle = styled(Layout.Title)`
  172. margin-top: ${space(0.5)};
  173. `;
  174. const BorderlessHeader = styled(Layout.Header)`
  175. border-bottom: 0;
  176. /* Not enough buttons to change direction for mobile view */
  177. grid-template-columns: 1fr auto;
  178. `;
  179. const TabLayoutHeader = styled(Layout.Header)`
  180. padding-top: 0;
  181. @media (max-width: ${p => p.theme.breakpoints[1]}) {
  182. padding-top: 0;
  183. }
  184. `;
  185. const StyledHeaderContent = styled(Layout.HeaderContent)`
  186. margin-bottom: 0;
  187. margin-right: ${space(2)};
  188. `;
  189. const StyledGlobalEventProcessingAlert = styled(GlobalEventProcessingAlert)`
  190. grid-column: 1/-1;
  191. margin-top: ${space(1)};
  192. margin-bottom: ${space(1)};
  193. @media (min-width: ${p => p.theme.breakpoints[1]}) {
  194. margin-top: ${space(2)};
  195. margin-bottom: 0;
  196. }
  197. `;