import {Fragment} from 'react';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
import ExternalLink from 'sentry/components/links/externalLink';
import QuestionTooltip from 'sentry/components/questionTooltip';
import {Tooltip} from 'sentry/components/tooltip';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {TableData} from 'sentry/utils/discover/discoverQuery';
import {getDuration} from 'sentry/utils/formatters';
import {PERFORMANCE_SCORE_COLORS} from 'sentry/views/performance/browser/webVitals/utils/performanceScoreColors';
import {
scoreToStatus,
STATUS_TEXT,
} from 'sentry/views/performance/browser/webVitals/utils/scoreToStatus';
import type {
ProjectScore,
WebVitals,
} from 'sentry/views/performance/browser/webVitals/utils/types';
import {useReplaceFidWithInpSetting} from 'sentry/views/performance/browser/webVitals/utils/useReplaceFidWithInpSetting';
type Props = {
onClick?: (webVital: WebVitals) => void;
projectData?: TableData;
projectScore?: ProjectScore;
showTooltip?: boolean;
transaction?: string;
};
const WEB_VITALS_METERS_CONFIG = {
lcp: {
name: t('Largest Contentful Paint'),
formatter: (value: number) => getFormattedDuration(value / 1000),
},
fcp: {
name: t('First Contentful Paint'),
formatter: (value: number) => getFormattedDuration(value / 1000),
},
fid: {
name: t('First Input Delay'),
formatter: (value: number) => getFormattedDuration(value / 1000),
},
cls: {
name: t('Cumulative Layout Shift'),
formatter: (value: number) => Math.round(value * 100) / 100,
},
ttfb: {
name: t('Time To First Byte'),
formatter: (value: number) => getFormattedDuration(value / 1000),
},
};
const WEB_VITALS_METERS_CONFIG_WITH_INP = {
lcp: WEB_VITALS_METERS_CONFIG.lcp,
fcp: WEB_VITALS_METERS_CONFIG.fcp,
inp: {
name: t('Interaction to Next Paint'),
formatter: (value: number) => getFormattedDuration(value / 1000),
},
cls: WEB_VITALS_METERS_CONFIG.cls,
ttfb: WEB_VITALS_METERS_CONFIG.ttfb,
};
export default function WebVitalMeters({
onClick,
projectData,
projectScore,
showTooltip = true,
}: Props) {
const theme = useTheme();
const shouldReplaceFidWithInp = useReplaceFidWithInpSetting();
if (!projectScore) {
return null;
}
const webVitalsConfig = shouldReplaceFidWithInp
? WEB_VITALS_METERS_CONFIG_WITH_INP
: WEB_VITALS_METERS_CONFIG;
const webVitals = Object.keys(webVitalsConfig) as WebVitals[];
const colors = theme.charts.getColorPalette(3);
return (
{webVitals.map((webVital, index) => {
const webVitalExists = projectScore[`${webVital}Score`] !== undefined;
const formattedMeterValueText = webVitalExists ? (
webVitalsConfig[webVital].formatter(
projectData?.data?.[0]?.[`p75(measurements.${webVital})`] as number
)
) : (
);
const headerText = webVitalsConfig[webVital].name;
const meterBody = (
{showTooltip && (
{tct(
`The p75 [webVital] value and aggregate [webVital] score of your selected project(s).
Scores and values may share some (but not perfect) correlation.`,
{
webVital: webVital.toUpperCase(),
}
)}
{t('Find out how performance scores are calculated here.')}
}
/>
)}
{headerText}
{formattedMeterValueText}
);
return (
webVitalExists && onClick?.(webVital)}
clickable={webVitalExists}
>
{webVitalExists && }
{webVitalExists && meterBody}
{!webVitalExists && (
{meterBody}
)}
);
})}
);
}
export const getFormattedDuration = (value: number) => {
return getDuration(value, value < 1 ? 0 : 2, true);
};
const Container = styled('div')`
margin-bottom: ${space(1)};
`;
const Flex = styled('div')<{gap?: number}>`
display: flex;
flex-direction: row;
justify-content: center;
width: 100%;
gap: ${p => (p.gap ? `${p.gap}px` : space(1))};
align-items: center;
flex-wrap: wrap;
`;
const MeterBarContainer = styled('div')<{clickable?: boolean}>`
flex: 1;
position: relative;
padding: 0;
cursor: ${p => (p.clickable ? 'pointer' : 'default')};
min-width: 140px;
`;
const MeterBarBody = styled('div')`
border: 1px solid ${p => p.theme.gray200};
border-radius: ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0 0;
border-bottom: none;
padding: ${space(1)} 0 ${space(0.5)} 0;
`;
const MeterHeader = styled('div')`
font-size: ${p => p.theme.fontSizeSmall};
color: ${p => p.theme.textColor};
display: inline-block;
text-align: center;
width: 100%;
`;
const MeterValueText = styled('div')`
display: flex;
justify-content: center;
align-items: center;
font-size: ${p => p.theme.headerFontSize};
color: ${p => p.theme.textColor};
flex: 1;
text-align: center;
`;
function MeterBarFooter({score}: {score: number | undefined}) {
if (score === undefined) {
return (
{t('No Data')}
);
}
const status = scoreToStatus(score);
return (
{STATUS_TEXT[status]} {score}
);
}
const MeterBarFooterContainer = styled('div')<{status: string}>`
color: ${p => p.theme[PERFORMANCE_SCORE_COLORS[p.status].normal]};
border-radius: 0 0 ${p => p.theme.borderRadius} ${p => p.theme.borderRadius};
background-color: ${p => p.theme[PERFORMANCE_SCORE_COLORS[p.status].light]};
border: solid 1px ${p => p.theme[PERFORMANCE_SCORE_COLORS[p.status].normal]};
font-size: ${p => p.theme.fontSizeExtraSmall};
padding: ${space(0.5)};
text-align: center;
`;
const NoValueContainer = styled('span')`
color: ${p => p.theme.gray300};
font-size: ${p => p.theme.headerFontSize};
`;
function NoValue() {
return {' \u2014 '};
}
const StyledTooltip = styled(Tooltip)`
display: block;
width: 100%;
`;
const StyledQuestionTooltip = styled(QuestionTooltip)`
position: absolute;
right: ${space(1)};
`;
export const Dot = styled('span')<{color: string}>`
display: inline-block;
margin-right: ${space(1)};
border-radius: ${p => p.theme.borderRadius};
width: ${space(1)};
height: ${space(1)};
background-color: ${p => p.color};
`;