pageOverview.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import React, {Fragment, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import omit from 'lodash/omit';
  4. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  5. import {LinkButton} from 'sentry/components/button';
  6. import {AggregateSpans} from 'sentry/components/events/interfaces/spans/aggregateSpans';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import {TabList, Tabs} from 'sentry/components/tabs';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {defined} from 'sentry/utils';
  12. import {trackAnalytics} from 'sentry/utils/analytics';
  13. import {browserHistory} from 'sentry/utils/browserHistory';
  14. import {decodeList, decodeScalar} from 'sentry/utils/queryString';
  15. import {useLocation} from 'sentry/utils/useLocation';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import useProjects from 'sentry/utils/useProjects';
  18. import useRouter from 'sentry/utils/useRouter';
  19. import BrowserTypeSelector from 'sentry/views/insights/browser/webVitals/components/browserTypeSelector';
  20. import {PerformanceScoreBreakdownChart} from 'sentry/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart';
  21. import {PageOverviewSidebar} from 'sentry/views/insights/browser/webVitals/components/pageOverviewSidebar';
  22. import {PageOverviewWebVitalsDetailPanel} from 'sentry/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel';
  23. import {PageSamplePerformanceTable} from 'sentry/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable';
  24. import WebVitalMeters from 'sentry/views/insights/browser/webVitals/components/webVitalMeters';
  25. import {useProjectRawWebVitalsQuery} from 'sentry/views/insights/browser/webVitals/queries/rawWebVitalsQueries/useProjectRawWebVitalsQuery';
  26. import {calculatePerformanceScoreFromStoredTableDataRow} from 'sentry/views/insights/browser/webVitals/queries/storedScoreQueries/calculatePerformanceScoreFromStored';
  27. import {useProjectWebVitalsScoresQuery} from 'sentry/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresQuery';
  28. import type {WebVitals} from 'sentry/views/insights/browser/webVitals/types';
  29. import decodeBrowserTypes from 'sentry/views/insights/browser/webVitals/utils/queryParameterDecoders/browserType';
  30. import {ModulePageFilterBar} from 'sentry/views/insights/common/components/modulePageFilterBar';
  31. import {ModulePageProviders} from 'sentry/views/insights/common/components/modulePageProviders';
  32. import {ModuleBodyUpsellHook} from 'sentry/views/insights/common/components/moduleUpsellHookWrapper';
  33. import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL';
  34. import {FrontendHeader} from 'sentry/views/insights/pages/frontend/frontendPageHeader';
  35. import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
  36. import {
  37. ModuleName,
  38. SpanIndexedField,
  39. type SubregionCode,
  40. } from 'sentry/views/insights/types';
  41. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  42. export enum LandingDisplayField {
  43. OVERVIEW = 'overview',
  44. SPANS = 'spans',
  45. }
  46. const LANDING_DISPLAYS = [
  47. {
  48. label: t('Overview'),
  49. field: LandingDisplayField.OVERVIEW,
  50. },
  51. {
  52. label: t('Aggregate Spans'),
  53. field: LandingDisplayField.SPANS,
  54. },
  55. ];
  56. function getCurrentTabSelection(selectedTab) {
  57. const tab = decodeScalar(selectedTab);
  58. if (tab && Object.values(LandingDisplayField).includes(tab as LandingDisplayField)) {
  59. return tab as LandingDisplayField;
  60. }
  61. return LandingDisplayField.OVERVIEW;
  62. }
  63. export function PageOverview() {
  64. const moduleURL = useModuleURL('vital');
  65. const organization = useOrganization();
  66. const location = useLocation();
  67. const {projects} = useProjects();
  68. const router = useRouter();
  69. const {view} = useDomainViewFilters();
  70. const transaction = location.query.transaction
  71. ? Array.isArray(location.query.transaction)
  72. ? location.query.transaction[0]
  73. : location.query.transaction
  74. : undefined;
  75. const project = useMemo(
  76. () => projects.find(p => p.id === String(location.query.project)),
  77. [projects, location.query.project]
  78. );
  79. const tab = getCurrentTabSelection(location.query.tab);
  80. // TODO: When visiting page overview from a specific webvital detail panel in the landing page,
  81. // we should automatically default this webvital state to the respective webvital so the detail
  82. // panel in this page opens automatically.
  83. const [state, setState] = useState<{webVital: WebVitals | null}>({
  84. webVital: (location.query.webVital as WebVitals) ?? null,
  85. });
  86. const query = decodeScalar(location.query.query);
  87. const browserTypes = decodeBrowserTypes(location.query[SpanIndexedField.BROWSER_NAME]);
  88. const subregions = decodeList(
  89. location.query[SpanIndexedField.USER_GEO_SUBREGION]
  90. ) as SubregionCode[];
  91. const {data: pageData, isPending} = useProjectRawWebVitalsQuery({
  92. transaction,
  93. browserTypes,
  94. subregions,
  95. });
  96. const {data: projectScores, isPending: isProjectScoresLoading} =
  97. useProjectWebVitalsScoresQuery({transaction, browserTypes, subregions});
  98. if (transaction === undefined) {
  99. // redirect user to webvitals landing page
  100. window.location.href = moduleURL;
  101. return null;
  102. }
  103. const transactionSummaryTarget =
  104. project &&
  105. !Array.isArray(location.query.project) && // Only render button to transaction summary when one project is selected.
  106. transaction &&
  107. transactionSummaryRouteWithQuery({
  108. orgSlug: organization.slug,
  109. transaction,
  110. query: {...location.query},
  111. projectID: project.id,
  112. view,
  113. });
  114. const projectScore =
  115. isProjectScoresLoading || isPending
  116. ? undefined
  117. : calculatePerformanceScoreFromStoredTableDataRow(projectScores?.data?.[0]);
  118. const handleTabChange = (value: string) => {
  119. trackAnalytics('insight.vital.overview.toggle_tab', {
  120. organization,
  121. tab: value,
  122. });
  123. browserHistory.push({
  124. ...location,
  125. query: {
  126. ...location.query,
  127. tab: value,
  128. },
  129. });
  130. };
  131. return (
  132. <React.Fragment>
  133. <Tabs value={tab} onChange={handleTabChange}>
  134. <FrontendHeader
  135. headerTitle={
  136. <Fragment>
  137. {transaction && project && <ProjectAvatar project={project} size={24} />}
  138. {transaction ?? t('Page Loads')}
  139. </Fragment>
  140. }
  141. headerActions={
  142. transactionSummaryTarget && (
  143. <LinkButton
  144. to={transactionSummaryTarget}
  145. onClick={() => {
  146. trackAnalytics('insight.vital.overview.open_transaction_summary', {
  147. organization,
  148. });
  149. }}
  150. size="sm"
  151. >
  152. {t('View Transaction Summary')}
  153. </LinkButton>
  154. )
  155. }
  156. hideDefaultTabs
  157. tabs={{
  158. value: tab,
  159. onTabChange: handleTabChange,
  160. tabList: (
  161. <TabList hideBorder>
  162. {LANDING_DISPLAYS.map(({label, field}) => (
  163. <TabList.Item key={field}>{label}</TabList.Item>
  164. ))}
  165. </TabList>
  166. ),
  167. }}
  168. breadcrumbs={transaction ? [{label: 'Page Summary'}] : []}
  169. module={ModuleName.VITAL}
  170. />
  171. <ModuleBodyUpsellHook moduleName={ModuleName.VITAL}>
  172. {tab === LandingDisplayField.SPANS ? (
  173. <Layout.Body>
  174. <Layout.Main fullWidth>
  175. {defined(transaction) && <AggregateSpans transaction={transaction} />}
  176. </Layout.Main>
  177. </Layout.Body>
  178. ) : (
  179. <Layout.Body>
  180. <Layout.Main>
  181. <TopMenuContainer>
  182. <ModulePageFilterBar moduleName={ModuleName.VITAL} />
  183. <BrowserTypeSelector />
  184. </TopMenuContainer>
  185. <Flex>
  186. <PerformanceScoreBreakdownChart
  187. transaction={transaction}
  188. browserTypes={browserTypes}
  189. subregions={subregions}
  190. />
  191. </Flex>
  192. <WebVitalMetersContainer>
  193. <WebVitalMeters
  194. projectData={pageData}
  195. projectScore={projectScore}
  196. onClick={webVital => {
  197. router.replace({
  198. pathname: location.pathname,
  199. query: {...location.query, webVital},
  200. });
  201. setState({...state, webVital});
  202. }}
  203. transaction={transaction}
  204. showTooltip={false}
  205. />
  206. </WebVitalMetersContainer>
  207. <PageSamplePerformanceTableContainer>
  208. <PageSamplePerformanceTable
  209. transaction={transaction}
  210. limit={15}
  211. search={query}
  212. />
  213. </PageSamplePerformanceTableContainer>
  214. </Layout.Main>
  215. <Layout.Side>
  216. <PageOverviewSidebar
  217. projectScore={projectScore}
  218. transaction={transaction}
  219. projectScoreIsLoading={isPending}
  220. browserTypes={browserTypes}
  221. subregions={subregions}
  222. />
  223. </Layout.Side>
  224. </Layout.Body>
  225. )}
  226. </ModuleBodyUpsellHook>
  227. <PageOverviewWebVitalsDetailPanel
  228. webVital={state.webVital}
  229. onClose={() => {
  230. router.replace({
  231. pathname: router.location.pathname,
  232. query: omit(router.location.query, 'webVital'),
  233. });
  234. setState({...state, webVital: null});
  235. }}
  236. />
  237. </Tabs>
  238. </React.Fragment>
  239. );
  240. }
  241. function PageWithProviders() {
  242. return (
  243. <ModulePageProviders moduleName="vital">
  244. <PageOverview />
  245. </ModulePageProviders>
  246. );
  247. }
  248. export default PageWithProviders;
  249. const TopMenuContainer = styled('div')`
  250. margin-bottom: ${space(1)};
  251. display: flex;
  252. gap: ${space(2)};
  253. `;
  254. const Flex = styled('div')`
  255. display: flex;
  256. flex-direction: row;
  257. justify-content: space-between;
  258. width: 100%;
  259. gap: ${space(1)};
  260. `;
  261. const PageSamplePerformanceTableContainer = styled('div')`
  262. margin-top: ${space(1)};
  263. `;
  264. const WebVitalMetersContainer = styled('div')`
  265. margin: ${space(2)} 0;
  266. `;