From b41c91d9b8d434bf6cc933a7a603376036649aab Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Tue, 12 Jan 2021 18:21:39 -0500 Subject: [PATCH 1/3] ref(vitals): Restructure vitals cards Restructure vitals cards to prepare for the upcoming backend cards. This restructure lifts the chart component of the cards to be customizable via a prop and simplifies the card for easier reuse. --- .../app/views/performance/landing/content.tsx | 10 +- .../views/performance/landing/vitalsCards.tsx | 436 +++++++----------- .../transactionSummary/userStats.tsx | 10 +- .../transactionVitals/vitalCard.tsx | 4 +- .../vitalDetail/vitalDetailContent.tsx | 2 +- .../performance/vitalDetail/vitalInfo.tsx | 19 +- .../vitalDetail/vitalsCardsDiscoverQuery.tsx | 30 +- 7 files changed, 209 insertions(+), 302 deletions(-) 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 || '' - ) - ); + if (frontendOnly) { + const isFrontend = eventView.project.some(projectId => + VITALS_PLATFORMS.includes( + projects.find(project => project.id === `${projectId}`)?.platform || '' + ) + ); - if (!showVitalsCard && !props.isAlwaysShown) { - return null; + 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(vitalName => { + const target = vitalDetailRouteWithQuery({ + orgSlug: organization.slug, + query: eventView.generateQueryStringObject(), + vitalName, + projectID: decodeList(location.query.project), + }); + + return ( + + + + ); + })} + + ); + }} ); } -export default withProjects(VitalsCards); - const VitalsContainer = styled('div')` display: grid; grid-template-columns: 1fr; @@ -118,18 +117,122 @@ const VitalsContainer = styled('div')` } `; -type CardProps = Omit & { - vitalName: WebVital; - tableData: any; - isLoading?: boolean; - noBorder?: boolean; - hideBar?: boolean; - hideEmptyState?: boolean; +type FrontendCardProps = { + isLoading: boolean; + result: any; + vital: WebVital; }; -const NonPanel = styled('div')``; +function FrontendCard(props: FrontendCardProps) { + const {isLoading, result, vital} = props; -const VitalCard = styled(Card)` + const value = isLoading ? '\u2014' : getP75(result, vital); + const chart = ; + + return ( + + ); +} + +type VitalBarProps = { + isLoading: boolean; + result: any; + vital: WebVital | WebVital[]; + value?: string; + showBar?: boolean; + showEmptyState?: boolean; + showDurationDetail?: boolean; + showVitalPercentNames?: boolean; +}; + +export function VitalBar(props: VitalBarProps) { + const { + isLoading, + result, + vital, + value, + showBar = true, + showEmptyState = true, + showDurationDetail = false, + showVitalPercentNames = false, + } = props; + + if (isLoading || !result) { + if (showEmptyState) { + // TODO(tonyx): loading state? + return null; + } + return null; + } + + 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 null; + } + + 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 +240,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 +263,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 +310,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..b84b2ba2008de1 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 + hideEmptyState 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 From 560c50bb126e5d7120fecd736a5480049b1911d0 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 13 Jan 2021 11:50:53 -0500 Subject: [PATCH 2/3] add loading/empty states --- .../views/performance/landing/vitalsCards.tsx | 24 ++++++++++++------- .../transactionVitals/vitalCard.tsx | 2 +- .../performance/vitalDetail/vitalInfo.tsx | 24 +++++++++---------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/sentry/static/sentry/app/views/performance/landing/vitalsCards.tsx b/src/sentry/static/sentry/app/views/performance/landing/vitalsCards.tsx index 2f1bbcb89877ca..cce9c5773aa468 100644 --- a/src/sentry/static/sentry/app/views/performance/landing/vitalsCards.tsx +++ b/src/sentry/static/sentry/app/views/performance/landing/vitalsCards.tsx @@ -3,7 +3,9 @@ import styled from '@emotion/styled'; import {Location} from 'history'; import Card from 'app/components/card'; +import EmptyStateWarning from 'app/components/emptyStateWarning'; import Link from 'app/components/links/link'; +import Placeholder from 'app/components/placeholder'; import QuestionTooltip from 'app/components/questionTooltip'; import {t} from 'app/locale'; import overflowEllipsis from 'app/styles/overflowEllipsis'; @@ -145,7 +147,7 @@ type VitalBarProps = { vital: WebVital | WebVital[]; value?: string; showBar?: boolean; - showEmptyState?: boolean; + showStates?: boolean; showDurationDetail?: boolean; showVitalPercentNames?: boolean; }; @@ -157,17 +159,21 @@ export function VitalBar(props: VitalBarProps) { vital, value, showBar = true, - showEmptyState = true, + showStates = false, showDurationDetail = false, showVitalPercentNames = false, } = props; - if (isLoading || !result) { - if (showEmptyState) { - // TODO(tonyx): loading state? - return null; - } - return null; + if (isLoading) { + return showStates ? : null; + } + + const emptyState = showStates ? ( + {t('No data available')} + ) : null; + + if (!result) { + return emptyState; } const counts: Counts = { @@ -183,7 +189,7 @@ export function VitalBar(props: VitalBarProps) { }); if (!counts.baseCount) { - return null; + return emptyState; } const p75: React.ReactNode = Array.isArray(vital) 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 b84b2ba2008de1..f1fa200250fd46 100644 --- a/src/sentry/static/sentry/app/views/performance/transactionVitals/vitalCard.tsx +++ b/src/sentry/static/sentry/app/views/performance/transactionVitals/vitalCard.tsx @@ -305,7 +305,7 @@ class VitalCard extends React.Component { location={location} vital={vital} hideBar - hideEmptyState + hideStates hideVitalPercentNames hideDurationDetail /> diff --git a/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalInfo.tsx b/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalInfo.tsx index dd9e293d012ce7..7544ab15cfc1cc 100644 --- a/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalInfo.tsx +++ b/src/sentry/static/sentry/app/views/performance/vitalDetail/vitalInfo.tsx @@ -14,7 +14,7 @@ type Props = { location: Location; vital: WebVital | WebVital[]; hideBar?: boolean; - hideEmptyState?: boolean; + hideStates?: boolean; hideVitalPercentNames?: boolean; hideDurationDetail?: boolean; }; @@ -26,7 +26,7 @@ export default function vitalInfo(props: Props) { organization, location, hideBar, - hideEmptyState, + hideStates, hideVitalPercentNames, hideDurationDetail, } = props; @@ -38,17 +38,15 @@ export default function vitalInfo(props: Props) { vitals={Array.isArray(vital) ? vital : [vital]} > {({isLoading, tableData}) => ( - - - + )} ); From 76c280f66827112e7df0ee314f576a25dcdfdacc Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 13 Jan 2021 12:08:47 -0500 Subject: [PATCH 3/3] reduce number of redundant components --- .../views/performance/landing/vitalsCards.tsx | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/sentry/static/sentry/app/views/performance/landing/vitalsCards.tsx b/src/sentry/static/sentry/app/views/performance/landing/vitalsCards.tsx index cce9c5773aa468..11b5f8dd77ff3d 100644 --- a/src/sentry/static/sentry/app/views/performance/landing/vitalsCards.tsx +++ b/src/sentry/static/sentry/app/views/performance/landing/vitalsCards.tsx @@ -80,21 +80,31 @@ export function FrontendCards(props: VitalsCardsProps) { const result = tableData?.data?.[0]; return ( - {vitals.map(vitalName => { + {vitals.map(vital => { const target = vitalDetailRouteWithQuery({ orgSlug: organization.slug, query: eventView.generateQueryStringObject(), - vitalName, + vitalName: vital, projectID: decodeList(location.query.project), }); + const value = isLoading ? '\u2014' : getP75(result, vital); + const chart = ( + + ); + return ( - + ); })} @@ -119,28 +129,6 @@ const VitalsContainer = styled('div')` } `; -type FrontendCardProps = { - isLoading: boolean; - result: any; - vital: WebVital; -}; - -function FrontendCard(props: FrontendCardProps) { - const {isLoading, result, vital} = props; - - const value = isLoading ? '\u2014' : getP75(result, vital); - const chart = ; - - return ( - - ); -} - type VitalBarProps = { isLoading: boolean; result: any;