index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import {useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import Breadcrumbs from 'sentry/components/breadcrumbs';
  4. import {Button} from 'sentry/components/button';
  5. import ButtonBar from 'sentry/components/buttonBar';
  6. import DiscoverButton from 'sentry/components/discoverButton';
  7. import {HighlightsIconSummary} from 'sentry/components/events/highlights/highlightsIconSummary';
  8. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  9. import * as Layout from 'sentry/components/layouts/thirds';
  10. import Placeholder from 'sentry/components/placeholder';
  11. import {IconMegaphone} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {EventTransaction} from 'sentry/types/event';
  15. import type {Organization} from 'sentry/types/organization';
  16. import {trackAnalytics} from 'sentry/utils/analytics';
  17. import type EventView from 'sentry/utils/discover/eventView';
  18. import {SavedQueryDatasets} from 'sentry/utils/discover/types';
  19. import type {UseApiQueryResult} from 'sentry/utils/queryClient';
  20. import type RequestError from 'sentry/utils/requestError/requestError';
  21. import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
  22. import {useLocation} from 'sentry/utils/useLocation';
  23. import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
  24. import {ProjectsRenderer} from 'sentry/views/explore/tables/tracesTable/fieldRenderers';
  25. import {useModuleURLBuilder} from 'sentry/views/insights/common/utils/useModuleURL';
  26. import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
  27. import {useTraceStateDispatch} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider';
  28. import {isRootTransaction} from '../../traceDetails/utils';
  29. import type {TraceMetaQueryResults} from '../traceApi/useTraceMeta';
  30. import TraceConfigurations from '../traceConfigurations';
  31. import {isTraceNode} from '../traceGuards';
  32. import type {TraceTree} from '../traceModels/traceTree';
  33. import {useHasTraceNewUi} from '../useHasTraceNewUi';
  34. import {getTraceViewBreadcrumbs} from './breadcrumbs';
  35. import {Meta} from './meta';
  36. import {Title} from './title';
  37. export interface TraceMetadataHeaderProps {
  38. metaResults: TraceMetaQueryResults;
  39. organization: Organization;
  40. rootEventResults: UseApiQueryResult<EventTransaction, RequestError>;
  41. traceEventView: EventView;
  42. traceSlug: string;
  43. tree: TraceTree;
  44. }
  45. function FeedbackButton() {
  46. const openForm = useFeedbackForm();
  47. return openForm ? (
  48. <Button
  49. size="xs"
  50. aria-label="trace-view-feedback"
  51. icon={<IconMegaphone size="xs" />}
  52. onClick={() =>
  53. openForm({
  54. messagePlaceholder: t('How can we make the trace view better for you?'),
  55. tags: {
  56. ['feedback.source']: 'trace-view',
  57. ['feedback.owner']: 'performance',
  58. },
  59. })
  60. }
  61. >
  62. {t('Give Feedback')}
  63. </Button>
  64. ) : null;
  65. }
  66. function PlaceHolder({organization}: {organization: Organization}) {
  67. const {view} = useDomainViewFilters();
  68. const moduleURLBuilder = useModuleURLBuilder(true);
  69. const location = useLocation();
  70. return (
  71. <HeaderLayout>
  72. <HeaderContent>
  73. <HeaderRow>
  74. <Breadcrumbs
  75. crumbs={getTraceViewBreadcrumbs(
  76. organization,
  77. location,
  78. moduleURLBuilder,
  79. view
  80. )}
  81. />
  82. <FeedbackButton />
  83. </HeaderRow>
  84. <HeaderRow>
  85. <PlaceHolderTitleWrapper>
  86. <StyledPlaceholder _width={300} _height={20} />
  87. <StyledPlaceholder _width={200} _height={18} />
  88. </PlaceHolderTitleWrapper>
  89. <PlaceHolderTitleWrapper>
  90. <StyledPlaceholder _width={300} _height={18} />
  91. <StyledPlaceholder _width={300} _height={24} />
  92. </PlaceHolderTitleWrapper>
  93. </HeaderRow>
  94. <StyledBreak />
  95. <HeaderRow>
  96. <PlaceHolderHighlightWrapper>
  97. <StyledPlaceholder _width={150} _height={20} />
  98. <StyledPlaceholder _width={150} _height={20} />
  99. <StyledPlaceholder _width={150} _height={20} />
  100. </PlaceHolderHighlightWrapper>
  101. <StyledPlaceholder _width={50} _height={28} />
  102. </HeaderRow>
  103. </HeaderContent>
  104. </HeaderLayout>
  105. );
  106. }
  107. const PlaceHolderTitleWrapper = styled('div')`
  108. display: flex;
  109. flex-direction: column;
  110. gap: ${space(0.5)};
  111. `;
  112. const PlaceHolderHighlightWrapper = styled('div')`
  113. display: flex;
  114. align-items: center;
  115. gap: ${space(1)};
  116. `;
  117. const StyledPlaceholder = styled(Placeholder)<{_height: number; _width: number}>`
  118. border-radius: ${p => p.theme.borderRadius};
  119. height: ${p => p._height}px;
  120. width: ${p => p._width}px;
  121. `;
  122. const CANDIDATE_TRACE_TITLE_OPS = ['pageload', 'navigation'];
  123. export const getRepresentativeTransaction = (
  124. tree: TraceTree
  125. ): TraceTree.Transaction | null => {
  126. const traceNode = tree.root.children[0];
  127. if (!traceNode) {
  128. return null;
  129. }
  130. if (!isTraceNode(traceNode)) {
  131. throw new TypeError('Not trace node');
  132. }
  133. let firstRootTransaction: TraceTree.Transaction | null = null;
  134. let candidateTransaction: TraceTree.Transaction | null = null;
  135. let firstTransaction: TraceTree.Transaction | null = null;
  136. for (const transaction of traceNode.value.transactions || []) {
  137. // If we find a root transaction, we can stop looking and use it for the title.
  138. if (!firstRootTransaction && isRootTransaction(transaction)) {
  139. firstRootTransaction = transaction;
  140. break;
  141. } else if (
  142. // If we haven't found a root transaction, but we found a candidate transaction
  143. // with an op that we care about, we can use it for the title. We keep looking for
  144. // a root.
  145. !candidateTransaction &&
  146. CANDIDATE_TRACE_TITLE_OPS.includes(transaction['transaction.op'])
  147. ) {
  148. candidateTransaction = transaction;
  149. continue;
  150. } else if (!firstTransaction) {
  151. // If we haven't found a root or candidate transaction, we can use the first transaction
  152. // in the trace for the title.
  153. firstTransaction = transaction;
  154. }
  155. }
  156. return firstRootTransaction ?? candidateTransaction ?? firstTransaction;
  157. };
  158. function LegacyTraceMetadataHeader(props: TraceMetadataHeaderProps) {
  159. const location = useLocation();
  160. const {view} = useDomainViewFilters();
  161. const moduleURLBuilder = useModuleURLBuilder(true);
  162. const trackOpenInDiscover = useCallback(() => {
  163. trackAnalytics('performance_views.trace_view.open_in_discover', {
  164. organization: props.organization,
  165. });
  166. }, [props.organization]);
  167. return (
  168. <Layout.Header>
  169. <Layout.HeaderContent>
  170. <Breadcrumbs
  171. crumbs={getTraceViewBreadcrumbs(
  172. props.organization,
  173. location,
  174. moduleURLBuilder,
  175. view
  176. )}
  177. />
  178. </Layout.HeaderContent>
  179. <Layout.HeaderActions>
  180. <ButtonBar gap={1}>
  181. <TraceConfigurations rootEventResults={props.rootEventResults} />
  182. <DiscoverButton
  183. size="sm"
  184. to={props.traceEventView.getResultsViewUrlTarget(
  185. props.organization,
  186. false,
  187. hasDatasetSelector(props.organization)
  188. ? SavedQueryDatasets.TRANSACTIONS
  189. : undefined
  190. )}
  191. onClick={trackOpenInDiscover}
  192. >
  193. {t('Open in Discover')}
  194. </DiscoverButton>
  195. <FeedbackWidgetButton />
  196. </ButtonBar>
  197. </Layout.HeaderActions>
  198. </Layout.Header>
  199. );
  200. }
  201. export function TraceMetaDataHeader(props: TraceMetadataHeaderProps) {
  202. const location = useLocation();
  203. const hasNewTraceViewUi = useHasTraceNewUi();
  204. const {view} = useDomainViewFilters();
  205. const moduleURLBuilder = useModuleURLBuilder(true);
  206. const dispatch = useTraceStateDispatch();
  207. const onProjectClick = useCallback(
  208. (projectSlug: string) => {
  209. dispatch({type: 'set query', query: `project:${projectSlug}`, source: 'external'});
  210. },
  211. [dispatch]
  212. );
  213. const projectSlugs = useMemo(() => {
  214. return Array.from(props.tree.projects.values()).map(project => project.slug);
  215. }, [props.tree.projects]);
  216. if (!hasNewTraceViewUi) {
  217. return <LegacyTraceMetadataHeader {...props} />;
  218. }
  219. const isLoading =
  220. props.metaResults.status === 'pending' ||
  221. props.rootEventResults.isPending ||
  222. props.tree.type === 'loading';
  223. if (isLoading) {
  224. return <PlaceHolder organization={props.organization} />;
  225. }
  226. const representativeTransaction = getRepresentativeTransaction(props.tree);
  227. return (
  228. <HeaderLayout>
  229. <HeaderContent>
  230. <HeaderRow>
  231. <Breadcrumbs
  232. crumbs={getTraceViewBreadcrumbs(
  233. props.organization,
  234. location,
  235. moduleURLBuilder,
  236. view
  237. )}
  238. />
  239. <FeedbackButton />
  240. </HeaderRow>
  241. <HeaderRow>
  242. <Title
  243. tree={props.tree}
  244. traceSlug={props.traceSlug}
  245. representativeTransaction={representativeTransaction}
  246. />
  247. <Meta
  248. organization={props.organization}
  249. rootEventResults={props.rootEventResults}
  250. tree={props.tree}
  251. meta={props.metaResults.data}
  252. representativeTransaction={representativeTransaction}
  253. />
  254. </HeaderRow>
  255. <StyledBreak />
  256. {props.rootEventResults.data ? (
  257. <HeaderRow>
  258. <StyledWrapper>
  259. <HighlightsIconSummary event={props.rootEventResults.data} />
  260. </StyledWrapper>
  261. <ProjectsRendererWrapper>
  262. <ProjectsRenderer
  263. disableLink
  264. onProjectClick={onProjectClick}
  265. projectSlugs={projectSlugs}
  266. visibleAvatarSize={24}
  267. maxVisibleProjects={3}
  268. />
  269. </ProjectsRendererWrapper>
  270. </HeaderRow>
  271. ) : null}
  272. </HeaderContent>
  273. </HeaderLayout>
  274. );
  275. }
  276. // We cannot change the cursor of the ProjectBadge component so we need to wrap it in a div
  277. const ProjectsRendererWrapper = styled('div')`
  278. img {
  279. cursor: pointer;
  280. }
  281. `;
  282. const HeaderLayout = styled('div')`
  283. background-color: ${p => p.theme.background};
  284. padding: ${space(1)} ${space(3)} ${space(1)} ${space(3)};
  285. border-bottom: 1px solid ${p => p.theme.border};
  286. `;
  287. const HeaderRow = styled('div')`
  288. display: flex;
  289. justify-content: space-between;
  290. gap: ${space(2)};
  291. align-items: center;
  292. @media (max-width: ${p => p.theme.breakpoints.small}) {
  293. gap: ${space(1)};
  294. flex-direction: column;
  295. }
  296. `;
  297. const HeaderContent = styled('div')`
  298. display: flex;
  299. flex-direction: column;
  300. `;
  301. const StyledBreak = styled('hr')`
  302. margin: ${space(1)} 0;
  303. border-color: ${p => p.theme.border};
  304. `;
  305. const StyledWrapper = styled('span')`
  306. display: flex;
  307. align-items: center;
  308. & > div {
  309. padding: 0;
  310. }
  311. `;