From 5c7029307cd4e1fece28904e11b4555f614fe834 Mon Sep 17 00:00:00 2001 From: k-fish Date: Thu, 21 Oct 2021 11:02:28 -0400 Subject: [PATCH 1/4] feat(perf): Add vital widget for performance landing This adds a vital widget that will pull the top list of worst vitals (for LCP at the moment) and list out the other vitals in terms of their good / meh / poor numbers, by count. --- .../app/components/charts/eventsRequest.tsx | 20 +- .../landing/views/frontendPageloadView.tsx | 10 + .../views/performance/landing/vitalsCards.tsx | 35 ++- .../widgets/components/widgetContainer.tsx | 3 + .../widgets/components/widgetHeader.tsx | 4 +- .../transforms/transformEventsToVitals.tsx | 31 ++ .../performance/landing/widgets/types.tsx | 6 +- .../widgets/widgets/lineChartListWidget.tsx | 2 +- .../widgets/widgets/singleFieldAreaWidget.tsx | 4 +- .../landing/widgets/widgets/vitalWidget.tsx | 295 ++++++++++++++++++ .../performance/vitalDetail/colorBar.tsx | 11 +- .../views/performance/vitalDetail/utils.tsx | 26 ++ .../performance/vitalDetail/vitalChart.tsx | 96 +++++- .../landing/widgets/widgetContainer.spec.jsx | 44 +++ 14 files changed, 552 insertions(+), 35 deletions(-) create mode 100644 static/app/views/performance/landing/widgets/transforms/transformEventsToVitals.tsx create mode 100644 static/app/views/performance/landing/widgets/widgets/vitalWidget.tsx diff --git a/static/app/components/charts/eventsRequest.tsx b/static/app/components/charts/eventsRequest.tsx index 716be26600628a..8dfeff961818dc 100644 --- a/static/app/components/charts/eventsRequest.tsx +++ b/static/app/components/charts/eventsRequest.tsx @@ -40,12 +40,9 @@ type LoadingStatus = { errored: boolean; }; -// Chart format for multiple series. -type MultiSeriesResults = Series[]; - export type RenderProps = LoadingStatus & TimeSeriesData & { - results?: MultiSeriesResults; + results?: Series[]; // Chart with multiple series. }; type DefaultProps = { @@ -483,15 +480,16 @@ class EventsRequest extends React.PureComponent a[0] - b[0]); - const results: MultiSeriesResults = sortedTimeseriesData.map(item => { + const results: Series[] = sortedTimeseriesData.map(item => { return item[1]; }); - const previousTimeseriesData: MultiSeriesResults | undefined = - sortedTimeseriesData.some(item => item[2] === null) - ? undefined - : sortedTimeseriesData.map(item => { - return item[2] as Series; - }); + const previousTimeseriesData: Series[] | undefined = sortedTimeseriesData.some( + item => item[2] === null + ) + ? undefined + : sortedTimeseriesData.map(item => { + return item[2] as Series; + }); return children({ loading, diff --git a/static/app/views/performance/landing/views/frontendPageloadView.tsx b/static/app/views/performance/landing/views/frontendPageloadView.tsx index 261178143ca589..ca48c24d5a914a 100644 --- a/static/app/views/performance/landing/views/frontendPageloadView.tsx +++ b/static/app/views/performance/landing/views/frontendPageloadView.tsx @@ -2,12 +2,22 @@ import {usePageError} from 'app/utils/performance/contexts/pageError'; import Table from '../../table'; import {FRONTEND_PAGELOAD_COLUMN_TITLES} from '../data'; +import {DoubleChartRow} from '../widgets/components/widgetChartRow'; +import {PerformanceWidgetSetting} from '../widgets/widgetDefinitions'; import {BasePerformanceViewProps} from './types'; export function FrontendPageloadView(props: BasePerformanceViewProps) { return (
+ - {showBar && } - - {showDurationDetail && p75 && ( -
- {t('The p75 for all transactions is ')} - {p75} -
- )} - -
+ {showBar && } + {showDetail && ( + + {showDurationDetail && p75 && ( +
+ {t('The p75 for all transactions is ')} + {p75} +
+ )} + + +
+ )} ); } diff --git a/static/app/views/performance/landing/widgets/components/widgetContainer.tsx b/static/app/views/performance/landing/widgets/components/widgetContainer.tsx index 282d1c5b88a549..80c04dc7bd4275 100644 --- a/static/app/views/performance/landing/widgets/components/widgetContainer.tsx +++ b/static/app/views/performance/landing/widgets/components/widgetContainer.tsx @@ -13,6 +13,7 @@ import {PerformanceWidgetSetting, WIDGET_DEFINITIONS} from '../widgetDefinitions import {LineChartListWidget} from '../widgets/lineChartListWidget'; import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget'; import {TrendsWidget} from '../widgets/trendsWidget'; +import {VitalWidget} from '../widgets/vitalWidget'; import {ChartRowProps} from './widgetChartRow'; @@ -93,6 +94,8 @@ const _WidgetContainer = (props: Props) => { return ; case GenericPerformanceWidgetDataType.area: return ; + case GenericPerformanceWidgetDataType.vitals: + return ; case GenericPerformanceWidgetDataType.line_list: return ; default: diff --git a/static/app/views/performance/landing/widgets/components/widgetHeader.tsx b/static/app/views/performance/landing/widgets/components/widgetHeader.tsx index ae2ca978c9bb28..8d805b0d6c19c3 100644 --- a/static/app/views/performance/landing/widgets/components/widgetHeader.tsx +++ b/static/app/views/performance/landing/widgets/components/widgetHeader.tsx @@ -13,7 +13,7 @@ import { export function WidgetHeader( props: GenericPerformanceWidgetProps & WidgetDataProps ) { - const {title, titleTooltip, subtitle, HeaderActions} = props; + const {title, titleTooltip, Subtitle, HeaderActions} = props; return ( @@ -21,7 +21,7 @@ export function WidgetHeader( {title} -
{subtitle ? subtitle : null}
+
{Subtitle ? : null}
{HeaderActions && ( diff --git a/static/app/views/performance/landing/widgets/transforms/transformEventsToVitals.tsx b/static/app/views/performance/landing/widgets/transforms/transformEventsToVitals.tsx new file mode 100644 index 00000000000000..6dba8a47b084f6 --- /dev/null +++ b/static/app/views/performance/landing/widgets/transforms/transformEventsToVitals.tsx @@ -0,0 +1,31 @@ +import {RenderProps} from 'app/components/charts/eventsRequest'; +import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams'; +import {defined} from 'app/utils'; + +import {QueryDefinitionWithKey, WidgetDataConstraint, WidgetPropUnion} from '../types'; + +export function transformEventsRequestToVitals( + widgetProps: WidgetPropUnion, + results: RenderProps, + _: QueryDefinitionWithKey +) { + const {start, end, utc, interval, statsPeriod} = getParams(widgetProps.location.query); + + const data = results.results ?? []; + + const childData = { + ...results, + isLoading: results.loading, + isErrored: results.errored, + hasData: defined(data) && !!data.length && !!data[0].data.length, + data, + + utc: utc === 'true', + interval, + statsPeriod: statsPeriod ?? undefined, + start: start ?? '', + end: end ?? '', + }; + + return childData; +} diff --git a/static/app/views/performance/landing/widgets/types.tsx b/static/app/views/performance/landing/widgets/types.tsx index a71bce61d8845e..8c8b8990a3a799 100644 --- a/static/app/views/performance/landing/widgets/types.tsx +++ b/static/app/views/performance/landing/widgets/types.tsx @@ -94,11 +94,14 @@ type HeaderActions = FunctionComponent<{ widgetData: T; }>; +type Subtitle = FunctionComponent<{ + widgetData: T; +}>; + export type GenericPerformanceWidgetProps = { // Header; title: string; titleTooltip: string; - subtitle?: JSX.Element; fields: string[]; chartHeight: number; @@ -109,6 +112,7 @@ export type GenericPerformanceWidgetProps = { organization: Organization; // Components + Subtitle?: Subtitle; HeaderActions?: HeaderActions; EmptyComponent?: FunctionComponent<{height?: number}>; diff --git a/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx b/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx index ae81598274417a..d20645d424899f 100644 --- a/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx +++ b/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx @@ -153,7 +153,7 @@ export function LineChartListWidget(props: Props) { return ( {...props} - subtitle={{t('Suggested transactions')}} + Subtitle={() => {t('Suggested transactions')}} HeaderActions={provided => ( )} diff --git a/static/app/views/performance/landing/widgets/widgets/singleFieldAreaWidget.tsx b/static/app/views/performance/landing/widgets/widgets/singleFieldAreaWidget.tsx index f942f5ca67bc6c..86335f4fd470f9 100644 --- a/static/app/views/performance/landing/widgets/widgets/singleFieldAreaWidget.tsx +++ b/static/app/views/performance/landing/widgets/widgets/singleFieldAreaWidget.tsx @@ -62,9 +62,9 @@ export function SingleFieldAreaWidget(props: Props) { return ( {...props} - subtitle={ + Subtitle={() => ( {t('Compared to last %s ', globalSelection.datetime.period)} - } + )} HeaderActions={provided => ( diff --git a/static/app/views/performance/landing/widgets/widgets/vitalWidget.tsx b/static/app/views/performance/landing/widgets/widgets/vitalWidget.tsx new file mode 100644 index 00000000000000..57ed6e813f7852 --- /dev/null +++ b/static/app/views/performance/landing/widgets/widgets/vitalWidget.tsx @@ -0,0 +1,295 @@ +import {Fragment, FunctionComponent, useMemo, useState} from 'react'; +import styled from '@emotion/styled'; +import {Location} from 'history'; + +import Button from 'app/components/button'; +import _EventsRequest from 'app/components/charts/eventsRequest'; +import Link from 'app/components/links/link'; +import Truncate from 'app/components/truncate'; +import {IconClose} from 'app/icons'; +import {t} from 'app/locale'; +import space from 'app/styles/space'; +import {Organization} from 'app/types'; +import DiscoverQuery, {TableDataRow} from 'app/utils/discover/discoverQuery'; +import EventView from 'app/utils/discover/eventView'; +import {WebVital} from 'app/utils/discover/fields'; +import {VitalData} from 'app/utils/performance/vitals/vitalsCardsDiscoverQuery'; +import {decodeList} from 'app/utils/queryString'; +import {MutableSearch} from 'app/utils/tokenizeSearch'; +import withApi from 'app/utils/withApi'; +import {vitalDetailRouteWithQuery} from 'app/views/performance/vitalDetail/utils'; +import {_VitalChart} from 'app/views/performance/vitalDetail/vitalChart'; + +import {VitalBar} from '../../vitalsCards'; +import {GenericPerformanceWidget} from '../components/performanceWidget'; +import SelectableList, {RightAlignedCell} from '../components/selectableList'; +import {transformDiscoverToList} from '../transforms/transformDiscoverToList'; +import {transformEventsRequestToVitals} from '../transforms/transformEventsToVitals'; +import {QueryDefinition, WidgetDataResult} from '../types'; +import {PerformanceWidgetSetting} from '../widgetDefinitions'; + +type Props = { + title: string; + titleTooltip: string; + fields: string[]; + chartColor?: string; + + eventView: EventView; + location: Location; + organization: Organization; + chartSetting: PerformanceWidgetSetting; + + ContainerActions: FunctionComponent<{isLoading: boolean}>; +}; + +type DataType = { + list: WidgetDataResult & ReturnType; + chart: WidgetDataResult & ReturnType; +}; + +export function VitalWidget(props: Props) { + const {ContainerActions, eventView, organization, location} = props; + const [selectedListIndex, setSelectListIndex] = useState(0); + + const Queries = { + list: useMemo>( + () => ({ + fields: props.fields[0], + component: provided => { + const _eventView = provided.eventView.clone(); + + const fieldFromProps = props.fields.map(field => ({ + field, + })); + + _eventView.sorts = [{kind: 'desc', field: props.fields[0]}]; + + _eventView.fields = [ + {field: 'transaction'}, + {field: 'title'}, + {field: 'project.id'}, + ...fieldFromProps, + ]; + const mutableSearch = new MutableSearch(_eventView.query); + _eventView.query = mutableSearch.formatString(); + return ; + }, + transform: transformDiscoverToList, + }), + [props.eventView, props.fields, props.organization.slug] + ), + chart: useMemo( + () => ({ + enabled: widgetData => { + return !!widgetData?.list?.data?.length; + }, + fields: props.fields, + component: provided => { + const _eventView = provided.eventView.clone(); + + _eventView.additionalConditions.setFilterValues('transaction', [ + provided.widgetData.list.data[selectedListIndex].transaction as string, + ]); + + return ( + + ); + }, + transform: transformEventsRequestToVitals, + }), + [props.eventView, selectedListIndex, props.organization.slug] + ), + }; + + const settingToVital: {[x: string]: WebVital} = { + [PerformanceWidgetSetting.WORST_LCP_VITALS]: WebVital.LCP, + }; + + const handleViewAllClick = () => { + // TODO(k-fish): Add analytics. + }; + + return ( + + {...props} + Subtitle={provided => { + const listItem = provided.widgetData.list?.data[selectedListIndex]; + + if (!listItem) { + return ; + } + + const data = { + [settingToVital[props.chartSetting]]: getVitalDataForListItem(listItem), + }; + + return ( + + + + ); + }} + HeaderActions={provided => { + const vital = settingToVital[props.chartSetting]; + const target = vitalDetailRouteWithQuery({ + orgSlug: organization.slug, + query: eventView.generateQueryStringObject(), + vitalName: vital, + projectID: decodeList(location.query.project), + }); + + return ( + +
+ +
+ +
+ ); + }} + Queries={Queries} + Visualizations={[ + { + component: provided => ( + <_VitalChart + {...provided.widgetData.chart} + {...provided} + field={props.fields[0]} + organization={organization} + query={eventView.query} + project={eventView.project} + environment={eventView.environment} + grid={{ + left: space(0), + right: space(0), + top: space(2), + bottom: space(2), + }} + /> + ), + height: 160, + }, + { + component: provided => ( + () => { + const transaction = listItem.transaction as string; + const _eventView = eventView.clone(); + + const initialConditions = new MutableSearch(_eventView.query); + initialConditions.addFilterValues('transaction', [transaction]); + + const vital = settingToVital[props.chartSetting]; + + _eventView.query = initialConditions.formatString(); + + const target = vitalDetailRouteWithQuery({ + orgSlug: organization.slug, + query: _eventView.generateQueryStringObject(), + vitalName: vital, + projectID: decodeList(location.query.project), + }); + + const data = { + [settingToVital[props.chartSetting]]: getVitalDataForListItem(listItem), + }; + + return ( + + + + + + + + + { + excludeTransaction(listItem.transaction, props); + setSelectListIndex(0); + }} + /> + + + ); + })} + /> + ), + height: 200, + noPadding: true, + }, + ]} + /> + ); +} + +function getVitalDataForListItem(listItem: TableDataRow) { + const poorData: number = + (listItem.count_if_measurements_lcp_greaterOrEquals_4000 as number) || 0; + const mehData: number = (listItem['equation[0]'] as number) || 0; + const goodData: number = (listItem['equation[1]'] as number) || 0; + const _vitalData = { + poor: poorData, + meh: mehData, + good: goodData, + p75: 0, + }; + const vitalData: VitalData = { + ..._vitalData, + total: _vitalData.poor + _vitalData.meh + _vitalData.good, + }; + + return vitalData; +} + +const EventsRequest = withApi(_EventsRequest); +const Subtitle = styled('span')` + color: ${p => p.theme.gray300}; + font-size: ${p => p.theme.fontSizeMedium}; +`; +const CloseContainer = styled('div')` + display: flex; + align-items: center; + justify-content: center; +`; +const GrowLink = styled(Link)` + flex-grow: 1; +`; + +const StyledIconClose = styled(IconClose)` + cursor: pointer; + color: ${p => p.theme.gray200}; + &:hover { + color: ${p => p.theme.gray300}; + } +`; diff --git a/static/app/views/performance/vitalDetail/colorBar.tsx b/static/app/views/performance/vitalDetail/colorBar.tsx index 93d4acfc4285a7..39789688911578 100644 --- a/static/app/views/performance/vitalDetail/colorBar.tsx +++ b/static/app/views/performance/vitalDetail/colorBar.tsx @@ -10,11 +10,15 @@ type ColorStop = { type Props = { colorStops: ColorStop[]; + barHeight?: number; }; const ColorBar = (props: Props) => { return ( - percent)}> + percent)} + > {props.colorStops.map(colorStop => { return ; })} @@ -24,17 +28,18 @@ const ColorBar = (props: Props) => { type VitalBarProps = { fractions: number[]; + barHeight?: number; }; const VitalBar = styled('div')` - height: 16px; + height: ${p => (p.barHeight ? `${p.barHeight}px` : '16px')}; width: 100%; overflow: hidden; position: relative; background: ${p => p.theme.gray100}; display: grid; grid-template-columns: ${p => p.fractions.map(f => `${f}fr`).join(' ')}; - margin-bottom: ${space(1)}; + margin-bottom: ${p => (p.barHeight ? '' : space(1))}; border-radius: 2px; `; diff --git a/static/app/views/performance/vitalDetail/utils.tsx b/static/app/views/performance/vitalDetail/utils.tsx index 79c1a7b9c0109f..c8f94ce765e0cc 100644 --- a/static/app/views/performance/vitalDetail/utils.tsx +++ b/static/app/views/performance/vitalDetail/utils.tsx @@ -33,6 +33,32 @@ export enum VitalState { GOOD = 'Good', } +// For mapping field equations for the vitals widget +export function fieldToVital(field: string) { + const FIELD_TO_VITAL_MAP: Record = { + 'count_if(measurements.lcp,greaterOrEquals,0) - count_if(measurements.lcp,greaterOrEquals,2500)': + VitalState.GOOD, + 'count_if(measurements.lcp,greaterOrEquals,2500) - count_if(measurements.lcp,greaterOrEquals,4000)': + VitalState.MEH, + 'count_if(measurements.lcp,greaterOrEquals,4000)': VitalState.POOR, + }; + + return FIELD_TO_VITAL_MAP[field]; +} + +// For mapping field equations for the vitals widget +export function vitalToField(vital: VitalState) { + const VITAL_MAP: Record = { + [VitalState.GOOD]: + 'count_if(measurements.lcp,greaterOrEquals,0) - count_if(measurements.lcp,greaterOrEquals,2500)', + [VitalState.MEH]: + 'count_if(measurements.lcp,greaterOrEquals,2500) - count_if(measurements.lcp,greaterOrEquals,4000)', + [VitalState.POOR]: 'count_if(measurements.lcp,greaterOrEquals,4000)', + }; + + return VITAL_MAP[vital]; +} + export const vitalStateColors: Record = { [VitalState.POOR]: 'red300', [VitalState.MEH]: 'yellow300', diff --git a/static/app/views/performance/vitalDetail/vitalChart.tsx b/static/app/views/performance/vitalDetail/vitalChart.tsx index 94df6b04a5b78b..e37ef6040fb126 100644 --- a/static/app/views/performance/vitalDetail/vitalChart.tsx +++ b/static/app/views/performance/vitalDetail/vitalChart.tsx @@ -17,6 +17,7 @@ import QuestionTooltip from 'app/components/questionTooltip'; import {IconWarning} from 'app/icons'; import {t} from 'app/locale'; import {OrganizationSummary} from 'app/types'; +import {Series} from 'app/types/echarts'; import {getUtcToLocalDateObject} from 'app/utils/dates'; import {axisLabelFormatter, tooltipFormatter} from 'app/utils/discover/charts'; import EventView from 'app/utils/discover/eventView'; @@ -27,7 +28,14 @@ import useApi from 'app/utils/useApi'; import {replaceSeriesName, transformEventStatsSmoothed} from '../trends/utils'; -import {getMaxOfSeries, vitalNameFromLocation, webVitalMeh, webVitalPoor} from './utils'; +import { + fieldToVital, + getMaxOfSeries, + vitalNameFromLocation, + vitalStateColors, + webVitalMeh, + webVitalPoor, +} from './utils'; const QUERY_KEYS = [ 'environment', @@ -271,3 +279,89 @@ function VitalChart({ } export default withRouter(VitalChart); + +export type _VitalChartProps = Props & { + data?: Series[]; + loading: boolean; + reloading: boolean; + field: string; + height?: number; + grid: LineChart['props']['grid']; +}; + +function __VitalChart(props: _VitalChartProps) { + const {field: yAxis, data: _results, loading, reloading, height, grid} = props; + if (!_results) { + return null; + } + const theme = useTheme(); + + const chartOptions = { + grid, + seriesOptions: { + showSymbol: false, + }, + tooltip: { + trigger: 'axis' as const, + valueFormatter: (value: number, seriesName?: string) => + tooltipFormatter(value, seriesName), + }, + xAxis: { + axisLine: { + show: false, + }, + axisTick: { + show: false, + }, + splitLine: { + show: false, + }, + }, + yAxis: { + axisLabel: { + color: theme.chartLabel, + showMaxLabel: false, + formatter: (value: number) => axisLabelFormatter(value, yAxis), + }, + }, + }; + + const results = _results.filter(r => !!fieldToVital(r.seriesName)); + + const {smoothedResults} = transformEventStatsSmoothed(results); + + const smoothedSeries = smoothedResults + ? smoothedResults.map(({seriesName, ...rest}) => { + return { + seriesName: fieldToVital(seriesName) || 'count', + ...rest, + color: theme[vitalStateColors[fieldToVital(seriesName)]], + lineStyle: { + opacity: 1, + width: 2, + }, + }; + }) + : []; + + return ( +
+ + + {getDynamicText({ + value: ( + {}} + series={[...smoothedSeries]} + /> + ), + fixed: 'Web Vitals Chart', + })} + +
+ ); +} + +export const _VitalChart = withRouter(__VitalChart); diff --git a/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx b/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx index 078e81950b631c..1dbb94dee68332 100644 --- a/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx +++ b/tests/js/spec/views/performance/landing/widgets/widgetContainer.spec.jsx @@ -146,6 +146,50 @@ describe('Performance > Widgets > WidgetContainer', function () { ); }); + it('Worst LCP widget', async function () { + const data = initializeData(); + + const wrapper = mountWithTheme( + , + data.routerContext + ); + await tick(); + wrapper.update(); + + expect(wrapper.find('div[data-test-id="performance-widget-title"]').text()).toEqual( + 'Worst LCP Web Vitals' + ); + expect(wrapper.find('a[data-test-id="view-all-button"]').text()).toEqual('View All'); + expect(eventsV2Mock).toHaveBeenCalledTimes(1); + expect(eventsV2Mock).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.objectContaining({ + query: expect.objectContaining({ + environment: [], + field: [ + 'transaction', + 'title', + 'project.id', + 'count_if(measurements.lcp,greaterOrEquals,4000)', + 'count_if(measurements.lcp,greaterOrEquals,2500)', + 'count_if(measurements.lcp,greaterOrEquals,0)', + 'equation|count_if(measurements.lcp,greaterOrEquals,2500) - count_if(measurements.lcp,greaterOrEquals,4000)', + 'equation|count_if(measurements.lcp,greaterOrEquals,0) - count_if(measurements.lcp,greaterOrEquals,2500)', + ], + per_page: 3, + project: [], + query: '', + sort: '-count_if(measurements.lcp,greaterOrEquals,4000)', + statsPeriod: '14d', + }), + }) + ); + }); + it('Most errors widget', async function () { const data = initializeData(); From 8df6685c5609141615db8d071dee0b282118031d Mon Sep 17 00:00:00 2001 From: k-fish Date: Thu, 21 Oct 2021 14:38:52 -0400 Subject: [PATCH 2/4] Fix some issues after merge master --- .../views/performance/landing/widgets/widgets/trendsWidget.tsx | 2 +- .../views/performance/landing/widgets/widgets/vitalWidget.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx b/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx index 9204839bc0e858..a77c8cc40c8dea 100644 --- a/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx +++ b/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx @@ -91,7 +91,7 @@ export function TrendsWidget(props: Props) { return ( {...rest} - subtitle={{t('Trending Transactions')}} + Subtitle={() => {t('Trending Transactions')}} HeaderActions={provided => } Queries={Queries} Visualizations={[ diff --git a/static/app/views/performance/landing/widgets/widgets/vitalWidget.tsx b/static/app/views/performance/landing/widgets/widgets/vitalWidget.tsx index 57ed6e813f7852..3ef075d9dadd16 100644 --- a/static/app/views/performance/landing/widgets/widgets/vitalWidget.tsx +++ b/static/app/views/performance/landing/widgets/widgets/vitalWidget.tsx @@ -20,6 +20,7 @@ import withApi from 'app/utils/withApi'; import {vitalDetailRouteWithQuery} from 'app/views/performance/vitalDetail/utils'; import {_VitalChart} from 'app/views/performance/vitalDetail/vitalChart'; +import {excludeTransaction} from '../../utils'; import {VitalBar} from '../../vitalsCards'; import {GenericPerformanceWidget} from '../components/performanceWidget'; import SelectableList, {RightAlignedCell} from '../components/selectableList'; From ce0d0608b45ef01ab42fa63c309de8a9feb27898 Mon Sep 17 00:00:00 2001 From: k-fish Date: Fri, 22 Oct 2021 13:19:13 -0400 Subject: [PATCH 3/4] Fix subtitle component --- .../performance/landing/widgets/widgets/histogramWidget.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/performance/landing/widgets/widgets/histogramWidget.tsx b/static/app/views/performance/landing/widgets/widgets/histogramWidget.tsx index 97a77d1cf5e537..3d39713bb103da 100644 --- a/static/app/views/performance/landing/widgets/widgets/histogramWidget.tsx +++ b/static/app/views/performance/landing/widgets/widgets/histogramWidget.tsx @@ -51,9 +51,9 @@ export function HistogramWidget(props: Props) { return ( {...props} - subtitle={ + Subtitle={() => ( {t('Compared to last %s ', globalSelection.datetime.period)} - } + )} HeaderActions={provided => ( From 59f1fcf460d1faeadf9fa1a2923fc757e872750d Mon Sep 17 00:00:00 2001 From: k-fish Date: Fri, 22 Oct 2021 15:00:25 -0400 Subject: [PATCH 4/4] Fix landing test --- tests/js/spec/views/performance/landing/index.spec.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/js/spec/views/performance/landing/index.spec.tsx b/tests/js/spec/views/performance/landing/index.spec.tsx index fff86183f1564c..c6ed36484f4406 100644 --- a/tests/js/spec/views/performance/landing/index.spec.tsx +++ b/tests/js/spec/views/performance/landing/index.spec.tsx @@ -106,11 +106,13 @@ describe('Performance > Landing > Index', function () { expect(wrapper.find('Table')).toHaveLength(1); const titles = wrapper.find('div[data-test-id="performance-widget-title"]'); - expect(titles).toHaveLength(3); + expect(titles).toHaveLength(5); - expect(titles.at(0).text()).toEqual('p75 LCP'); - expect(titles.at(1).text()).toEqual('LCP Distribution'); - expect(titles.at(2).text()).toEqual('FCP Distribution'); + expect(titles.at(0).text()).toEqual('Transactions Per Minute'); + expect(titles.at(1).text()).toEqual('Most Related Errors'); + expect(titles.at(2).text()).toEqual('p75 LCP'); + expect(titles.at(3).text()).toEqual('LCP Distribution'); + expect(titles.at(4).text()).toEqual('FCP Distribution'); }); it('renders frontend other view', async function () {