pageOverview.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import {useMemo, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  5. import Breadcrumbs from 'sentry/components/breadcrumbs';
  6. import {LinkButton} from 'sentry/components/button';
  7. import {AggregateSpans} from 'sentry/components/events/interfaces/spans/aggregateSpans';
  8. import FeatureBadge from 'sentry/components/featureBadge';
  9. import FeedbackWidget from 'sentry/components/feedback/widget/feedbackWidget';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  12. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  13. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  14. import {TabList, Tabs} from 'sentry/components/tabs';
  15. import {IconChevron} from 'sentry/icons';
  16. import {t} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import {Tag} from 'sentry/types';
  19. import {defined} from 'sentry/utils';
  20. import {decodeScalar} from 'sentry/utils/queryString';
  21. import {useLocation} from 'sentry/utils/useLocation';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import useProjects from 'sentry/utils/useProjects';
  24. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  25. import {PageOverviewFeaturedTagsList} from 'sentry/views/performance/browser/webVitals/components/pageOverviewFeaturedTagsList';
  26. import {PageOverviewSidebar} from 'sentry/views/performance/browser/webVitals/components/pageOverviewSidebar';
  27. import {PerformanceScoreBreakdownChart} from 'sentry/views/performance/browser/webVitals/components/performanceScoreBreakdownChart';
  28. import WebVitalsRingMeters from 'sentry/views/performance/browser/webVitals/components/webVitalsRingMeters';
  29. import {PageOverviewWebVitalsDetailPanel} from 'sentry/views/performance/browser/webVitals/pageOverviewWebVitalsDetailPanel';
  30. import {calculatePerformanceScore} from 'sentry/views/performance/browser/webVitals/utils/calculatePerformanceScore';
  31. import {WebVitals} from 'sentry/views/performance/browser/webVitals/utils/types';
  32. import {useProjectWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/useProjectWebVitalsQuery';
  33. import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders';
  34. import {PageOverviewWebVitalsTagDetailPanel} from './pageOverWebVitalsTagDetailPanel';
  35. export enum LandingDisplayField {
  36. OVERVIEW = 'overview',
  37. SPANS = 'spans',
  38. }
  39. const LANDING_DISPLAYS = [
  40. {
  41. label: t('Overview'),
  42. field: LandingDisplayField.OVERVIEW,
  43. },
  44. {
  45. label: t('Aggregate Spans'),
  46. field: LandingDisplayField.SPANS,
  47. },
  48. ];
  49. function getCurrentTabSelection(selectedTab) {
  50. const tab = decodeScalar(selectedTab);
  51. if (tab && Object.values(LandingDisplayField).includes(tab as LandingDisplayField)) {
  52. return tab as LandingDisplayField;
  53. }
  54. return LandingDisplayField.OVERVIEW;
  55. }
  56. export default function PageOverview() {
  57. const organization = useOrganization();
  58. const location = useLocation();
  59. const {projects} = useProjects();
  60. const transaction = location.query.transaction
  61. ? Array.isArray(location.query.transaction)
  62. ? location.query.transaction[0]
  63. : location.query.transaction
  64. : undefined;
  65. const project = useMemo(
  66. () => projects.find(p => p.id === String(location.query.project)),
  67. [projects, location.query.project]
  68. );
  69. const tab = getCurrentTabSelection(location.query.tab);
  70. // TODO: When visiting page overview from a specific webvital detail panel in the landing page,
  71. // we should automatically default this webvital state to the respective webvital so the detail
  72. // panel in this page opens automatically.
  73. const [state, setState] = useState<{webVital: WebVitals | null; tag?: Tag}>({
  74. webVital: (location.query.webVital as WebVitals) ?? null,
  75. tag: undefined,
  76. });
  77. const {data: pageData, isLoading} = useProjectWebVitalsQuery({transaction});
  78. if (transaction === undefined) {
  79. // redirect user to webvitals landing page
  80. window.location.href = normalizeUrl(
  81. `/organizations/${organization.slug}/performance/browser/pageloads/`
  82. );
  83. return null;
  84. }
  85. const projectScore = isLoading
  86. ? undefined
  87. : calculatePerformanceScore({
  88. lcp: pageData?.data[0]['p75(measurements.lcp)'] as number,
  89. fcp: pageData?.data[0]['p75(measurements.fcp)'] as number,
  90. cls: pageData?.data[0]['p75(measurements.cls)'] as number,
  91. ttfb: pageData?.data[0]['p75(measurements.ttfb)'] as number,
  92. fid: pageData?.data[0]['p75(measurements.fid)'] as number,
  93. });
  94. return (
  95. <ModulePageProviders title={[t('Performance'), t('Web Vitals')].join(' — ')}>
  96. <Tabs
  97. value={tab}
  98. onChange={value => {
  99. browserHistory.push({
  100. ...location,
  101. query: {
  102. ...location.query,
  103. tab: value,
  104. },
  105. });
  106. }}
  107. >
  108. <Layout.Header>
  109. <Layout.HeaderContent>
  110. <Breadcrumbs
  111. crumbs={[
  112. {
  113. label: 'Performance',
  114. to: normalizeUrl(`/organizations/${organization.slug}/performance/`),
  115. preservePageFilters: true,
  116. },
  117. {
  118. label: 'Web Vitals',
  119. to: normalizeUrl(
  120. `/organizations/${organization.slug}/performance/browser/pageloads/`
  121. ),
  122. preservePageFilters: true,
  123. },
  124. ...(transaction ? [{label: 'Page Overview'}] : []),
  125. ]}
  126. />
  127. <Layout.Title>
  128. {transaction && project && <ProjectAvatar project={project} size={24} />}
  129. {transaction ?? t('Page Loads')}
  130. <FeatureBadge type="alpha" />
  131. </Layout.Title>
  132. </Layout.HeaderContent>
  133. <Layout.HeaderActions />
  134. <TabList hideBorder>
  135. {LANDING_DISPLAYS.map(({label, field}) => (
  136. <TabList.Item key={field}>{label}</TabList.Item>
  137. ))}
  138. </TabList>
  139. </Layout.Header>
  140. {tab === LandingDisplayField.SPANS ? (
  141. <Layout.Body>
  142. <Layout.Main fullWidth>
  143. {defined(transaction) && <AggregateSpans transaction={transaction} />}
  144. </Layout.Main>
  145. </Layout.Body>
  146. ) : (
  147. <Layout.Body>
  148. <FeedbackWidget />
  149. <Layout.Main>
  150. <TopMenuContainer>
  151. {transaction && (
  152. <ViewAllPagesButton
  153. to={{
  154. ...location,
  155. pathname: '/performance/browser/pageloads/',
  156. query: {...location.query, transaction: undefined},
  157. }}
  158. >
  159. <IconChevron direction="left" /> {t('View All Pages')}
  160. </ViewAllPagesButton>
  161. )}
  162. <PageFilterBar condensed>
  163. <ProjectPageFilter />
  164. <DatePageFilter />
  165. </PageFilterBar>
  166. </TopMenuContainer>
  167. <Flex>
  168. <PerformanceScoreBreakdownChart transaction={transaction} />
  169. </Flex>
  170. <WebVitalsRingMeters
  171. projectScore={projectScore}
  172. onClick={webVital => setState({...state, webVital})}
  173. transaction={transaction}
  174. />
  175. <Flex>
  176. <PageOverviewFeaturedTagsList
  177. tag="browser.name"
  178. title={t('Slowest Browsers')}
  179. transaction={transaction}
  180. onClick={tag => setState({...state, tag})}
  181. />
  182. <PageOverviewFeaturedTagsList
  183. tag="release"
  184. title={t('Slowest Releases')}
  185. transaction={transaction}
  186. onClick={tag => setState({...state, tag})}
  187. />
  188. <PageOverviewFeaturedTagsList
  189. tag="geo.country_code"
  190. title={t('Slowest Regions')}
  191. transaction={transaction}
  192. onClick={tag => setState({...state, tag})}
  193. />
  194. </Flex>
  195. </Layout.Main>
  196. <Layout.Side>
  197. <PageOverviewSidebar
  198. projectScore={projectScore}
  199. transaction={transaction}
  200. />
  201. </Layout.Side>
  202. </Layout.Body>
  203. )}
  204. <PageOverviewWebVitalsDetailPanel
  205. webVital={state.webVital}
  206. onClose={() => {
  207. setState({...state, webVital: null});
  208. }}
  209. />
  210. <PageOverviewWebVitalsTagDetailPanel
  211. tag={state.tag}
  212. onClose={() => {
  213. setState({...state, tag: undefined});
  214. }}
  215. />
  216. </Tabs>
  217. </ModulePageProviders>
  218. );
  219. }
  220. const ViewAllPagesButton = styled(LinkButton)`
  221. margin-right: ${space(1)};
  222. `;
  223. const TopMenuContainer = styled('div')`
  224. display: flex;
  225. `;
  226. const Flex = styled('div')`
  227. display: flex;
  228. flex-direction: row;
  229. justify-content: space-between;
  230. width: 100%;
  231. gap: ${space(1)};
  232. margin-top: ${space(1)};
  233. `;