From 8aa6f38eab26550b3dd8d1cf4b5df3a1ff42904b Mon Sep 17 00:00:00 2001 From: Max Topolsky Date: Mon, 17 Nov 2025 14:23:35 -0500 Subject: [PATCH 1/3] abstract metric cards for build comparison --- .../main/BuildComparisonMetricCards.tsx | 157 ++++++++++++++++++ .../main/sizeCompareMainContent.tsx | 148 +---------------- .../views/preprod/components/metricCard.tsx | 89 ++++++++++ 3 files changed, 252 insertions(+), 142 deletions(-) create mode 100644 static/app/views/preprod/buildComparison/main/BuildComparisonMetricCards.tsx create mode 100644 static/app/views/preprod/components/metricCard.tsx diff --git a/static/app/views/preprod/buildComparison/main/BuildComparisonMetricCards.tsx b/static/app/views/preprod/buildComparison/main/BuildComparisonMetricCards.tsx new file mode 100644 index 00000000000000..36f3e3c9c467d1 --- /dev/null +++ b/static/app/views/preprod/buildComparison/main/BuildComparisonMetricCards.tsx @@ -0,0 +1,157 @@ +import type {ReactNode} from 'react'; +import styled from '@emotion/styled'; + +import {Flex, Stack} from '@sentry/scraps/layout'; +import {Heading, Text} from '@sentry/scraps/text'; + +import {PercentChange} from 'sentry/components/percentChange'; +import {IconArrow, IconCode, IconDownload} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import {formatBytesBase10} from 'sentry/utils/bytes/formatBytesBase10'; +import {MetricCard} from 'sentry/views/preprod/components/metricCard'; +import type { + SizeAnalysisComparisonResults, + SizeComparisonApiResponse, +} from 'sentry/views/preprod/types/appSizeTypes'; +import {getLabels} from 'sentry/views/preprod/utils/labelUtils'; + +interface BuildComparisonMetricCardsProps { + comparisonResponse: SizeComparisonApiResponse | undefined; + comparisonResults: SizeAnalysisComparisonResults | undefined; +} + +interface ComparisonMetric { + base: number; + diff: number; + head: number; + icon: ReactNode; + key: string; + percentageChange: number; + title: string; +} + +export function BuildComparisonMetricCards(props: BuildComparisonMetricCardsProps) { + const {comparisonResults, comparisonResponse} = props; + + if (!comparisonResults) { + return null; + } + + const labels = getLabels( + comparisonResponse?.head_build_details.app_info?.platform ?? undefined + ); + const {size_metric_diff_item} = comparisonResults; + + const metrics: ComparisonMetric[] = [ + { + key: 'install', + title: labels.installSizeLabel, + icon: , + head: size_metric_diff_item.head_install_size, + base: size_metric_diff_item.base_install_size, + diff: + size_metric_diff_item.head_install_size - size_metric_diff_item.base_install_size, + percentageChange: + size_metric_diff_item.base_install_size === 0 + ? 0 + : (size_metric_diff_item.head_install_size - + size_metric_diff_item.base_install_size) / + size_metric_diff_item.base_install_size, + }, + { + key: 'download', + title: labels.downloadSizeLabel, + icon: , + head: size_metric_diff_item.head_download_size, + base: size_metric_diff_item.base_download_size, + diff: + size_metric_diff_item.head_download_size - + size_metric_diff_item.base_download_size, + percentageChange: + size_metric_diff_item.base_download_size === 0 + ? 0 + : (size_metric_diff_item.head_download_size - + size_metric_diff_item.base_download_size) / + size_metric_diff_item.base_download_size, + }, + ]; + + return ( + + {metrics.map(metric => { + const {variant, icon} = getTrend(metric.diff); + + return ( + + + + {formatBytesBase10(metric.head)} + + {icon} + + {metric.diff > 0 ? '+' : metric.diff < 0 ? '-' : ''} + {formatBytesBase10(Math.abs(metric.diff))} + {metric.percentageChange !== 0 && ( + + {' ('} + + {')'} + + )} + + + + + + {t('Base Build Size:')} + + + {metric.base === 0 ? t('Not present') : formatBytesBase10(metric.base)} + + + + + ); + })} + + ); +} + +function getTrend(diff: number): { + variant: 'danger' | 'success' | 'muted'; + icon?: React.ReactNode; +} { + if (diff > 0) { + return { + variant: 'danger', + icon: , + }; + } + + if (diff < 0) { + return { + variant: 'success', + icon: , + }; + } + + return {variant: 'muted'}; +} + +const DiffText = styled(Text)` + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 0.25em; + + span { + display: inline-flex; + align-items: center; + white-space: nowrap; + } +`; diff --git a/static/app/views/preprod/buildComparison/main/sizeCompareMainContent.tsx b/static/app/views/preprod/buildComparison/main/sizeCompareMainContent.tsx index 2efd17e421d0ad..0382e6ca58edbb 100644 --- a/static/app/views/preprod/buildComparison/main/sizeCompareMainContent.tsx +++ b/static/app/views/preprod/buildComparison/main/sizeCompareMainContent.tsx @@ -1,6 +1,5 @@ import {useMemo, useState} from 'react'; import {useTheme} from '@emotion/react'; -import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; import {InputGroup} from '@sentry/scraps/input/inputGroup'; @@ -10,23 +9,15 @@ import {Heading, Text} from '@sentry/scraps/text'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import LoadingIndicator from 'sentry/components/loadingIndicator'; -import {PercentChange} from 'sentry/components/percentChange'; -import { - IconArrow, - IconChevron, - IconCode, - IconDownload, - IconRefresh, - IconSearch, -} from 'sentry/icons'; +import {IconChevron, IconRefresh, IconSearch} from 'sentry/icons'; import {t} from 'sentry/locale'; -import {formatBytesBase10} from 'sentry/utils/bytes/formatBytesBase10'; import {fetchMutation, useApiQuery, useMutation} from 'sentry/utils/queryClient'; import type {UseApiQueryResult} from 'sentry/utils/queryClient'; import type RequestError from 'sentry/utils/requestError/requestError'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; +import {BuildComparisonMetricCards} from 'sentry/views/preprod/buildComparison/main/BuildComparisonMetricCards'; import {SizeCompareItemDiffTable} from 'sentry/views/preprod/buildComparison/main/sizeCompareItemDiffTable'; import {SizeCompareSelectedBuilds} from 'sentry/views/preprod/buildComparison/main/sizeCompareSelectedBuilds'; import {BuildError} from 'sentry/views/preprod/components/buildError'; @@ -41,7 +32,6 @@ import type { SizeAnalysisComparisonResults, SizeComparisonApiResponse, } from 'sentry/views/preprod/types/appSizeTypes'; -import {getLabels} from 'sentry/views/preprod/utils/labelUtils'; function getMainComparison( response: SizeComparisonApiResponse | undefined @@ -121,54 +111,6 @@ export function SizeCompareMainContent() { }, }); - // Process the comparison data for metrics cards - const processedMetrics = useMemo(() => { - if (!comparisonDataQuery.data) { - return []; - } - - const {size_metric_diff_item} = comparisonDataQuery.data; - - // Calculate summary data - const installSizeDiff = - size_metric_diff_item.head_install_size - size_metric_diff_item.base_install_size; - const downloadSizeDiff = - size_metric_diff_item.head_download_size - size_metric_diff_item.base_download_size; - const installSizePercentage = - installSizeDiff / size_metric_diff_item.base_install_size; - const downloadSizePercentage = - downloadSizeDiff / size_metric_diff_item.base_download_size; - - const labels = getLabels( - sizeComparisonQuery.data?.head_build_details.app_info?.platform ?? undefined - ); - // Calculate metrics - const metrics = [ - { - title: labels.installSizeLabel, - head: size_metric_diff_item.head_install_size, - base: size_metric_diff_item.base_install_size, - diff: - size_metric_diff_item.head_install_size - - size_metric_diff_item.base_install_size, - percentageChange: installSizePercentage, - icon: IconCode, - }, - { - title: labels.downloadSizeLabel, - head: size_metric_diff_item.head_download_size, - base: size_metric_diff_item.base_download_size, - diff: - size_metric_diff_item.head_download_size - - size_metric_diff_item.base_download_size, - percentageChange: downloadSizePercentage, - icon: IconDownload, - }, - ]; - - return metrics; - }, [comparisonDataQuery.data, sizeComparisonQuery.data]); - // Filter diff items based on the toggle and search query const filteredDiffItems = useMemo(() => { if (!comparisonDataQuery.data?.diff_items) { @@ -292,75 +234,10 @@ export function SizeCompareMainContent() { }} /> - {/* Metrics Grid */} - - {processedMetrics.map((metric, index) => { - let variant: 'danger' | 'success' | 'muted' = 'muted'; - let icon: React.ReactNode | undefined; - if (metric.diff > 0) { - variant = 'danger'; - icon = ; - } else if (metric.diff < 0) { - variant = 'success'; - icon = ; - } - - return ( - - - - - - {metric.title} - - - - - {formatBytesBase10(metric.head)} - - {icon} - - {metric.diff > 0 ? '+' : metric.diff < 0 ? '-' : ''} - {formatBytesBase10(Math.abs(metric.diff))} - {metric.percentageChange && ( - - {' ('} - - {')'} - - )} - - - - - - {t('Comparison:')} - - - {metric.base === 0 - ? t('Not present') - : formatBytesBase10(metric.base)} - - - - - - ); - })} - + {/* Items Changed Section */} @@ -433,16 +310,3 @@ export function SizeCompareMainContent() { ); } - -const DiffText = styled(Text)` - display: inline-flex; - align-items: center; - flex-wrap: wrap; - gap: 0.25em; - - span { - display: inline-flex; - align-items: center; - white-space: nowrap; - } -`; diff --git a/static/app/views/preprod/components/metricCard.tsx b/static/app/views/preprod/components/metricCard.tsx new file mode 100644 index 00000000000000..1fe136195ea904 --- /dev/null +++ b/static/app/views/preprod/components/metricCard.tsx @@ -0,0 +1,89 @@ +import type {CSSProperties, ReactNode} from 'react'; +import styled from '@emotion/styled'; + +import {Flex, Stack} from '@sentry/scraps/layout'; +import {Text} from '@sentry/scraps/text'; +import {Tooltip} from '@sentry/scraps/tooltip'; + +interface MetricCardAction { + ariaLabel: string; + icon: ReactNode; + onClick: () => void; + tooltip: ReactNode; +} + +interface MetricCardProps { + children: ReactNode; + icon: ReactNode; + label: string; + action?: MetricCardAction; + labelTooltip?: ReactNode; + style?: CSSProperties; +} + +export function MetricCard(props: MetricCardProps) { + const {icon, label, labelTooltip, action, children, style} = props; + + return ( + + + + {icon} + {labelTooltip ? ( + + + {label} + + + ) : ( + + {label} + + )} + + {action && ( + + + {action.icon} + + + )} + + {children} + + ); +} + +const IconButton = styled('button')` + display: inline-flex; + align-items: center; + justify-content: center; + padding: ${p => p.theme.space['2xs']}; + border: none; + background: transparent; + cursor: pointer; + color: ${p => p.theme.white}; + border-radius: ${p => p.theme.borderRadius}; + + &:hover { + opacity: 0.8; + } + + &:focus-visible { + outline: 2px solid ${p => p.theme.white}; + outline-offset: 2px; + } +`; From f454474dec3001e6e8de30e2210c5a3603d2d234 Mon Sep 17 00:00:00 2001 From: Max Topolsky Date: Mon, 17 Nov 2025 15:04:25 -0500 Subject: [PATCH 2/3] remove styled components --- .../main/BuildComparisonMetricCards.tsx | 38 +++++++++-------- .../views/preprod/components/metricCard.tsx | 41 +++++-------------- 2 files changed, 31 insertions(+), 48 deletions(-) diff --git a/static/app/views/preprod/buildComparison/main/BuildComparisonMetricCards.tsx b/static/app/views/preprod/buildComparison/main/BuildComparisonMetricCards.tsx index 36f3e3c9c467d1..b35ca6baf1ec25 100644 --- a/static/app/views/preprod/buildComparison/main/BuildComparisonMetricCards.tsx +++ b/static/app/views/preprod/buildComparison/main/BuildComparisonMetricCards.tsx @@ -1,5 +1,4 @@ import type {ReactNode} from 'react'; -import styled from '@emotion/styled'; import {Flex, Stack} from '@sentry/scraps/layout'; import {Heading, Text} from '@sentry/scraps/text'; @@ -88,11 +87,29 @@ export function BuildComparisonMetricCards(props: BuildComparisonMetricCardsProp {formatBytesBase10(metric.head)} {icon} - + {metric.diff > 0 ? '+' : metric.diff < 0 ? '-' : ''} {formatBytesBase10(Math.abs(metric.diff))} {metric.percentageChange !== 0 && ( - + {' ('} )} - + @@ -142,16 +159,3 @@ function getTrend(diff: number): { return {variant: 'muted'}; } - -const DiffText = styled(Text)` - display: inline-flex; - align-items: center; - flex-wrap: wrap; - gap: 0.25em; - - span { - display: inline-flex; - align-items: center; - white-space: nowrap; - } -`; diff --git a/static/app/views/preprod/components/metricCard.tsx b/static/app/views/preprod/components/metricCard.tsx index 1fe136195ea904..535ac9f24b0b27 100644 --- a/static/app/views/preprod/components/metricCard.tsx +++ b/static/app/views/preprod/components/metricCard.tsx @@ -1,6 +1,6 @@ import type {CSSProperties, ReactNode} from 'react'; -import styled from '@emotion/styled'; +import {Button} from '@sentry/scraps/button/button'; import {Flex, Stack} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; @@ -51,39 +51,18 @@ export function MetricCard(props: MetricCardProps) { )} {action && ( - - - {action.icon} - - +