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..f473a49bf43987 --- /dev/null +++ b/static/app/views/preprod/buildComparison/main/BuildComparisonMetricCards.tsx @@ -0,0 +1,168 @@ +import {useMemo, type ReactNode} from 'react'; + +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; + + const metrics = useMemo(() => { + if (!comparisonResults) { + return []; + } + + const labels = getLabels( + comparisonResponse?.head_build_details.app_info?.platform ?? undefined + ); + const {size_metric_diff_item} = comparisonResults; + + return [ + { + 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, + }, + ]; + }, [comparisonResults, comparisonResponse]); + + if (!comparisonResults) { + return null; + } + + 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'}; +} 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..535ac9f24b0b27 --- /dev/null +++ b/static/app/views/preprod/components/metricCard.tsx @@ -0,0 +1,68 @@ +import type {CSSProperties, ReactNode} from 'react'; + +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'; + +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 && ( +