import {useMemo, useState} from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import omit from 'lodash/omit'; import ProjectAvatar from 'sentry/components/avatar/projectAvatar'; import Breadcrumbs from 'sentry/components/breadcrumbs'; import {LinkButton} from 'sentry/components/button'; import {AggregateSpans} from 'sentry/components/events/interfaces/spans/aggregateSpans'; import FloatingFeedbackWidget from 'sentry/components/feedback/widget/floatingFeedbackWidget'; import {COL_WIDTH_UNDEFINED, GridColumnOrder} from 'sentry/components/gridEditable'; import * as Layout from 'sentry/components/layouts/thirds'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter'; import {TabList, Tabs} from 'sentry/components/tabs'; import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {defined} from 'sentry/utils'; import {decodeScalar} from 'sentry/utils/queryString'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import useRouter from 'sentry/utils/useRouter'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import {PageOverviewSidebar} from 'sentry/views/performance/browser/webVitals/components/pageOverviewSidebar'; import {PerformanceScoreBreakdownChart} from 'sentry/views/performance/browser/webVitals/components/performanceScoreBreakdownChart'; import WebVitalMeters from 'sentry/views/performance/browser/webVitals/components/webVitalMeters'; import {PageOverviewWebVitalsDetailPanel} from 'sentry/views/performance/browser/webVitals/pageOverviewWebVitalsDetailPanel'; import { PageSamplePerformanceTable, TransactionSampleRowWithScoreAndExtra, } from 'sentry/views/performance/browser/webVitals/pageSamplePerformanceTable'; import {calculatePerformanceScoreFromTableDataRow} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/calculatePerformanceScore'; import {useProjectRawWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/useProjectRawWebVitalsQuery'; import {WebVitals} from 'sentry/views/performance/browser/webVitals/utils/types'; import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders'; import {transactionSummaryRouteWithQuery} from '../../transactionSummary/utils'; export enum LandingDisplayField { OVERVIEW = 'overview', SPANS = 'spans', } const LANDING_DISPLAYS = [ { label: t('Overview'), field: LandingDisplayField.OVERVIEW, }, { label: t('Aggregate Spans'), field: LandingDisplayField.SPANS, }, ]; const SAMPLES_COLUMN_ORDER: GridColumnOrder< keyof TransactionSampleRowWithScoreAndExtra >[] = [ {key: 'id', width: COL_WIDTH_UNDEFINED, name: 'Event ID'}, {key: 'user.display', width: COL_WIDTH_UNDEFINED, name: 'User'}, {key: 'measurements.lcp', width: COL_WIDTH_UNDEFINED, name: 'LCP'}, {key: 'measurements.fcp', width: COL_WIDTH_UNDEFINED, name: 'FCP'}, {key: 'measurements.fid', width: COL_WIDTH_UNDEFINED, name: 'FID'}, {key: 'measurements.cls', width: COL_WIDTH_UNDEFINED, name: 'CLS'}, {key: 'measurements.ttfb', width: COL_WIDTH_UNDEFINED, name: 'TTFB'}, {key: 'profile.id', width: COL_WIDTH_UNDEFINED, name: 'Profile'}, {key: 'replayId', width: COL_WIDTH_UNDEFINED, name: 'Replay'}, {key: 'score', width: COL_WIDTH_UNDEFINED, name: 'Score'}, ]; function getCurrentTabSelection(selectedTab) { const tab = decodeScalar(selectedTab); if (tab && Object.values(LandingDisplayField).includes(tab as LandingDisplayField)) { return tab as LandingDisplayField; } return LandingDisplayField.OVERVIEW; } export default function PageOverview() { const organization = useOrganization(); const location = useLocation(); const {projects} = useProjects(); const router = useRouter(); const transaction = location.query.transaction ? Array.isArray(location.query.transaction) ? location.query.transaction[0] : location.query.transaction : undefined; const project = useMemo( () => projects.find(p => p.id === String(location.query.project)), [projects, location.query.project] ); const tab = getCurrentTabSelection(location.query.tab); // TODO: When visiting page overview from a specific webvital detail panel in the landing page, // we should automatically default this webvital state to the respective webvital so the detail // panel in this page opens automatically. const [state, setState] = useState<{webVital: WebVitals | null}>({ webVital: (location.query.webVital as WebVitals) ?? null, }); const query = decodeScalar(location.query.query); const {data: pageData, isLoading} = useProjectRawWebVitalsQuery({transaction}); if (transaction === undefined) { // redirect user to webvitals landing page window.location.href = normalizeUrl( `/organizations/${organization.slug}/performance/browser/pageloads/` ); return null; } const transactionSummaryTarget = project && !Array.isArray(location.query.project) && // Only render button to transaction summary when one project is selected. transaction && transactionSummaryRouteWithQuery({ orgSlug: organization.slug, transaction, query: {...location.query}, projectID: project.id, }); const projectScore = isLoading ? undefined : calculatePerformanceScoreFromTableDataRow(pageData?.data?.[0]); return ( { browserHistory.push({ ...location, query: { ...location.query, tab: value, }, }); }} > {transaction && project && } {transaction ?? t('Page Loads')} {transactionSummaryTarget && ( {t('View Transaction Summary')} )} {LANDING_DISPLAYS.map(({label, field}) => ( {label} ))} {tab === LandingDisplayField.SPANS ? ( {defined(transaction) && } ) : ( {transaction && ( {t('View All Pages')} )} { router.replace({ pathname: location.pathname, query: {...location.query, webVital}, }); setState({...state, webVital}); }} transaction={transaction} showTooltip={false} /> )} { router.replace({ pathname: router.location.pathname, query: omit(router.location.query, 'webVital'), }); setState({...state, webVital: null}); }} /> ); } const ViewAllPagesButton = styled(LinkButton)` margin-right: ${space(1)}; `; const TopMenuContainer = styled('div')` margin-bottom: ${space(1)}; display: flex; `; const Flex = styled('div')` display: flex; flex-direction: row; justify-content: space-between; width: 100%; gap: ${space(1)}; `; const PageSamplePerformanceTableContainer = styled('div')` margin-top: ${space(1)}; `; const WebVitalMetersContainer = styled('div')` margin: ${space(2)} 0 ${space(1)} 0; `;