diff --git a/src/sentry/static/sentry/app/views/performance/landing/content.tsx b/src/sentry/static/sentry/app/views/performance/landing/content.tsx index 0aa80afb766a5f..bcfea11514c254 100644 --- a/src/sentry/static/sentry/app/views/performance/landing/content.tsx +++ b/src/sentry/static/sentry/app/views/performance/landing/content.tsx @@ -26,7 +26,7 @@ import { LANDING_DISPLAYS, LandingDisplayField, } from './utils'; -import VitalsCards from './vitalsCards'; +import {FrontendCards} from './vitalsCards'; type Props = { organization: Organization; @@ -133,11 +133,11 @@ class LandingContent extends React.Component { - {currentLandingDisplay.field === LandingDisplayField.FRONTEND && ( { onSearch={handleSearch} /> - - VITALS_PLATFORMS.includes( - projects.find(project => project.id === `${projectId}`)?.platform || '' - ) - ); +export function FrontendCards(props: VitalsCardsProps) { + const {eventView, location, organization, projects, frontendOnly = false} = props; - if (!showVitalsCard && !props.isAlwaysShown) { - return null; + if (frontendOnly) { + const isFrontend = eventView.project.some(projectId => + VITALS_PLATFORMS.includes( + projects.find(project => project.id === `${projectId}`)?.platform || '' + ) + ); + + if (!isFrontend) { + return null; + } } - const shownVitals = [WebVital.FCP, WebVital.LCP, WebVital.FID, WebVital.CLS]; + const vitals = [WebVital.FCP, WebVital.LCP, WebVital.FID, WebVital.CLS]; return ( - {({isLoading, tableData}) => ( - - {props.hasCondensedVitals ? ( - - ) : ( - shownVitals.map(vitalName => ( - - )) - )} - - )} + {({isLoading, tableData}) => { + const result = tableData?.data?.[0]; + return ( + + {vitals.map(vital => { + const target = vitalDetailRouteWithQuery({ + orgSlug: organization.slug, + query: eventView.generateQueryStringObject(), + vitalName: vital, + projectID: decodeList(location.query.project), + }); + + const value = isLoading ? '\u2014' : getP75(result, vital); + const chart = ( + + ); + + return ( + + + + ); + })} + + ); + }} ); } -export default withProjects(VitalsCards); - const VitalsContainer = styled('div')` display: grid; grid-template-columns: 1fr; @@ -118,18 +129,104 @@ const VitalsContainer = styled('div')` } `; -type CardProps = Omit & { - vitalName: WebVital; - tableData: any; - isLoading?: boolean; - noBorder?: boolean; - hideBar?: boolean; - hideEmptyState?: boolean; +type VitalBarProps = { + isLoading: boolean; + result: any; + vital: WebVital | WebVital[]; + value?: string; + showBar?: boolean; + showStates?: boolean; + showDurationDetail?: boolean; + showVitalPercentNames?: boolean; }; -const NonPanel = styled('div')``; +export function VitalBar(props: VitalBarProps) { + const { + isLoading, + result, + vital, + value, + showBar = true, + showStates = false, + showDurationDetail = false, + showVitalPercentNames = false, + } = props; + + if (isLoading) { + return showStates ? : null; + } + + const emptyState = showStates ? ( + {t('No data available')} + ) : null; -const VitalCard = styled(Card)` + if (!result) { + return emptyState; + } + + const counts: Counts = { + poorCount: 0, + mehCount: 0, + goodCount: 0, + baseCount: 0, + }; + const vitals = Array.isArray(vital) ? vital : [vital]; + vitals.forEach(vitalName => { + const c = getCounts(result, vitalName); + Object.keys(counts).forEach(countKey => (counts[countKey] += c[countKey])); + }); + + if (!counts.baseCount) { + return emptyState; + } + + const p75: React.ReactNode = Array.isArray(vital) + ? null + : value ?? getP75(result, vital); + const percents = getPercentsFromCounts(counts); + const colorStops = getColorStopsFromPercents(percents); + + return ( + + {showBar && } + + {showDurationDetail && p75 && ( +
+ {t('The p75 for all transactions is ')} + {p75} +
+ )} + +
+
+ ); +} + +type VitalCardProps = { + title: string; + tooltip: string; + value: string; + chart: React.ReactNode; +}; + +function VitalCard(props: VitalCardProps) { + const {chart, title, tooltip, value} = props; + return ( + + + {t(title)} + + + {value} + {chart} + + ); +} + +const StyledCard = styled(Card)` color: ${p => p.theme.textColor}; padding: ${space(2)} ${space(3)}; align-items: flex-start; @@ -137,13 +234,13 @@ const VitalCard = styled(Card)` margin-bottom: ${space(2)}; `; -export function LinkedVitalsCard(props: CardProps) { - const {vitalName} = props; - return ( - - - - ); +function getP75(result: any, vitalName: WebVital): string { + const p75 = result?.[getAggregateAlias(vitalsP75Fields[vitalName])] ?? null; + if (p75 === null) { + return '\u2014'; + } else { + return vitalName === WebVital.CLS ? p75.toFixed(2) : `${p75.toFixed(0)}ms`; + } } type Counts = { @@ -160,7 +257,7 @@ function getCounts(result: any, vitalName: WebVital): Counts { const mehTotal: number = parseFloat(result[getAggregateAlias(vitalsMehFields[vitalName])]) || 0; const mehCount = mehTotal - poorCount; - const baseCount: number = parseFloat(base) || Number.MIN_VALUE; + const baseCount: number = parseFloat(base) || 0; const goodCount: number = baseCount - mehCount - poorCount; @@ -207,207 +304,6 @@ function getColorStopsFromPercents(percents: Percent[]) { })); } -export function VitalsCard(props: CardProps) { - const {isLoading, tableData, vitalName, noBorder, hideBar, hideEmptyState} = props; - - const measurement = vitalMap[vitalName]; - - if (isLoading || !tableData || !tableData.data || !tableData.data[0]) { - return ( - - ); - } - - const result = tableData.data[0]; - const base = result[getAggregateAlias(vitalsBaseFields[vitalName])]; - - if (!base) { - return ( - - ); - } - - const percents = getPercentsFromCounts(getCounts(result, vitalName)); - const p75: number = - parseFloat(result[getAggregateAlias(vitalsP75Fields[vitalName])]) || 0; - const value = vitalName === WebVital.CLS ? p75.toFixed(2) : p75.toFixed(0); - - return ( - - ); -} - -type CondensedCardProps = Props & { - tableData: any; - isLoading?: boolean; - condensedVitals: WebVital[]; -}; - -/** - * To aggregate and visualize all vital counts in returned data. - */ -function CondensedVitalsCard(props: CondensedCardProps) { - const {isLoading, tableData} = props; - - if (isLoading || !tableData || !tableData.data || !tableData.data[0]) { - return ; - } - - const result = tableData.data[0]; - - const vitals = props.condensedVitals; - - const allCounts: Counts = { - poorCount: 0, - mehCount: 0, - goodCount: 0, - baseCount: 0, - }; - vitals.forEach(vitalName => { - const counts = getCounts(result, vitalName); - Object.keys(counts).forEach(countKey => (allCounts[countKey] += counts[countKey])); - }); - - if (!allCounts.baseCount) { - return ; - } - - const percents = getPercentsFromCounts(allCounts); - - return ( - - ); -} - -type CardContentProps = { - percents: Percent[]; - title?: string; - titleDescription?: string; - value?: string; - noBorder?: boolean; - showVitalPercentNames?: boolean; - showDurationDetail?: boolean; - hideBar?: boolean; -}; - -function VitalsCardContent(props: CardContentProps) { - const { - percents, - noBorder, - title, - titleDescription, - value, - showVitalPercentNames, - showDurationDetail, - hideBar, - } = props; - const Container = noBorder ? NonPanel : VitalCard; - const colorStops = getColorStopsFromPercents(percents); - - return ( - - {noBorder || ( - - {t(`${title}`)} - - - )} - {noBorder || {value}} - {!hideBar && } - - {showDurationDetail && ( -
- {t('The p75 for all transactions is ')} - {value} -
- )} - -
-
- ); -} - -type BlankCardProps = { - noBorder?: boolean; - measurement?: string; - titleDescription?: string; - hideEmptyState?: boolean; -}; - -const BlankCard = (props: BlankCardProps) => { - const Container = props.noBorder ? NonPanel : VitalCard; - - if (props.hideEmptyState) { - return null; - } - - return ( - - {props.noBorder || ( - - {t(`${props.measurement}`)} - - - )} - {'\u2014'} - - ); -}; - -type VitalLinkProps = Omit & { - vitalName: WebVital; - children: React.ReactNode; -}; - -const VitalLink = (props: VitalLinkProps) => { - const {organization, eventView, vitalName, children, location} = props; - - const view = eventView.clone(); - - const target = vitalDetailRouteWithQuery({ - orgSlug: organization.slug, - query: view.generateQueryStringObject(), - vitalName, - projectID: decodeList(location.query.project), - }); - - return ( - - {children} - - ); -}; - const BarDetail = styled('div')` font-size: ${p => p.theme.fontSizeMedium}; diff --git a/src/sentry/static/sentry/app/views/performance/transactionSummary/userStats.tsx b/src/sentry/static/sentry/app/views/performance/transactionSummary/userStats.tsx index f7587a587151d7..369a533422235b 100644 --- a/src/sentry/static/sentry/app/views/performance/transactionSummary/userStats.tsx +++ b/src/sentry/static/sentry/app/views/performance/transactionSummary/userStats.tsx @@ -13,7 +13,7 @@ import space from 'app/styles/space'; import {Organization} from 'app/types'; import EventView from 'app/utils/discover/eventView'; import {getFieldRenderer} from 'app/utils/discover/fieldRenderers'; -import {getAggregateAlias} from 'app/utils/discover/fields'; +import {getAggregateAlias, WebVital} from 'app/utils/discover/fields'; import {decodeScalar} from 'app/utils/queryString'; import {getTermHelp} from 'app/views/performance/data'; import { @@ -23,7 +23,7 @@ import { } from 'app/views/performance/transactionVitals/constants'; import {vitalsRouteWithQuery} from 'app/views/performance/transactionVitals/utils'; -import VitalsCards from '../landing/vitalsCards'; +import VitalInfo from '../vitalDetail/vitalInfo'; type Props = { eventView: EventView; @@ -125,11 +125,13 @@ function UserStats({eventView, totals, location, organization, transactionName}: - ); diff --git a/src/sentry/static/sentry/app/views/performance/transactionVitals/vitalCard.tsx b/src/sentry/static/sentry/app/views/performance/transactionVitals/vitalCard.tsx index 63acf3eaa26d65..f1fa200250fd46 100644 --- a/src/sentry/static/sentry/app/views/performance/transactionVitals/vitalCard.tsx +++ b/src/sentry/static/sentry/app/views/performance/transactionVitals/vitalCard.tsx @@ -303,11 +303,11 @@ class VitalCard extends React.Component { eventView={eventView} organization={organization} location={location} - vitalName={vital} + vital={vital} hideBar + hideStates hideVitalPercentNames hideDurationDetail - hideEmptyState /> diff --git a/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalDetailContent.tsx b/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalDetailContent.tsx index 2ef68004df4224..75e98e185bb7a0 100644 --- a/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalDetailContent.tsx +++ b/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalDetailContent.tsx @@ -184,7 +184,7 @@ class VitalDetailContent extends React.Component { eventView={eventView} organization={organization} location={location} - vitalName={vital} + vital={vital} /> {({isLoading, tableData}) => ( - - - + )} ); diff --git a/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalsCardsDiscoverQuery.tsx b/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalsCardsDiscoverQuery.tsx index 2d638ab7c492d5..d773180a949d10 100644 --- a/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalsCardsDiscoverQuery.tsx +++ b/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalsCardsDiscoverQuery.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {MetaType} from 'app/utils/discover/eventView'; +import {WebVital} from 'app/utils/discover/fields'; import GenericDiscoverQuery, { DiscoverQueryProps, } from 'app/utils/discover/genericDiscoverQuery'; @@ -23,25 +24,22 @@ export type TableData = { }; type Props = DiscoverQueryProps & { - onlyVital?: string; + vitals: WebVital[]; }; function getRequestPayload(props: Props) { - const {eventView, onlyVital} = props; + const {eventView, vitals} = props; const apiPayload = eventView?.getEventsAPIPayload(props.location); - const vitalFields = onlyVital - ? [ - vitalsPoorFields[onlyVital], - vitalsBaseFields[onlyVital], - vitalsMehFields[onlyVital], - vitalsP75Fields[onlyVital], - ] - : [ - ...Object.values(vitalsPoorFields), - ...Object.values(vitalsMehFields), - ...Object.values(vitalsBaseFields), - ...Object.values(vitalsP75Fields), - ]; + const vitalFields: string[] = vitals + .map(vital => + [ + vitalsPoorFields[vital], + vitalsBaseFields[vital], + vitalsMehFields[vital], + vitalsP75Fields[vital], + ].filter(Boolean) + ) + .reduce((fields, fs) => fields.concat(fs), []); apiPayload.field = [...vitalFields]; delete apiPayload.sort; return apiPayload; @@ -49,7 +47,7 @@ function getRequestPayload(props: Props) { function VitalsCardDiscoverQuery(props: Props) { return ( - + getRequestPayload={getRequestPayload} route="eventsv2" noPagination