header.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import Feature from 'app/components/acl/feature';
  5. import {GuideAnchor} from 'app/components/assistant/guideAnchor';
  6. import Button from 'app/components/button';
  7. import ButtonBar from 'app/components/buttonBar';
  8. import {CreateAlertFromViewButton} from 'app/components/createAlertButton';
  9. import FeatureBadge from 'app/components/featureBadge';
  10. import * as Layout from 'app/components/layouts/thirds';
  11. import ListLink from 'app/components/links/listLink';
  12. import NavTabs from 'app/components/navTabs';
  13. import {IconSettings} from 'app/icons';
  14. import {t} from 'app/locale';
  15. import {Organization, Project} from 'app/types';
  16. import {trackAnalyticsEvent} from 'app/utils/analytics';
  17. import EventView from 'app/utils/discover/eventView';
  18. import {decodeScalar} from 'app/utils/queryString';
  19. import Breadcrumb from 'app/views/performance/breadcrumb';
  20. import {eventsRouteWithQuery} from './transactionEvents/utils';
  21. import {tagsRouteWithQuery} from './transactionTags/utils';
  22. import {vitalsRouteWithQuery} from './transactionVitals/utils';
  23. import TeamKeyTransactionButton from './teamKeyTransactionButton';
  24. import TransactionThresholdButton from './transactionThresholdButton';
  25. import {TransactionThresholdMetric} from './transactionThresholdModal';
  26. import {transactionSummaryRouteWithQuery} from './utils';
  27. export enum Tab {
  28. TransactionSummary,
  29. RealUserMonitoring,
  30. Tags,
  31. Events,
  32. }
  33. type Props = {
  34. eventView: EventView;
  35. location: Location;
  36. organization: Organization;
  37. projects: Project[];
  38. transactionName: string;
  39. currentTab: Tab;
  40. hasWebVitals: boolean;
  41. onChangeThreshold?: (threshold: number, metric: TransactionThresholdMetric) => void;
  42. handleIncompatibleQuery: React.ComponentProps<
  43. typeof CreateAlertFromViewButton
  44. >['onIncompatibleQuery'];
  45. };
  46. class TransactionHeader extends React.Component<Props> {
  47. trackAlertClick(errors?: Record<string, boolean>) {
  48. const {organization} = this.props;
  49. trackAnalyticsEvent({
  50. eventKey: 'performance_views.summary.create_alert_clicked',
  51. eventName: 'Performance Views: Create alert clicked',
  52. organization_id: organization.id,
  53. status: errors ? 'error' : 'success',
  54. errors,
  55. url: window.location.href,
  56. });
  57. }
  58. trackVitalsTabClick = () => {
  59. const {organization} = this.props;
  60. trackAnalyticsEvent({
  61. eventKey: 'performance_views.vitals.vitals_tab_clicked',
  62. eventName: 'Performance Views: Vitals tab clicked',
  63. organization_id: organization.id,
  64. });
  65. };
  66. trackTagsTabClick = () => {
  67. const {organization} = this.props;
  68. trackAnalyticsEvent({
  69. eventKey: 'performance_views.tags.tags_tab_clicked',
  70. eventName: 'Performance Views: Tags tab clicked',
  71. organization_id: organization.id,
  72. });
  73. };
  74. trackEventsTabClick = () => {
  75. const {organization} = this.props;
  76. trackAnalyticsEvent({
  77. eventKey: 'performance_views.events.events_tab_clicked',
  78. eventName: 'Performance Views: Events tab clicked',
  79. organization_id: organization.id,
  80. });
  81. };
  82. handleIncompatibleQuery: React.ComponentProps<
  83. typeof CreateAlertFromViewButton
  84. >['onIncompatibleQuery'] = (incompatibleAlertNoticeFn, errors) => {
  85. this.trackAlertClick(errors);
  86. this.props.handleIncompatibleQuery?.(incompatibleAlertNoticeFn, errors);
  87. };
  88. handleCreateAlertSuccess = () => {
  89. this.trackAlertClick();
  90. };
  91. renderCreateAlertButton() {
  92. const {eventView, organization, projects} = this.props;
  93. return (
  94. <CreateAlertFromViewButton
  95. eventView={eventView}
  96. organization={organization}
  97. projects={projects}
  98. onIncompatibleQuery={this.handleIncompatibleQuery}
  99. onSuccess={this.handleCreateAlertSuccess}
  100. referrer="performance"
  101. />
  102. );
  103. }
  104. renderKeyTransactionButton() {
  105. const {eventView, organization, transactionName} = this.props;
  106. return (
  107. <TeamKeyTransactionButton
  108. transactionName={transactionName}
  109. eventView={eventView}
  110. organization={organization}
  111. />
  112. );
  113. }
  114. renderSettingsButton() {
  115. const {organization, transactionName, eventView, onChangeThreshold} = this.props;
  116. return (
  117. <Feature
  118. organization={organization}
  119. features={['project-transaction-threshold-override']}
  120. >
  121. {({hasFeature}) =>
  122. hasFeature ? (
  123. <GuideAnchor
  124. target="project_transaction_threshold_override"
  125. position="bottom"
  126. >
  127. <TransactionThresholdButton
  128. organization={organization}
  129. transactionName={transactionName}
  130. eventView={eventView}
  131. onChangeThreshold={onChangeThreshold}
  132. />
  133. </GuideAnchor>
  134. ) : (
  135. <Button
  136. href={`/settings/${organization.slug}/performance/`}
  137. icon={<IconSettings />}
  138. aria-label={t('Settings')}
  139. />
  140. )
  141. }
  142. </Feature>
  143. );
  144. }
  145. render() {
  146. const {organization, location, transactionName, currentTab, hasWebVitals} =
  147. this.props;
  148. const summaryTarget = transactionSummaryRouteWithQuery({
  149. orgSlug: organization.slug,
  150. transaction: transactionName,
  151. projectID: decodeScalar(location.query.project),
  152. query: location.query,
  153. });
  154. const vitalsTarget = vitalsRouteWithQuery({
  155. orgSlug: organization.slug,
  156. transaction: transactionName,
  157. projectID: decodeScalar(location.query.project),
  158. query: location.query,
  159. });
  160. const tagsTarget = tagsRouteWithQuery({
  161. orgSlug: organization.slug,
  162. transaction: transactionName,
  163. projectID: decodeScalar(location.query.project),
  164. query: location.query,
  165. });
  166. const eventsTarget = eventsRouteWithQuery({
  167. orgSlug: organization.slug,
  168. transaction: transactionName,
  169. projectID: decodeScalar(location.query.project),
  170. query: location.query,
  171. });
  172. return (
  173. <Layout.Header>
  174. <Layout.HeaderContent>
  175. <Breadcrumb
  176. organization={organization}
  177. location={location}
  178. transactionName={transactionName}
  179. realUserMonitoring={currentTab === Tab.RealUserMonitoring}
  180. />
  181. <Layout.Title>{transactionName}</Layout.Title>
  182. </Layout.HeaderContent>
  183. <Layout.HeaderActions>
  184. <ButtonBar gap={1}>
  185. <Feature organization={organization} features={['incidents']}>
  186. {({hasFeature}) => hasFeature && this.renderCreateAlertButton()}
  187. </Feature>
  188. {this.renderKeyTransactionButton()}
  189. {this.renderSettingsButton()}
  190. </ButtonBar>
  191. </Layout.HeaderActions>
  192. <React.Fragment>
  193. <StyledNavTabs>
  194. <ListLink
  195. to={summaryTarget}
  196. isActive={() => currentTab === Tab.TransactionSummary}
  197. >
  198. {t('Overview')}
  199. </ListLink>
  200. {hasWebVitals && (
  201. <ListLink
  202. to={vitalsTarget}
  203. isActive={() => currentTab === Tab.RealUserMonitoring}
  204. onClick={this.trackVitalsTabClick}
  205. >
  206. {t('Web Vitals')}
  207. </ListLink>
  208. )}
  209. <Feature features={['organizations:performance-tag-page']}>
  210. <ListLink
  211. to={tagsTarget}
  212. isActive={() => currentTab === Tab.Tags}
  213. onClick={this.trackTagsTabClick}
  214. >
  215. {t('Tags')}
  216. <FeatureBadge type="beta" noTooltip />
  217. </ListLink>
  218. </Feature>
  219. <Feature features={['organizations:performance-events-page']}>
  220. <ListLink
  221. to={eventsTarget}
  222. isActive={() => currentTab === Tab.Events}
  223. onClick={this.trackEventsTabClick}
  224. >
  225. {t('All Events')}
  226. <FeatureBadge type="new" noTooltip />
  227. </ListLink>
  228. </Feature>
  229. </StyledNavTabs>
  230. </React.Fragment>
  231. </Layout.Header>
  232. );
  233. }
  234. }
  235. const StyledNavTabs = styled(NavTabs)`
  236. margin-bottom: 0;
  237. /* Makes sure the tabs are pushed into another row */
  238. width: 100%;
  239. `;
  240. export default TransactionHeader;